1405 words
7 minutes
FLARE Learning Hub - Chapter 7: Intermediate Disassembly

Chapter 7 của “Malware Analysis Crash Course” (FLARE/Mandiant) là chương dài nhất tính tới giờ — gom một loạt cấu trúc dữ liệu/lập trình bậc cao (con trỏ, mảng, vòng lặp, struct, switch) và cách chúng thực sự trông như thế nào khi bị compile xuống assembly.

mandiant
/
flare-learning-hub
Waiting for api.github.com...
00K
0K
0K
Waiting...

Pointers and Memory#

Pointer ở cấp assembly chỉ là một giá trị đại diện cho một địa chỉ bộ nhớ, thể hiện theo 2 cách:

  • Pointer như một biến — dùng LEA để tính địa chỉ rồi lưu vào thanh ghi/bộ nhớ (giống hệt toán tử & trong C).
  • Pointer như một giá trị — một số nguyên nằm sẵn trong bộ nhớ, tình cờ được dùng để truy cập bộ nhớ khác (dereference) — về mặt vận hành không khác gì nhau, chỉ khác ở ngữ cảnh sử dụng.

Pattern hay gặp: LEA tính địa chỉ → MOV ghi địa chỉ đó vào một ô nhớ khác (biến con trỏ) → code sau đó đọc lại ô nhớ đó rồi dereference để lấy giá trị thật sự được trỏ tới.

mov eax, [ebp-4]      ; EAX = giá trị con trỏ (một địa chỉ)
mov ecx, [eax]          ; ECX = giá trị TẠI địa chỉ đó (dereference)

MUL và IMUL#

MUL src — nhân không dấu (unsigned), chỉ nhận 1 operand tường minh, operand còn lại ngầm định theo kích thước:

Kích thướcPhép tínhKết quả
8-bitAL × r/m8→ AX
16-bitAX × r/m16→ DX:AX
32-bitEAX × r/m32→ EDX:EAX

Phần cao của kết quả nằm ở EDX (hoặc DX/AH), phần thấp ở EAX (hoặc AX/AL). Ví dụ 10 × 7 = 70 với phép nhân 32-bit: EAX = 70, EDX = 0 (vì kết quả không tràn qua 32-bit).

IMUL — nhân có dấu (signed), có 3 dạng:

imul ebx               ; 1 toán hạng: EAX * EBX -> EDX:EAX (giống MUL nhưng có dấu)
imul eax, ebx            ; 2 toán hạng: EAX = EAX * EBX (cắt bớt nếu tràn)
imul eax, ebx, 4          ; 3 toán hạng: EAX = EBX * 4 (cắt bớt nếu tràn)

Dạng 2 và 3 toán hạng sẽ cắt bớt (truncate) kết quả nếu vượt quá kích thước đích — hiếm khi gây vấn đề vì dạng này chủ yếu dùng để tính offset (thường là số nhỏ).

Arrays#

Mảng không tồn tại như kiểu dữ liệu riêng ở cấp assembly — chỉ là công thức: địa chỉ phần tử = base + index × kích_thước_phần_tử. Dấu hiệu nhận diện mảng trong disassembly:

  • Truy cập bộ nhớ dùng base register cộng với offset tính từ một index
  • Vòng lặp truy cập bộ nhớ tuần tự với index tăng dần

Compiler hay dùng SHL để nhân nhanh thay vì MUL/IMUL khi kích thước phần tử là lũy thừa của 2 (SHL 1 bước = nhân 2, 2 bước = nhân 4…). Mảng cục bộ nằm trên stack, mảng toàn cục nằm trong .data — cả hai dùng chung công thức index, chỉ khác địa chỉ base.

Loops#

Vòng lặp for kiểu C gồm 4 thành phần, ánh xạ sang assembly:

  • Initialization — chạy 1 lần duy nhất, thường là MOV gán biến đếm = 0
  • Condition — kiểm tra trước mỗi vòng lặp: CMP biến đếm với giới hạn, theo sau là 1 lệnh nhảy có điều kiện. Compiler hay đảo logic: thay vì “tiếp tục nếu i < length”, nó check “thoát nếu i >= length” (ví dụ dùng JNB - Jump if Not Below)
  • Body — thân vòng lặp, chạy có điều kiện
  • Update — chạy sau mỗi vòng body, thường INC biến đếm rồi nhảy ngược về bước Condition

Ở chế độ graph view của IDA, mũi tên đi ngược từ khối Update trở lại khối Condition chính là dấu hiệu trực quan nhận ra vòng lặp.

String Instructions: MOVS, STOS, SCAS#

Nhóm lệnh thao tác hiệu quả trên chuỗi byte/word/dword, luôn dùng ESI (nguồn) và/hoặc EDI (đích):

  • MOVS (Move String) — copy từ [ESI] sang [EDI]. Hậu tố quy định kích thước: MOVSB (byte), MOVSW (word), MOVSD (dword). Sau mỗi lần chạy, cả ESI lẫn EDI đều tự tăng đúng bằng kích thước vừa copy.
  • STOS (Store String) — ghi giá trị trong EAX/AX/AL vào [EDI]. STOSB/STOSW/STOSD theo kích thước. Khi EAX được zero trước (xor eax, eax), STOS trở thành cách hiệu quả để zero hóa một vùng nhớ. EDI tự tăng theo kích thước đã ghi.
  • SCAS (Scan String) — so sánh EAX/AX/AL với [EDI], chỉ set flag (không sửa gì). SCASB so AL với [EDI]. EDI cũng tự tăng sau mỗi lần so sánh.

