Chương 6: Nhận Diện Các Cấu Trúc Code C Trong Assembly


Tổng quan

Mục tiêu: Không phân tích từng lệnh assembly riêng lẻ — mà nhận diện nhóm lệnh để hiểu chức năng ở mức cao.

Malware thường được viết bằng C. Mỗi chương trình C được tạo từ các code construct (cấu trúc code): loop, if, switch, struct, linked list,… Khi nhận diện được các construct này trong assembly, ta có thể hiểu chương trình nhanh hơn nhiều.


1. Biến Global vs Local

So sánh C code

// Global — khai báo ngoài hàm
int x = 1;
int y = 2;
void main() { x = x + y; printf("Total = %d\n", x); }

// Local — khai báo trong hàm
void main() {
    int x = 1;
    int y = 2;
    x = x + y;
    printf("Total = %d\n", x);
}

Assembly: Global Variable

mov eax, dword_40CF60      ; load x từ địa chỉ bộ nhớ cố định
add eax, dword_40C000      ; cộng y
mov dword_40CF60, eax      ; ghi lại vào bộ nhớ → ảnh hưởng toàn chương trình
mov ecx, dword_40CF60
push ecx
push offset aTotalD        ; "total = %d\n"
call printf

Assembly: Local Variable

mov dword ptr [ebp-4], 0   ; x = 0 (trên stack)
mov dword ptr [ebp-8], 1   ; y = 1 (trên stack)
mov eax, [ebp-4]
add eax, [ebp-8]
mov [ebp-4], eax
mov ecx, [ebp-4]
push ecx
push offset aTotalD        ; "total = %d\n"
call printf
Quy tắc nhận diện
  • Global variable → tham chiếu bằng địa chỉ bộ nhớ cố định (vd: dword_40CF60)
  • Local variable → tham chiếu bằng offset từ ebp trên stack (vd: [ebp-4], [ebp+var_4])

2. Arithmetic Operations (Phép toán số học)

C code ví dụ

int a = 0;
int b = 1;
a = a + 11;
a = a - b;
a--;
b++;
b = a % 3;

Assembly tương ứng

mov [ebp+var_4], 0          ; a = 0
mov [ebp+var_8], 1          ; b = 1
mov eax, [ebp+var_4]
add eax, 0Bh                ; a = a + 11 (0x0B = 11)
mov [ebp+var_4], eax
mov ecx, [ebp+var_4]
sub ecx, [ebp+var_8]        ; a = a - b
mov [ebp+var_4], ecx
mov edx, [ebp+var_4]
sub edx, 1                  ; a-- (compiler dùng sub thay vì dec)
mov [ebp+var_4], edx
mov eax, [ebp+var_8]
add eax, 1                  ; b++ (compiler dùng add thay vì inc)
mov [ebp+var_8], eax
mov eax, [ebp+var_4]
cdq                         ; mở rộng eax sang edx:eax (chuẩn bị chia)
mov ecx, 3
idiv ecx                    ; edx:eax / ecx → eax = thương, edx = dư
mov [ebp+var_8], edx        ; b = a % 3 (lấy phần )

3. If Statements

C code

int x = 1;
int y = 2;
if (x == y) {
    printf("x equals y.\n");
} else {
    printf("x is not equal to y.\n");
}

Assembly

mov [ebp+var_8], 1
mov [ebp+var_4], 2
mov eax, [ebp+var_8]
cmp eax, [ebp+var_4]           ; so sánh x và y
jnz short loc_40102B           ; nếu KHÔNG bằng → nhảy sang else
push offset aXEqualsY_         ; "x equals y.\n"
call printf
add esp, 4
jmp short loc_401038           ; nhảy qua else
loc_40102B:
push offset aXIsNotEqualToY    ; "x is not equal to y.\n"
call printf
Đọc graph trong IDA Pro
IDA Pro hiển thị false / true tại điểm quyết định. Hai nhánh dẫn đến cuối hàm. Rất hữu ích để thấy ngay cấu trúc if-else.

Nested If Statement

int x = 0, y = 1, z = 2;
if (x == y) {
    if (z == 0) printf("z is zero and x = y.\n");
    else        printf("z is non-zero and x = y.\n");
} else {
    if (z == 0) printf("z zero and x != y.\n");
    else        printf("z non-zero and x != y.\n");
}
cmp eax, [ebp+var_4]
jnz short loc_401047       ; if x != y → else branch
cmp [ebp+var_C], 0
jnz short loc_401038       ; if z != 0 → inner else
push offset ...            ; "z is zero and x = y."
...
loc_401047:
cmp [ebp+var_C], 0
jnz short loc_40105C       ; if z != 0 → inner else
...

4. Loops (Vòng lặp)

4.1 For Loop

