977 words
5 minutes
FLARE Learning Hub - Chapter 8: x86-64 Assembly

Chapter 8 của “Malware Analysis Crash Course” (FLARE/Mandiant) chuyển từ x86 32-bit sang x86-64 — phần lớn malware/phần mềm hiện đại trên Windows đều build cho 64-bit, nên đây là nền tảng bắt buộc để đọc được sample đời mới.

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

Primary x86-64 Registers#

x86-64 (hay AMD64/x64) mở rộng x86 theo 3 hướng:

  • Mở rộng 8 thanh ghi mục đích chung sẵn có lên 64-bit, đổi tiền tố thành R: RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP
  • Bổ sung 8 thanh ghi hoàn toàn mới: R8 tới R15
  • Mở rộng instruction pointer thành RIP và flags register thành RFLAGS

Giống hệt cơ chế sub-register ở 32-bit, vẫn truy cập được phần thấp hơn: EAX/AX/AL cho RAX. Với các thanh ghi mới R8-R15, quy ước đặt hậu tố: R8D (32-bit), R8W (16-bit), R8B (8-bit). Có một khác biệt nhỏ: RAX có AH (byte cao của AX), nhưng các thanh ghi mới không có dạng byte-cao tương đương — chỉ RAX-RDX (và các thanh ghi gốc) mới có AH/BH/CH/DH.

x86-64 Calling Convention: __fastcall#

Trên Windows x64, Microsoft compiler dùng __fastcall — khác hẳn STDCALL/CDECL ở Chapter 4. Chỉ 4 tham số số nguyên đầu tiên truyền qua thanh ghi:

  • RCX — tham số 1
  • RDX — tham số 2
  • R8 — tham số 3
  • R9 — tham số 4

Tham số thứ 5 trở đi mới ra stack, bắt đầu ngay tại offset 0x20 tính từ RSP (ngay sau vùng “home space” 32-byte, nói ở phần dưới) — tham số thứ 5 ở [RSP+0x20], thứ 6 ở [RSP+0x28], cứ thế.

; gọi hàm addSixInts(1, 2, 3, 4, 5, 6)
sub     rsp, 38h
mov     dword ptr [rsp+28h], 6    ; tham số 6 -> stack
mov     dword ptr [rsp+20h], 5    ; tham số 5 -> stack
mov     r9d, 4                     ; tham số 4 -> R9
mov     r8d, 3                     ; tham số 3 -> R8
mov     edx, 2                     ; tham số 2 -> RDX
mov     ecx, 1                     ; tham số 1 -> RCX
call    addSixInts
add     rsp, 38h
ret

x86-64 Stack Conventions và Home Space#

Khác biệt lớn nhất so với 32-bit: ở x64, toàn bộ stack frame được cấp phát một lần duy nhất trong prologue (sub rsp, X) và giữ cố định suốt thời gian hàm chạy — không còn cảnh PUSH/POP liên tục làm RSP nhấp nhô như 32-bit. Vì RSP không đổi trong suốt hàm, việc đánh địa chỉ tương đối theo RSP trở nên đáng tin cậy, nên RBP không còn bắt buộc phải làm frame pointer nữa — được giải phóng thành một thanh ghi mục đích chung bình thường.

Khoảng sub rsp, X đó dùng để chứa:

  • Các thanh ghi non-volatile cần bảo toàn (có thể được PUSH ngay đầu prologue)
  • Biến cục bộ
  • Chỗ cho tham số của các hàm mà hàm này sẽ gọi tiếp (compiler tự tính sẵn kích thước lớn nhất cần)
  • Home space (còn gọi là shadow space/shadow store)

