Chapter 3 của “Malware Analysis Crash Course” (FLARE/Mandiant) nói về flow control cơ bản — cách chương trình nhảy từ chỗ này sang chỗ khác, và cách một function call thực sự diễn ra ở cấp assembly.
JMP
JMP target — nhảy không điều kiện bằng cách ghi đè thẳng EIP thành địa chỉ target, tương đương goto. Không kiểm tra điều kiện gì cả, và không lưu lại nơi vừa nhảy đi để quay về (khác hẳn CALL). Một vòng lặp vô hạn đơn giản nhất về bản chất chỉ là: tăng một thanh ghi, rồi JMP ngược lại đúng lệnh tăng đó.
Cross References (Xrefs) trong IDA
Xref là liên kết hai chiều giữa nơi một địa chỉ được tham chiếu tới và nơi nó được định nghĩa — IDA tự dựng toàn bộ bảng này khi phân tích binary. Có 2 loại:
- Code xref — nối giữa các lệnh với nhau, sinh ra bởi
JMP/CALL/nhánh rẽ. Trên giao diện, mũi tên hướng lên nghĩa là tham chiếu tới từ địa chỉ thấp hơn, mũi tên hướng xuống là từ địa chỉ cao hơn. - Data xref — nối dữ liệu (string, hằng số, biến) với đoạn code đang thao tác lên nó.
Thao tác hay dùng: double-click vào 1 xref để nhảy thẳng tới đó, hover để xem preview không cần rời khỏi vị trí hiện tại, và nhấn X trên một phần tử để liệt kê tất cả nơi tham chiếu tới nó — đây là cách nhanh nhất để trả lời câu hỏi “hàm/biến này được dùng ở đâu”.
CALL
CALL target gộp 2 thao tác xảy ra đồng thời:
PUSHđịa chỉ của lệnh ngay sauCALL(gọi là return address) lên stackJMPtớitarget
Về bản chất tương đương hệt: push <địa chỉ lệnh kế tiếp>; jmp target. Nếu hàm nhận tham số, caller phải PUSH chúng lên stack trước khi CALL chạy — nên ngay sau khi CALL thực thi, thứ tự trên đỉnh stack (từ trên xuống) là: return address, rồi tới các tham số theo thứ tự đẩy ngược (thường phải-sang-trái).
RET
RET làm ngược lại: POP giá trị ở đỉnh stack ra rồi nhảy EIP tới đó — vì giá trị đó chính là return address mà CALL đã đẩy vào trước đó.
Điểm quan trọng hay bị bỏ sót: RET có thể nhận một operand tùy chọn là số byte — ví dụ ret 8. Khi có operand này, sau khi pop xong return address, RET tự cộng thêm đúng số byte đó vào ESP, coi như tự dọn luôn phần tham số trên stack. Cơ chế này gọi là callee cleanup (hàm được gọi tự dọn dẹp) — sẽ quay lại kỹ hơn ở khái niệm calling convention STDCALL (Chapter 4).
ret ; chỉ pop return address rồi nhảy tới
ret 8 ; pop return address, nhảy tới, RỒI cộng thêm 8 vào ESP (tự dọn 2 tham số 4-byte)Đây cũng là lý do một lỗ hổng như stack buffer overflow nguy hiểm: nếu attacker ghi đè được return address nằm trên stack trước khi RET chạy, họ kiểm soát được chương trình sẽ nhảy tới đâu tiếp theo.
Vòng đời đầy đủ của một Function Call
- Chuẩn bị tham số — caller
PUSHtham số lên stack (thường theo thứ tự phải sang trái) - Gọi hàm —
CALLđẩy return address, nhảy vào hàm - Hàm thực thi — chạy thân hàm
- Trả về —
RETpop return address, nhảy về đúng chỗ - Dọn stack — tùy calling convention mà caller hay callee chịu trách nhiệm dọn phần tham số đã đẩy
Strings
String trong binary thường xuất hiện dưới 2 dạng mã hóa:
- Narrow/ASCII — mỗi ký tự 1 byte, kết thúc bằng 1 byte
0x00. Ví dụ"Hello"nằm dưới dạng hex:48 65 6C 6C 6Frồi tới byte00kết thúc. - Wide/Unicode (UTF-16LE) — mỗi ký tự chiếm 2 byte (hỗ trợ gần như mọi ngôn ngữ), kết thúc bằng 2 byte
0x00 0x00. Ví dụ"Hel"ở dạng wide:48 00 65 00 6C 00.
Khi một hàm cần dùng string, chương trình chỉ truyền địa chỉ bắt đầu của chuỗi đó (không phải nội dung) — hàm nhận địa chỉ sẽ tự đọc tuần tự từ đó cho tới khi gặp byte-NUL tương ứng. Khi IDA/Ghidra nhận diện được string, nó tự gán label và hiển thị ngay trong code — đây là một trong những đầu mối “ăn liền” đầu tiên khi mở một sample mới: lướt qua danh sách string để đoán chức năng trước khi đọc kỹ từng hàm.
Hiểu được CALL/RET là bước đệm bắt buộc cho Chapter 4 — nơi mình ghi về cách một hàm tổ chức tham số và biến cục bộ của chính nó trên stack (stack frame).