Direction Flag (DF) điều khiển chiều tăng/giảm của ESI/EDI sau mỗi lệnh: DF = 0 (mặc định, qua lệnh CLD) thì tăng, DF = 1 (qua lệnh STD) thì giảm.

Instruction Repeat Prefixes#

Biến các lệnh string ở trên thành vòng lặp tự động, dùng ECX làm bộ đếm:

  • REP — lặp lại trong khi ECX > 0, tự giảm ECX sau mỗi lần, dừng khi ECX == 0. Dùng với MOVS/STOS.
  • REPE/REPZ — lặp trong khi (ZF == 1 VÀ ECX > 0). Dùng với SCAS/CMPS, dừng ngay khi gặp giá trị khác hoặc hết đếm.
  • REPNE/REPNZ — lặp trong khi (ZF == 0 VÀ ECX > 0) — ngược lại, dừng khi gặp giá trị bằng hoặc hết đếm.
rep movsd              ; copy ECX dword liên tục từ [ESI] sang [EDI]
xor eax, eax
rep stosb               ; ghi 0 vào ECX byte liên tiếp tại [EDI] -> zero hóa buffer
xor al, al
repne scasb             ; quét tìm byte 0x00 (NUL) trong chuỗi tại [EDI] -> tìm độ dài string

Intrinsic Functions#

Intrinsic là hàm thư viện chuẩn được compiler “biết” và thay thế trực tiếp bằng chuỗi lệnh tối ưu ngay tại chỗ gọi, thay vì phát sinh một lời gọi hàm thật:

Hàm CThường được inline thành
memcpy()rep movs*
memset()rep stos* (thường kèm set EAX/AL trước)
strlen()repne scasb (tìm byte NUL)

Đây là lý do nhiều khi code nguồn rõ ràng gọi memcpy/memset/strlen nhưng disassembly lại không hề có CALL nào tới các hàm đó — chúng đã bị “biến mất” vào chuỗi rep mov/stos/scas ngay tại vị trí gọi.

Structs#

Thông tin kiểu struct biến mất hoàn toàn sau khi compile — một struct cấp phát trên stack trông không khác gì các biến cục bộ độc lập. Để nhận ra struct, cần tìm:

  • Kích thước cấp phát — một lệnh memset/STOS khởi tạo nguyên một vùng nhớ liên tục gợi ý cả vùng đó thuộc về nhau
  • Pattern truy cập — nhiều lệnh cùng cộng các offset cố định (0, 4, 8…) vào cùng một base chung, gợi ý đó là các field riêng biệt của 1 struct

Trong IDA: tạo struct mới qua Local Types (phím Shift+F1) → chuột phải → Add type. Thêm field bằng phím D (nhấn lặp lại để chuyển qua lại byte/word/dword); field dạng mảng thì chuột phải chọn Array. Sau khi định nghĩa xong, áp dụng cho biến qua Edit → Struct var — tên field sẽ tự động thay thế mọi offset số khô khan ở nơi struct đó được dùng.

Lưu ý về padding: compiler C tự chèn khoảng trống để căn chỉnh field (alignment) — ví dụ field int 4-byte luôn bắt đầu ở địa chỉ chia hết cho 4, nên nếu phía trước nó là 1 field 1-byte, sẽ xuất hiện khoảng trống (padding) giữa 2 field trong bộ nhớ.

Switch Statements and Jump Tables#

Compiler chọn 1 trong 2 chiến lược tùy số lượng case:

  • Ít case — sinh chuỗi CMP + nhảy có điều kiện nối tiếp nhau, về bản chất giống hệt if/else lồng nhau.
  • Nhiều case liên tiếp — dùng jump table: một mảng chứa sẵn địa chỉ của từng case, nhảy gián tiếp qua đó.

Cơ chế jump table gồm 3 bước:

  1. Tính index = giá trị switch trừ đi case nhỏ nhất (ví dụ case từ 'a' tới 'd', trừ 'a' cho ra index 0-3)
  2. Kiểm tra bound — nếu index vượt quá case lớn nhất, nhảy tới nhánh default
  3. Index vào bảng địa chỉ: jmp [jump_table + index*4]
cmp eax, 4              ; kiểm tra case nằm trong khoảng hợp lệ (0-4)
ja default_case
jmp [jump_table + eax*4]   ; nhảy gián tiếp theo index

jump_table:
    dd case_0, case_1, case_2, case_3, case_4

IDA tự gán các jump table dạng này tiền tố jpt_ khi nhận diện được. Nhận ra pattern jmp [table + reg*4] quan trọng vì control flow graph tĩnh khó suy ra hết các đích nhảy có thể — IDA cần tự nhận diện đúng jump table mới vẽ đủ toàn bộ nhánh của switch.


Chapter này khép lại phần assembly 32-bit “kinh điển”. Chapter 8 mình chuyển sang x86-64 — tập thanh ghi mở rộng và calling convention hoàn toàn khác so với 32-bit.

FLARE Learning Hub - Chapter 7: Intermediate Disassembly
https://stewmalwarehunter.id.vn/posts/flare-macc-ch7---intermediate-disassembly/
Author
Stew
Published at
2026-09-18
License
CC BY-NC-SA 4.0