4 thành phần bắt buộc: khởi tạo → so sánh → thực thi → tăng/giảm

int i;
for (i = 0; i < 100; i++) {
    printf("i equals %d\n", i);
}
mov [ebp+var_4], 0          ; [1] Khởi tạo: i = 0
jmp short loc_401016        ; nhảy qua phần increment (lần đầu)

loc_40100D:                 ; [4] Increment
mov eax, [ebp+var_4]
add eax, 1
mov [ebp+var_4], eax

loc_401016:                 ; [2] So sánh
cmp [ebp+var_4], 64h        ; i < 100?
jge short loc_40102F        ; nếu i >= 100 → thoát loop

                            ; [3] Thực thi
mov ecx, [ebp+var_4]
push ecx
push offset aID             ; "i equals %d\n"
call printf
add esp, 8
jmp short loc_40100D        ; quay lại increment
flowchart TD A[Khởi tạo: i=0] --> B[So sánh: i < 100?] B -- true --> C[Thực thi: printf] C --> D[Increment: i++] D --> B B -- false --> E[Thoát loop]

4.2 While Loop

int status = 0;
int result = 0;
while (status == 0) {
    result = performAction();
    status = checkResult(result);
}
mov [ebp+var_4], 0          ; status = 0
mov [ebp+var_8], 0          ; result = 0

loc_401044:
cmp [ebp+var_4], 0
jnz short loc_401063        ; nếu status != 0 → thoát

call performAction
mov [ebp+var_8], eax        ; result = performAction()
mov eax, [ebp+var_8]
push eax
call checkResult
add esp, 4
mov [ebp+var_4], eax        ; status = checkResult(result)
jmp short loc_401044        ; lặp lại
While vs For trong assembly
  • For loop: có phần increment riêng biệt, lần đầu nhảy qua increment
  • While loop: không có phần increment riêng, chỉ có conditional jump và unconditional jump quay đầu

5. Calling Conventions (Quy ước gọi hàm)

Calling convention quy định: thứ tự push tham số, ai dọn stack sau khi gọi, return value ở đâu.

Pseudocode tham chiếu

int test(int x, int y, int z);
int a, b, c, ret;
ret = test(a, b, c);

5.1 cdecl

  • Tham số push từ phải sang trái
  • Caller dọn stack (add esp, N)
  • Return value trong EAX
push c
push b
push a
call test
add esp, 12        ; caller dọn stack (3 tham số × 4 bytes)
mov ret, eax       ; lấy return value

5.2 stdcall

  • Tham số push từ phải sang trái
  • Callee dọn stack (dùng ret N ở cuối hàm)
  • Không có add esp ở phía caller
  • Đây là convention chuẩn của Windows API

5.3 fastcall

  • 2 tham số đầu truyền qua register ECX, EDX (Microsoft convention)
  • Tham số còn lại push từ phải sang trái
  • Nhanh hơn vì ít dùng stack

So sánh Visual Studio vs GCC (push vs move)

Visual Studio (push)GCC (move)
push eax (var_8)mov [esp+4], eax
push ecx (var_4)mov [esp], eax
call addercall adder
add esp, 8có dòng nàykhông có dòng này

6. Switch Statements

6.1 If-Style Switch (≤ 3-4 case)

switch (i) {
    case 1: printf("i = %d", i+1); break;
    case 2: printf("i = %d", i+2); break;
    case 3: printf("i = %d", i+3); break;
    default: break;
}
cmp [ebp+var_8], 1
jz short loc_401027        ; case 1
cmp [ebp+var_8], 2
jz short loc_40103D        ; case 2
cmp [ebp+var_8], 3
jz short loc_401053        ; case 3
jmp short loc_401067       ; default

loc_401027:                ; case 1
    mov ecx, [ebp+var_4]
    add ecx, 1
    push ecx
    push offset unk_40C000
    call printf
    jmp short loc_401067   ; break

loc_40103D:                ; case 2
    ...

6.2 Jump Table Switch (≥ 4 case liên tiếp)

Compiler tối ưu bằng cách tạo bảng địa chỉ — thay vì so sánh tuần tự, dùng index để nhảy thẳng.

switch (i) {
    case 1: ... break;
    case 2: ... break;
    case 3: ... break;
    case 4: ... break;  // thêm case 4 → compiler chuyển sang jump table
    default: break;
}
mov ecx, [ebp+var_8]
sub ecx, 1                      ; điều chỉnh range 1-4 → 0-3
mov [ebp+var_8], ecx
cmp [ebp+var_8], 3
ja short loc_401082             ; nếu > 3 → default
mov edx, [ebp+var_8]
jmp ds:off_401088[edx*4]        ; nhảy theo jump table

; Jump table
off_401088 dd offset loc_40102C ; case 1
           dd offset loc_401042 ; case 2
           dd offset loc_401058 ; case 3
           dd offset loc_40106E ; case 4
