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

graph TD A[Disassembler Algorithms] --> B[Linear Disassembly] A --> C[Flow-Oriented Disassembly] B --> D["Duyệt tuần tự từng byte, bất kể flow control"] C --> E["Theo dõi luồng thực thi thực sự"] D --> F["Dễ implement, dễ bị đánh lừa"] E --> G["Chính xác hơn, nhưng vẫn có điểm yếu"]

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      retn
graph LR JZ["JZ +3"] -->|always| POP JNZ["JNZ +1"] -->|always| POP JZ --> FAKE["0xE8 (fake CALL opcode)"] FAKE -->|disassembler bị lừa| WRONG["4 byte tiếp theo bị ăn mất"] POP["POP EAX → RETN"]

Fix trong IDA Pro:

  • Đặt cursor lên byte 0xE8, nhấn D → chuyển thành data
  • Đặt cursor lên loc_4011C5, nhấn C → 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      retn

5.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 48
graph LR A["EB FF → JMP -1 (nhảy vào chính byte FF)"] B["FF C0 → INC EAX"] C["48 → DEC EAX"] A -->|"CPU thực thi"| B --> C
  • EB FF: JMP -1 → nhảy đến byte FF (byte thứ 2 của chính lệnh JMP)
  • FF C0: INC EAX
  • 48: 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 FF vừa thuộc JMP vừa thuộc INC 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            retn

Fix 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 rogue

Kế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          retn

Script 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-ref

Fix 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  retn

Fix:

  • 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 chain

Kích hoạt exception để trigger handler:

xor ecx, ecx    ; ECX = 0
div ecx         ; divide by zero  exception!  gọi ExceptionHandler

Khô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ật

6.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

mindmap root((Anti-Disassembly)) Defeating Algorithms Linear: chèn data/rogue byte Flow-oriented: opaque predicate jz + jnz cùng target xor eax eax + jz Impossible Disassembly 1 byte thuộc 2 lệnh JMP inward MOV + JZ multilevel Obscuring Flow Control Function Pointer Return Pointer Abuse SEH Handler Stack Frame Defeat
Kỹ thuậtTấn công vàoFix trong IDA Pro
jz + jnz cùng targetFlow-oriented false branchD trên rogue byte, C trên target
xor + jz hằngFalse branch trustD + C
Byte dùng chungGiới hạn biểu diễn của disassemblerPatchByte NOP
Function pointerCross-reference trackingAddCodeXref
retn abuseFunction boundary detectionNOP + ALT-P
SEH handlerKhông có cross-refC trên vùng data ẩn
Stack frame defeatESP analysisALT-K hoặc NOP