Chương 15: Anti-Disassembly
1. Giới thiệu
Anti-disassembly là kỹ thuật tạo ra mã hoặc dữ liệu đặc biệt khiến công cụ phân tích disassembly tạo ra danh sách lệnh sai so với những gì thực sự được thực thi.
Mục tiêu của kỹ thuật này:
- Làm chậm hoặc ngăn chặn quá trình phân tích mã độc
- Vô hiệu hóa các công cụ phát hiện tự động (antivirus heuristic, similarity detection)
- Tăng độ khó yêu cầu kỹ năng cao hơn từ phía analyst
2. Hiểu về Disassembly
Disassembly không đơn giản: cùng một chuỗi byte có thể cho ra nhiều cách diễn giải lệnh khác nhau. Malware author lợi dụng điều này để tạo ra chuỗi lệnh trông khác với những gì CPU thực sự chạy.
2.1. Hai loại thuật toán Disassembler
3. Linear Disassembly
Nguyên lý
Duyệt tuần tự qua từng byte, disassemble từng lệnh một mà không quan tâm đến flow control.
// Ví dụ implement Linear Disassembly bằng libdisasm
char buffer[BUF_SIZE];
int position = 0;
while (position < BUF_SIZE) {
x86_insn_t insn;
int size = x86_disasm(buf, BUF_SIZE, 0, position, &insn);
if (size != 0) {
char disassembly_line[1024];
x86_format_insn(&insn, disassembly_line, 1024, intel_syntax);
printf("%s\n", disassembly_line);
position += size; // (1) Tiến theo kích thước lệnh hợp lệ
} else {
position++; // (2) Lệnh không hợp lệ → tiến 1 byte
}
}4. Flow-Oriented Disassembly
Nguyên lý
Theo dõi luồng thực thi thực sự: khi gặp jmp, jz, call — disassembler thêm target vào danh sách chờ để disassemble sau.
5. Các kỹ thuật Anti-Disassembly cụ thể
5.1. Jump cùng Target (Opaque Predicate kép)
Cơ chế: Hai lệnh nhảy có điều kiện ngược nhau, cùng trỏ đến một địa chỉ → thực chất là jmp vô điều kiện, nhưng disassembler không nhận ra.
74 03 jz short loc_4011C5 ; nếu ZF=1 → nhảy
75 01 jnz short loc_4011C5 ; nếu ZF=0 → nhảy
; → luôn nhảy, KHÔNG BAO GIỜ rơi vào byte dưới
E8 db 0E8h ; ← rogue byte, disassembler bị lừa đọc đây là CALL
; ---------------------
loc_4011C5:
58 pop eax
C3 retnFix trong IDA Pro:
- Đặt cursor lên byte
0xE8, nhấnD→ chuyển thành data - Đặt cursor lên
loc_4011C5, nhấnC→ chuyển thành code
5.2. Điều kiện Hằng (Single False Conditional)
Cơ chế: Dùng một conditional jump mà điều kiện luôn luôn đúng tại runtime, nhưng disassembler không biết điều đó.
33 C0 xor eax, eax ; ZF luôn = 1 sau lệnh này
74 01 jz loc_4011C5 ; điều kiện luôn đúng → luôn nhảy
; disassembler xử lý false branch trước:
E9 db 0E9h ; ← rogue byte (opcode JMP 5-byte), che 4 byte tiếp theo
; ---------------------
loc_4011C5:
58 pop eax
C3 retn5.3. Impossible Disassembly (Byte dùng chung cho nhiều lệnh)
Cơ chế nâng cao: Một byte là một phần của hai lệnh khác nhau đều được thực thi. Không có disassembler nào hiện tại biểu diễn được điều này.
Ví dụ đơn giản (4 byte):
EB FF C0 48EB FF:JMP -1→ nhảy đến byteFF(byte thứ 2 của chính lệnh JMP)FF C0:INC EAX48:DEC EAX- Kết quả thực tế: EAX tăng rồi giảm = NOP phức tạp
- Disassembler không thể biểu diễn
FFvừa thuộcJMPvừa thuộcINC EAX
Ví dụ phức tạp hơn:
66 B8 EB 05 mov ax, 5EBh ; ← byte EB 05 vừa là operand, vừa là lệnh JMP +5
31 C0 xor eax, eax ; → ZF luôn = 1
74 F9 jz -7 ; nhảy về byte EB 05 (giữa lệnh MOV ở trên!)
E8 ... call <fake> ; ← KHÔNG BAO GIỜ thực thi, nhưng disassembler thấy
; --- Thực ra nhảy đến:
EB 05 jmp +5 ; từ byte EB 05 trong operand của MOV
58 pop eax
C3 retnFix bằng IDAPython:
def NopBytes(start, length):
for i in range(0, length):
PatchByte(start + i, 0x90)
MakeCode(start)
NopBytes(0x004011C0, 4) # NOP phần MOV ax bị dùng chung
NopBytes(0x004011C6, 3) # NOP jz + E8 rogueKết quả sau khi fix:
90 nop
90 nop
90 nop
90 nop
31 C0 xor eax, eax
90 nop
90 nop
90 nop
58 pop eax
C3 retnScript NOP tiện lợi với hotkey ALT-N trong IDA Pro:
import idaapi
idaapi.CompileLine('static n_key() { RunPythonStatement("nopIt()"); }')
AddHotkey("Alt-N", "n_key")
def nopIt():
start = ScreenEA()
end = NextHead(start)
for ea in range(start, end):
PatchByte(ea, 0x90)
Jump(end)
Refresh()6. Obscuring Flow Control (Che giấu luồng điều khiển)
6.1. Function Pointer
Vấn đề: IDA Pro có thể phát hiện khi địa chỉ hàm được load vào biến, nhưng không phát hiện được tất cả các chỗ gọi qua pointer đó.
mov [ebp+var_4], offset sub_4011C0 ; IDA thấy reference này
call [ebp+var_4] ; ← IDA Pro KHÔNG thêm cross-ref
call [ebp+var_4] ; ← IDA Pro KHÔNG thêm cross-refFix bằng IDC:
AddCodeXref(0x004011DE, 0x004011C0, fl_CF);
AddCodeXref(0x004011EA, 0x004011C0, fl_CF);6.2. Return Pointer Abuse
Cơ chế: Dùng retn như một jmp tùy ý bằng cách thao túng giá trị trên stack.
004011C0 call $+5 ; push địa chỉ 004011C5 lên stack
004011C5 add [esp+0], 5 ; cộng thêm 5 → stack top = 004011CA
004011C9 retn ; pop và nhảy đến 004011CA
; ← IDA Pro kết thúc hàm ở đây, nghĩ rằng hàm đã xong
004011CA push ebp ; ← hàm THẬT bắt đầu ở đây, IDA không biết
004011CB mov ebp, esp
...
004011D6 retnFix:
- NOP 3 lệnh đầu (
call $+5,add [esp],retn) - Nhấn ALT-P trong IDA Pro → điều chỉnh lại function boundary
6.3. Structured Exception Handling (SEH) Abuse
Cơ chế: Dùng SEH để chuyển flow đến một hàm mà disassembler không thấy bất kỳ cross-reference nào.
Cấu trúc SEH chain:
struct _EXCEPTION_REGISTRATION {
DWORD prev; // con trỏ đến record trước
DWORD handler; // con trỏ đến hàm xử lý exception
};SEH chain được trỏ bởi fs:[0] → là linked list trên stack, grows upward.
Thêm handler vào đầu chain:
push ExceptionHandler ; địa chỉ hàm handler
push fs:[0] ; record hiện tại (prev)
mov fs:[0], esp ; đặt record mới lên đầu chainKích hoạt exception để trigger handler:
xor ecx, ecx ; ECX = 0
div ecx ; divide by zero → exception! → gọi ExceptionHandlerKhôi phục stack trong handler:
mov esp, [esp+8] ; lấy lại ESP ban đầu (OS thêm 1 handler nữa → +8)
mov eax, fs:[0]
mov eax, [eax]
mov eax, [eax]
mov fs:[0], eax ; unlink cả 2 handler
add esp, 8
; → tiếp tục code thật6.4. Defeating Stack-Frame Analysis
Cơ chế: Tạo conditional branch với điều kiện hằng liên quan đến ESP → disassembler chọn sai nhánh → phân tích stack frame sai → IDA Pro báo hàm có 62 tham số thay vì 0.
sub esp, 8
sub esp, 4
cmp esp, 1000h ; ESP luôn > 0x1000 trong Windows process bình thường
jl short loc_less ; false branch: IDA xử lý trước, tin tưởng hơn
add esp, 4 ; true path: chỉ undo lệnh sub esp, 4 ở trên
jmp short loc_done
loc_less:
add esp, 104h ; IDA bị lừa tin đây là path thật → stack offset sai
loc_done:
; ... code thật với ESP-based frame ...7. Tổng kết
| Kỹ thuật | Tấn công vào | Fix trong IDA Pro |
|---|---|---|
| jz + jnz cùng target | Flow-oriented false branch | D trên rogue byte, C trên target |
| xor + jz hằng | False branch trust | D + C |
| Byte dùng chung | Giới hạn biểu diễn của disassembler | PatchByte NOP |
| Function pointer | Cross-reference tracking | AddCodeXref |
retn abuse | Function boundary detection | NOP + ALT-P |
| SEH handler | Không có cross-ref | C trên vùng data ẩn |
| Stack frame defeat | ESP analysis | ALT-K hoặc NOP |