Chương 4: Crash Course in x86 Disassembly


Tại sao cần Disassembly?

Phân tích tĩnh cơ bản (static analysis) chỉ như nhìn bên ngoài xác chết trong buổi khám nghiệm tử thi — bạn thấy được cấu trúc nhưng không hiểu cơ chế hoạt động bên trong. Phân tích động cơ bản (dynamic analysis) có thể cho biết malware phản ứng thế nào, nhưng không thể tiết lộ format của gói tin hay logic xử lý.

Disassembly lấp đầy khoảng trống đó: chuyển đổi machine code → assembly code để analyst đọc và phân tích.


Các tầng trừu tượng (Levels of Abstraction)

Interpreted Languages   (C#, Java, .NET, Perl) → Bytecode → Interpreter
High-level Languages    (C, C++) → Compiler → Machine Code
Low-level Languages     (Assembly) ← Disassembler ← Machine Code
Machine Code            (Opcodes / hex)
Microcode               (Firmware - gắn với phần cứng cụ thể)
Hardware                (Digital logic: XOR, AND, OR, NOT gates)

Lưu ý quan trọng: Assembly là ngôn ngữ cấp cao nhất có thể được khôi phục đáng tin cậy và nhất quán từ machine code khi không có source code.

TầngMô tảLiên quan đến malware analysis?
HardwareMạch điện vật lýKhông
Microcode/FirmwareVi lệnh cho phần cứng cụ thểHiếm khi
Machine CodeOpcodes (hex)Đây là dạng lưu trên disk
Low-level (Assembly)Human-readable, mnemonicsAnalyst làm việc tại đây
High-level (C/C++)Logic trừu tượngMalware author viết tại đây
InterpretedBytecode + interpreterMột số malware dùng

Kiến trúc Von Neumann & x86

graph LR subgraph CPU CU[Control Unit
EIP - Instruction Pointer] ALU[Arithmetic Logic Unit] REG[Registers] CU <--> ALU ALU <--> REG end RAM[Main Memory
RAM] IO[I/O Devices
HDD, Keyboard, Monitor] CPU <--> RAM CPU <--> IO
  • Control Unit: Lấy lệnh từ RAM qua register EIP (instruction pointer)
  • ALU: Thực thi lệnh, lưu kết quả vào registers hoặc memory
  • Registers: Bộ nhớ nhỏ trong CPU, truy cập nhanh hơn RAM nhiều

Tại sao malware chủ yếu là x86? 32-bit Windows được thiết kế chạy trên x86. AMD64/Intel 64 cũng hỗ trợ chạy binary x86 32-bit. → Hầu hết malware được compile cho x86.


Memory Layout

High Memory Address
┌─────────────┐
│    Data     │  ← Static/global values, loaded khi program khởi động
├─────────────┤
│    Code     │  ← Instructions CPU fetch để execute
├─────────────┤
│    Heap     │  ← Dynamic memory (malloc/free), thay đổi liên tục
├─────────────┤
│    Stack    │  ← Local variables, function parameters, return addresses
└─────────────┘
Low Memory Address
Vùng nhớĐặc điểmGhi chú
DataStatic/global, set khi loadÍt thay đổi khi chạy
CodeCPU instructionsReadonly (thường)
HeapDynamic allocationmalloc/free trong C
StackLIFO, local vars + paramsTăng về phía địa chỉ thấp

Instructions — Cấu trúc lệnh Assembly

Mỗi lệnh gồm: mnemonic [destination_operand], [source_operand]

mov ecx, 0x42    ; mnemonic=mov, dest=ecx, src=0x42

Opcodes và Endianness

Instruction:  mov ecx, 0x42
Opcodes:      B9 42 00 00 00
              ^  ^-----------^
              |  0x42 dạng little-endian (0x42000000 → đọc là 0x42)
              mov ecx

Ba loại Operands

LoạiVí dụÝ nghĩa
Immediate0x42Giá trị cố định (literal)
RegisterecxTham chiếu đến register
Memory address[eax], [ebx+8]Giá trị tại địa chỉ memory, dùng dấu []

Registers — Thanh ghi x86

Bốn nhóm register

graph TD REG[x86 Registers] REG --> GEN[General Registers
EAX EBX ECX EDX EBP ESP ESI EDI] REG --> SEG[Segment Registers
CS SS DS ES FS GS] REG --> FLAG[Status Register
EFLAGS] REG --> IP[Instruction Pointer
EIP]

General Registers — Chi tiết

32-bit16-bit8-bit High8-bit LowConvention phổ biến
EAXAXAHALReturn value của function call
EBXBXBHBLBase pointer (dữ liệu)
ECXCXCHCLCounter (vòng lặp, rep)
EDXDXDHDLKết hợp với EAX cho mul/div
EBPBPBase pointer của stack frame
ESPSPStack pointer (top of stack)
ESISISource index (string ops)
EDIDIDestination index (string ops)

Ví dụ phân rã EAX (giá trị 0xA9DC81F5):

EAX  [A9][DC][81][F5]   32-bit = 0xA9DC81F5
AX        [81][F5]      16-bit = 0x81F5
AH        [81]           8-bit = 0x81
AL            [F5]       8-bit = 0xF5

EFLAGS — Status Register

32-bit, mỗi bit là một flag. Các flag quan trọng nhất:

FlagTênKhi nào được set (=1)?
ZFZero FlagKết quả operation = 0
CFCarry FlagKết quả quá lớn/quá nhỏ cho destination
SFSign FlagKết quả âm, hoặc MSB được set sau arithmetic
TFTrap FlagDebug mode: CPU chỉ execute 1 lệnh/lần

EIP — Instruction Pointer

EIP lưu địa chỉ của lệnh tiếp theo sẽ được thực thi.


Các lệnh thông dụng

mov — Di chuyển dữ liệu

Format: mov destination, source

mov eax, ebx          ; EAX = giá trị của EBX
mov eax, 0x42         ; EAX = 0x42 (immediate)
mov eax, [0x4037C4]   ; EAX = 4 bytes tại địa chỉ 0x4037C4
mov eax, [ebx]        ; EAX = 4 bytes tại địa chỉ chứa trong EBX
mov eax, [ebx+esi*4]  ; EAX = 4 bytes tại địa chỉ (EBX + ESI*4)

lea — Load Effective Address

lea eax, [ebx+8]   ; EAX = địa chỉ (EBX+8) — KHÔNG phải giá trị tại đó
mov eax, [ebx+8]   ; EAX = GIÁ TRỊ tại địa chỉ (EBX+8)

Tại sao dùng lea? Còn dùng để tính toán hiệu quả hơn:

lea ebx, [eax*5+5]   ; ebx = (eax+1)*5  — chỉ 1 lệnh
; Thay vì:
inc eax
mov ecx, 5
mul ecx
mov ebx, eax         ; 4 lệnh

Arithmetic — Số học

add eax, ebx      ; EAX = EAX + EBX
sub eax, 0x10     ; EAX = EAX - 0x10  (set ZF nếu = 0, CF nếu underflow)
inc edx           ; EDX = EDX + 1
dec ecx           ; ECX = ECX - 1

mul 0x50          ; EDX:EAX = EAX * 0x50  (kết quả 64-bit!)
div 0x75          ; EAX = EDX:EAX / 0x75, EDX = remainder

Logical & Shift

xor eax, eax       ; EAX = 0  (trick tối ưu: 2 bytes thay vì 5 bytes của mov eax,0)
or  eax, 0x7575    ; EAX = EAX OR 0x7575
and eax, 0xFF      ; EAX = EAX AND 0xFF

shl eax, 1         ; EAX << 1 = EAX * 2
shl eax, 2         ; EAX << 2 = EAX * 4
shr eax, 1         ; EAX >> 1 = EAX / 2

ror bl, 2          ; Rotate right: bits "rơi ra" bên phải quay lại bên trái
rol bl, 2          ; Rotate left

nop — No Operation

nop   ; Opcode: 0x90 — không làm gì cả, nhảy sang lệnh kế
; Thực ra   danh của: xchg eax, eax

Dùng để làm gì? NOP sled trong buffer overflow: tạo vùng đệm để shellcode không bị execute từ giữa:

[NOP][NOP][NOP]...[NOP][SHELLCODE]
     ↑ Jump vào đây cũng sẽ trượt đến shellcode

Stack — Ngăn xếp

Cơ chế LIFO

PUSH 1 → PUSH 2 → PUSH 3
POP → 3  POP → 2  POP → 1

Stack phát triển từ địa chỉ cao xuống thấp:

High Address  ←── Stack base
              [   ...   ]
              [   arg2  ]
              [   arg1  ]
              [ret addr ]  ← CALL tự push EIP vào đây
              [ old EBP ]  ← PUSH EBP
              [ local 1 ]
              [ local 2 ]
ESP ────────→ [ local N ]  ← Top of stack (địa chỉ thấp nhất đang dùng)
Low Address
RegisterVai trò
ESPStack pointer — trỏ đến đỉnh stack (thay đổi liên tục)
EBPBase pointer — cố định trong suốt 1 function, dùng làm mốc tham chiếu

Function Call Flow (cdecl convention)

sequenceDiagram participant Caller participant Stack participant Callee Caller->>Stack: PUSH arguments (right to left) Caller->>Stack: CALL → auto PUSH EIP (return address) Callee->>Stack: PUSH EBP (save caller's base pointer) Callee->>Stack: MOV EBP, ESP (set new base pointer) Callee->>Stack: SUB ESP, N (allocate local variables) Note over Callee: Function body executes... Callee->>Stack: MOV ESP, EBP + POP EBP (epilogue/leave) Callee->>Stack: RET → POP return address into EIP Caller->>Stack: ADD ESP, N (clean up arguments)

Prologue (đầu hàm):

push ebp          ; lưu EBP của caller
mov  ebp, esp     ; thiết lập frame mới
sub  esp, 0x40    ; cấp phát local variables

Epilogue (cuối hàm):

leave             ; = mov esp, ebp + pop ebp
ret               ; pop return address  EIP

Conditionals & Branching

So sánh: cmptest

cmp dst, src    ; giống SUB nhưng không lưu kết quả, chỉ set flags
test eax, eax   ; giống AND nhưng không lưu kết quả  check NULL nhanh
cmp dst, srcZFCF
dst == src10
dst < src01
dst > src00

Conditional Jumps (Jcc)

jz  loc   ; jump nếu ZF=1 (bằng 0)
jnz loc   ; jump nếu ZF=0 (khác 0)
je  loc   ; jump nếu bằng nhau (sau cmp)
jne loc   ; jump nếu khác nhau
jg  loc   ; jump nếu dst > src (signed)
jl  loc   ; jump nếu dst < src (signed)
ja  loc   ; jump nếu dst > src (unsigned)
jb  loc   ; jump nếu dst < src (unsigned)
jo  loc   ; jump nếu overflow flag set
js  loc   ; jump nếu sign flag set
jecxz loc ; jump nếu ECX = 0

REP Instructions — Thao tác data buffer

ESI = source, EDI = destination, ECX = counter

InstructionC equivalentMô tả
rep movsbmemcpy()Copy ECX bytes từ ESI → EDI
repe cmpsbmemcmp()So sánh 2 buffer, dừng khi khác nhau hoặc ECX=0
rep stosbmemset()Ghi giá trị AL vào ECX bytes tại EDI
repne scasbstrchr()-likeTìm byte AL trong buffer tại EDI
; Ví dụ memset - zero out buffer
xor  eax, eax      ; AL = 0
mov  ecx, 100      ; 100 bytes
lea  edi, [buffer]
rep stosb          ; fill 100 bytes with 0

Prefix termination:

PrefixDừng khi
repECX = 0
repe/repzECX = 0 hoặc ZF = 0
repne/repnzECX = 0 hoặc ZF = 1

C main() trong Assembly

int main(int argc, char** argv)

Trên hệ thống 32-bit, mỗi pointer = 4 bytes:

; Kiểm tra argc == 3
cmp [ebp+argc], 3
jz  loc_continue

; Truy cập argv[1] = argv + 4 (offset 4)
mov eax, [ebp+argv]   ; eax = argv (base pointer của array)
mov ecx, [eax+4]      ; ecx = argv[1]  (offset +4)
push ecx
push offset "-r"
push 2
call strncmp

; Truy cập argv[2] = argv + 8 (offset 8)
mov eax, [ebp+argv]
mov ecx, [eax+8]      ; ecx = argv[2]  (offset +8)
push ecx
call DeleteFileA

Tổng kết — Checklist phân tích