Home space là vùng 32-byte cố định mà caller luôn phải chừa sẵn, nằm ngay dưới return address trên stack — dưới home space mới tới các tham số truyền qua stack (thứ 5 trở đi). Mục đích: cho phép callee “spill” (lưu tạm ra bộ nhớ) 4 tham số truyền qua thanh ghi — đảm bảo debugger luôn tìm thấy giá trị tham số ngay cả khi thanh ghi đã bị ghi đè trong quá trình hàm chạy (ở 32-bit không cần vì tham số vốn đã luôn nằm sẵn trên stack). Ở bản build release tối ưu, hàm có thể tận dụng luôn home space làm vùng scratch chung thay vì thật sự spill thanh ghi — gần như không tốn thêm chi phí gì vì việc chỉnh stack chỉ làm đúng 1 lần ở prologue.

Multi-byte NOPs#

x86-64 vẫn giữ NOP 1-byte (0x90) nhưng dùng mạnh tay các dạng NOP nhiều byte để căn chỉnh code tới biên 16-byte, tối ưu hiệu năng cache (cache line CPU thường là bội số của 16 byte). Ví dụ thực tế:

401000  75 0E              jnz    short loc_401010
401002  35 EF BE AD DE     xor    eax, 0DEADBEEFh
401007  05 CE FA ED FE     add    eax, 0FEEDFACEh
40100C  0F 1F 40 00        nop    dword ptr [eax+00h]   ; NOP 4-byte để căn chỉnh
401010  loc_401010:
401010  89 C2              mov    edx, eax

NOP 4-byte tại 0x40100C ở đây có tác dụng duy nhất: đẩy lệnh kế tiếp (đích của 1 jump) tới đúng địa chỉ chia hết cho 16 (0x401010).

MOVSX, MOVSXD và MOVZX#

Nhóm lệnh mở rộng dữ liệu nhỏ thành thanh ghi lớn hơn, xuất hiện nhiều ở 64-bit hơn hẳn 32-bit vì code 64-bit hay trộn lẫn kiểu dữ liệu 32-bit và 64-bit:

  • MOVSX / MOVSXD (Move with Sign-Extension / Sign-Extension Doubleword) — giữ đúng dấu khi mở rộng, lấp đầy bit cao bằng bit dấu (MSB) của giá trị gốc.
  • MOVZX (Move with Zero-Extension) — lấp đầy bit cao bằng toàn số 0, dùng cho giá trị unsigned.
mov     ax,  -5           ; ax = -5 (16-bit; FFFB)
movsx   ebx, ax           ; ebx = -5 (32-bit; FFFFFFFB) - giữ đúng dấu âm
movsxd  rdx, ebx          ; rdx = -5 (64-bit; FFFFFFFF`FFFFFFFB)

So sánh: nếu dùng MOV thường để chép 0xFFFB (word) thẳng vào RDX mà không extend, kết quả sẽ là 65531 dương (00000000'0000FFFB) — hoàn toàn sai so với giá trị gốc -5.

mov     ax,  0FFFBh       ; ax = FFFB
movzx   ebx, ax           ; ebx = 0000FFFB (zero-extend, coi là unsigned)
movzx   rdx, ax           ; rdx = 00000000`0000FFFB

Lưu ý: không tồn tại MOVZXD — vì ở chế độ 64-bit, một lệnh MOV ghi vào thanh ghi 32-bit (ví dụ EBX) đã tự động zero-extend luôn 32-bit cao của thanh ghi 64-bit tương ứng (RBX) rồi, nên không cần lệnh riêng để làm việc đó nữa.


x86-64 không thay đổi bản chất logic của chương trình, chỉ thay đổi cách nó được biểu diễn ở cấp thấp. Chapter cuối cùng mình sẽ ghi về phần thực dụng nhất khi RE malware trên Windows: Windows API, data type, và các pattern gọi hàm hệ thống thường gặp.

FLARE Learning Hub - Chapter 8: x86-64 Assembly
https://stewmalwarehunter.id.vn/posts/flare-macc-ch8---x86-64-assembly/
Author
Stew
Published at
2026-09-24
License
CC BY-NC-SA 4.0