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.
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:
R8tớiR15 - Mở rộng instruction pointer thành
RIPvà flags register thànhRFLAGS
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ố 1RDX— tham số 2R8— tham số 3R9— 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
retx86-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
PUSHngay đầ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, eaxNOP 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`0000FFFBLư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.