flowchart TD A["sub ecx, 1 (điều chỉnh index)"] --> B{"cmp > 3?"} B -- yes --> E[default/end] B -- no --> C["jmp table[edx*4]"] C --> D1[case 1] C --> D2[case 2] C --> D3[case 3] C --> D4[case 4] D1 & D2 & D3 & D4 --> E
Tại sao nhân 4?
Mỗi entry trong jump table là một địa chỉ 32-bit = 4 bytes. Nên edx * 4 để tính đúng offset.

7. Arrays (Mảng)

int b[5] = {123, 87, 487, 7, 978}; // global
void main() {
    int a[5];                        // local
    for (int i = 0; i < 5; i++) {
        a[i] = i;
        b[i] = i;
    }
}
; Local array a — truy cập qua stack + offset
mov ecx, [ebp+var_18]          ; i
mov edx, [ebp+var_18]          ; value = i
mov [ebp+ecx*4+var_14], edx    ; a[i] = i  (base = var_14)

; Global array b — truy cập qua địa chỉ bộ nhớ cố định
mov eax, [ebp+var_18]
mov ecx, [ebp+var_18]
mov dword_40A000[ecx*4], eax   ; b[i] = i  (base = 0x40A000)

8. Structs

struct my_structure {
    int x[5];   // offset 0x00 - 0x10
    char y;     // offset 0x14
    double z;   // offset 0x18
};
; test(struct my_structure *q):
mov eax, [ebp+arg_0]
mov byte ptr [eax+14h], 61h    ; q->y = 'a' (0x61 = 'a' trong ASCII)
mov ecx, [ebp+arg_0]
fld ds:dbl_40B120
fstp qword ptr [ecx+18h]       ; q->z = 15.6 (floating-point instruction → double)

; for loop gán q->x[i]:
mov eax, [ebp+var_4]           ; i
mov ecx, [ebp+arg_0]           ; base pointer của struct
mov [ecx+eax*4], eax           ; q->x[i] = i
Cách nhận diện struct từ disassembly
  • Cùng một base pointer (arg_0) được dùng với nhiều offset khác nhau
  • Offset +0x14char (1 byte), offset +0x18 dùng với fstpdouble
  • Khó phân biệt struct với các biến liền kề — cần xem ngữ cảnh sử dụng

9. Linked List Traversal

struct node {
    int x;
    struct node *next;  // offset +4
};

// Loop 1: tạo 10 node
for (i = 1; i <= 10; i++) {
    curr = malloc(sizeof(pnode));
    curr->x = i;
    curr->next = head;
    head = curr;
}

// Loop 2: duyệt và in
curr = head;
while (curr) {
    printf("%d\n", curr->x);
    curr = curr->next;
}
; Loop 1 — for loop tạo node
mov [esp], 8
call malloc
mov [ebp+var_4], eax           ; curr = malloc(8)
mov edx, [ebp+var_4]
mov eax, [ebp+var_C]
mov [edx], eax                 ; curr->x = i  (offset 0)
mov edx, [ebp+var_4]
mov eax, [ebp+var_8]
mov [edx+4], eax               ; curr->next = head  (offset +4)
mov eax, [ebp+var_4]
mov [ebp+var_8], eax           ; head = curr

; Loop 2 — while loop duyệt
loc_4010B1:
cmp [ebp+var_4], 0
jz short locret_4010D7         ; if curr == NULL → thoát
mov eax, [ebp+var_4]
mov eax, [eax]                 ; curr->x
push eax
call printf
mov eax, [ebp+var_4]
mov eax, [eax+4]               ; curr->next
mov [ebp+var_4], eax           ; curr = curr->next
jmp short loc_4010B1
Cách nhận diện Linked List

var_4 được gán giá trị từ [eax+4], mà eax cũng lấy từ var_4 trước đó.

Struct trỏ đến struct cùng loại tại offset cố định = linked list.

Đây là dấu hiệu đệ quy tự tham chiếu (self-referential pointer).


Tổng kết: Bảng nhận diện nhanh

ConstructDấu hiệu trong assembly
Global varĐịa chỉ cố định (dword_XXXXXX)
Local var[ebp - N] hoặc [ebp+var_N]
If statementcmp + conditional jump (jz, jnz, …)
For loop4 phần: init + jmp qua increment + cmp + increment
While loopcmp + conditional jump + unconditional jump quay đầu, không có increment riêng
Switch (if-style)Chuỗi cmp + jz liên tiếp
Switch (jump table)sub điều chỉnh index + jmp [base + reg*4]
ArrayBase address + index * element_size
StructCùng base pointer + nhiều offset khác nhau
Linked list[eax+N] → gán lại cho chính pointer đó