Mình bắt đầu học lại x86 assembly một cách bài bản bằng bộ tài liệu free của team FLARE (Mandiant) — “Malware Analysis Crash Course”. Đây là series ghi chú lại phần lý thuyết mình đọc được.
Assembly, Disassembly và Decompilation
Ba khái niệm hay bị lẫn vào nhau:
- Assembly — dạng human-readable của machine code, mỗi dòng gần như tương ứng 1-1 với một lệnh CPU. Mỗi kiến trúc (x86, ARM, MIPS) có cú pháp assembly riêng, nhưng nguyên lý nền tảng thì giống nhau.
- Disassembly — quá trình ngược: một disassembler (IDA Pro, Ghidra…) phân tích machine code nhị phân rồi dựng lại thành assembly kèm mnemonic/operand đầy đủ.
- Decompilation — đi xa hơn nữa: cố gắng biến assembly trở lại thành mã nguồn bậc cao (C/C++). Khác với disassembly, decompilation không bao giờ khôi phục hoàn hảo được mã gốc — vì tên biến, comment, định nghĩa kiểu dữ liệu đã mất vĩnh viễn trong lúc compile, cộng thêm compiler optimization làm biến dạng logic gốc. Decompiler chỉ cho ra một bản xấp xỉ “best-effort”, nhưng cực kỳ hữu ích để suy luận ở mức trừu tượng cao hơn thay vì đọc từng dòng assembly.
Pipeline đầy đủ (và chiều ngược lại khi làm RE):
Biên dịch: Source Code (.c) → Compiler → .ASM (intermediate) → Assembler → .OBJ → Linker → .EXE Binary
Reverse: .EXE Binary → Disassembler → Assembly Listing → Decompiler → Pseudo-Source Code (xấp xỉ)Data Types
x86 làm việc với các đơn vị dữ liệu kích thước cố định, tương ứng kiểu dữ liệu Windows API và C:
| Tên | Kích thước | Windows API | Kiểu C tương đương | Directive khai báo |
|---|---|---|---|---|
| Byte | 8 bit | BYTE | unsigned char | db |
| Word | 16 bit | WORD | unsigned short | dw |
| Dword | 32 bit | DWORD | unsigned long | dd |
| Qword | 64 bit | QWORD | unsigned long long | dq |
x86 Registers
General-Purpose Registers
8 thanh ghi 32-bit: EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP. Hầu hết lệnh có thể dùng các thanh ghi này thay thế lẫn nhau, nhưng có quy ước ngầm (ví dụ ESP luôn dành cho quản lý stack, EAX hay chứa giá trị trả về của hàm).
Sub-register — một thanh ghi “chia nhỏ” được
4 thanh ghi đầu tiên (EAX, EBX, ECX, EDX) có thể truy cập theo từng phần nhỏ hơn:
EAX (32-bit)
└── AX (16-bit, nửa thấp của EAX)
├── AH (8-bit cao của AX)
└── AL (8-bit thấp của AX)Sửa AL sẽ làm thay đổi byte thấp nhất của EAX, không đụng tới phần còn lại. Lưu ý: chỉ 4 thanh ghi đầu mới chia được H/L kiểu này — ESI, EDI, EBP, ESP chỉ chia được tới mức 16-bit (SI, DI, BP, SP), không có dạng 8-bit cao/thấp riêng.
EFLAGS và EIP
- EFLAGS — thanh ghi 32-bit chứa các cờ trạng thái/điều khiển, mỗi bit phản ánh kết quả của phép toán số học/logic gần nhất hoặc trạng thái CPU — các cờ này quyết định hành vi rẽ nhánh có điều kiện sau này.
- EIP (Instruction Pointer) — chứa địa chỉ bộ nhớ của lệnh kế tiếp sẽ thực thi. Khác với thanh ghi mục đích chung, EIP không thể sửa trực tiếp — nó chỉ được CPU tự cập nhật ngầm khi fetch lệnh tuần tự, hoặc cập nhật tường minh bởi các lệnh điều khiển luồng (jump, call, return).
Instruction Basics
Opcode, Mnemonic, Operand — 3 thành phần của một lệnh
- Opcode — chuỗi nhị phân mã hóa phép toán (cộng, di chuyển dữ liệu, gọi hàm…)
- Mnemonic — tên viết tắt dễ đọc cho opcode đó (ví dụ
mov,add) - Operand — tham số của lệnh, đóng vai trò dữ liệu nguồn/đích hoặc cả hai. Số lượng operand thay đổi tùy lệnh, thường từ 0 tới 3.
Ví dụ cụ thể: chuỗi byte B8 41 00 00 00 dịch ra thành mov eax, 41h. Byte đầu B8 là opcode nghĩa là “move hằng số vào EAX”; 4 byte còn lại mã hóa giá trị hằng số đó. Vì x86 lưu số theo little-endian, giá trị 00000041h lại nằm trong bộ nhớ dưới dạng 41 00 00 00 (byte có trọng số thấp nhất nằm trước).
Intel syntax vs AT&T syntax
Tài liệu này (và hầu hết IDA/Ghidra mặc định) dùng Intel syntax — operand đích viết trước, nguồn viết sau (mov đích, nguồn). AT&T syntax (phổ biến trên gdb/Linux) viết ngược lại (nguồn trước, đích sau) — cùng một lệnh nhưng đọc theo 2 chiều khác nhau, cần biết đang đọc syntax nào để khỏi hiểu nhầm hướng dữ liệu di chuyển.
3 loại Operand
| Loại | Mô tả | Ví dụ |
|---|---|---|
| Immediate | Giá trị là chính operand đó | 8, 10h, -123, 'A' |
| Register | Giá trị nằm trong thanh ghi | eax, si, ecx, esp |
| Memory | Operand là tham chiếu tới một địa chỉ bộ nhớ | [00020001], [ebp+08h], [eax+esi] |
Một ràng buộc quan trọng: MOV không thể chuyển trực tiếp từ memory sang memory — bắt buộc phải đi qua một thanh ghi trung gian.
MOV
MOV đích, nguồn — copy giá trị từ nguồn sang đích, không hề thay đổi giá trị nguồn. Đây là lệnh xuất hiện nhiều nhất trong bất kỳ đoạn disassembly nào vì phần lớn logic chương trình xoay quanh việc di chuyển dữ liệu.
mov eax, 0FFh ; EAX = 0xFF
mov ebx, eax ; EBX = EAX (EAX không đổi)NOP
NOP — không làm gì, chỉ nhảy sang lệnh kế tiếp. Về mặt chức năng tương đương xchg eax, eax (tự hoán đổi một thanh ghi với chính nó). Mã hóa chỉ 1 byte (0x90) nên rất tiện để ghi đè lệnh không mong muốn hoặc lấp khoảng trống khi patch/debug.
Hacking Assembly với IDA
IDA cho phép tự viết/sửa assembly ngay trong database đang debug. Quy trình cơ bản: mở IDA database, nhấn Continue (F9) để chạy, rồi dùng 4 panel chính:
- Disassembly — xem lệnh dạng tuyến tính hoặc graph
- Registers — xem/sửa giá trị thanh ghi
- Memory — xem/sửa bộ nhớ dạng hex
- Stack — quan sát trạng thái stack
Muốn tự “viết” một lệnh assembly: chọn địa chỉ đích → Edit → Patch Program → Assemble → gõ lệnh muốn chèn. Các phím debug hay dùng: Step Over (F8) chạy qua 1 lệnh mà không nhảy vào hàm con, Step Into (F7) nhảy hẳn vào trong hàm được gọi, Add Breakpoint (F2), và Set IP (chuột phải) để ép EIP nhảy tới địa chỉ khác (chính là kỹ thuật “forcing execution” ở Chapter 6).
Arithmetic Instructions
ADD (cộng), SUB (trừ), INC (+1), DEC (-1) — đích phải là thanh ghi hoặc bộ nhớ, không thể là immediate. Cả 4 lệnh đều cập nhật EFLAGS sau khi chạy.
Bitwise Instructions
7 lệnh thao tác ở cấp bit: XOR, AND, OR (phép logic trên 2 toán hạng), SHL/SHR (dịch bit trái/phải, phần trống được lấp bằng 0 — dữ liệu bị dịch ra ngoài sẽ mất), và ROL/ROR (xoay bit trái/phải — bit bị đẩy ra một đầu sẽ vòng lại đầu kia thay vì mất đi). Phân biệt rõ shift (mất dữ liệu) và rotate (không mất) là điểm hay bị nhầm.
Accessing Memory
CPU bắt buộc phải biết đọc/ghi bao nhiêu byte mỗi lần truy cập bộ nhớ — xác định theo 2 cách:
- Suy luận ngầm — nếu một operand là thanh ghi, kích thước thanh ghi đó quyết định luôn kích thước truy cập (
mov [402000h], ebxkhông cần ghi rõ vì EBX đã là 4 byte). - Khai báo tường minh — dùng directive kích thước:
byte ptr,word ptr,dword ptrkhi không có thanh ghi nào gợi ý sẵn kích thước (ví dụmov dword ptr [402000h], 0).
mov eax, [402000h] ; đọc 4 byte (vì đích là EAX)
mov ax, [402000h] ; chỉ đọc 2 byte (vì đích là AX)
mov dword ptr [402000h], 0 ; phải khai báo rõ vì nguồn là immediate, không tự suy luận đượcMemory Operands — công thức tính địa chỉ
Địa chỉ hiệu dụng (effective address) được tính từ tối đa 3 thành phần:
Effective Address = Base + Index × Scale + Displacement- Base — thanh ghi chứa địa chỉ gốc
- Index × Scale — một thanh ghi index nhân với hệ số scale (1, 2, 4 hoặc 8) — đúng kích thước 1 phần tử mảng
- Displacement — hằng số offset cố định (có thể âm)
[ebp+8] ; base = EBP, displacement = 8
[esp+ebx*4+8] ; base = ESP, index = EBX, scale = 4, displacement = 8Pattern index*scale chính là lý do vì sao công thức này gắn liền với việc duyệt mảng (sẽ nói kỹ hơn ở Chapter 7).
LEA (Load Effective Address)
LEA đích, [biểu thức] — khác hẳn các lệnh khác: nó không hề đọc bộ nhớ, chỉ tính toán giá trị của biểu thức trong ngoặc vuông như một phép tính thuần túy rồi ghi kết quả (một địa chỉ, hoặc đơn giản là một con số) vào đích. Ngoặc vuông ở đây chỉ mang tính ký hiệu, không phải lệnh truy cập bộ nhớ thật.
Hai công dụng chính:
- Tính offset — về bản chất tương đương toán tử
&(lấy địa chỉ) trong C - Compiler tối ưu — thay cả chuỗi lệnh cộng/nhân bằng đúng 1 lệnh
LEAduy nhất
lea eax, [ebx+8] ; EAX = EBX + 8 (không đọc bộ nhớ)
lea eax, [ebx*4+5] ; EAX = EBX*4 + 5
lea eax, [ecx+ebx*4+5] ; EAX = ECX + EBX*4 + 5Nhìn thấy LEA trong disassembly, đừng vội nghĩ “đang lấy địa chỉ của biến gì đó” — rất nhiều khi nó chỉ đơn giản là một phép ADD/MUL được compiler viết dưới dạng khác.
Đây là nền tảng bắt buộc phải nắm trước khi đọc bất kỳ đoạn disassembly nào. Chapter tiếp theo mình sẽ ghi về Stack — khái niệm cũng nền tảng không kém.