Epilogue/Prologue và Calling Conventions
Epilogue và Prologue là 2 cơ chế khởi tạo khung ngăn xếp, và khôi phục khung ngăn xếp trong 1 hàm. Epilogue gọi khi bắt đầu 1 hàm, còn prologue được gọi khi kết thúc hàm đó.

Có nhiều cơ chế calling convention khác nhau, nhưng sử dụng phổ biến nhất là stdcall, stack sẽ tự động được khôi phục sau khi kết thúc hàm. Tham khảo các calling convention trên Windows tại link sau: x86 calling conventions – Wikipedia.
Đoạn mã sau là chương trình asm gọi hàm, sử dụng prologue, epilogue.
.386
.model flat
option casemap:none
.code
_main proc
push ebp
mov ebp, esp
push 1
push 2
call calc
mov esp, ebp
pop ebp
ret
_main endp
calc proc
push ebp
mov ebp, esp
mov eax, [ebp + 8] ; 1
mov edx, [ebp + 12] ; 2
add eax, edx
mov esp, ebp
pop ebp
ret
calc endp
.data
end _main
Từ asm tới obj, exe trên Windows và các thành phần quan trọng
Sử dụng ml masm_name.asm và link objname.obj để compile file asm thành file obj và exe.

Dưới đây là mã asm 1 chương trình đơn giản, sử dụng các biến textCode và textData được khai báo ở 2 section là .code và .data.
.386
.model flat
option casemap:none
.code
_main proc
push ebp
mov ebp, esp
mov eax, offset textCode
mov edx, offset textData
jmp end_label
call end_label
call calc
end_label:
mov esp, ebp
pop ebp
ret
_main endp
textCode db "TextCode", 0ah
calc proc
push ebp
mov ebp, esp
mov esp, ebp
pop ebp
ret
calc endp
.data
textData db "TextData", 0ah
end _main
.data
Trong mã trên, có 3 thành phần quan trọng là biến, nhãn và hàm. Trong đó, các lệnh call, jmp sẽ nhảy tới label hoặc hàm dựa trên offset tính từ điểm gọi, còn lại sẽ dựa trên địa chỉ tuyệt đối.
Sử dụng dumpbin /all objname.obj để dump các thành phần của file obj.
Section code như dưới đây, 2 phần mã được điền 0 là của textCode và textData, lưu trữ offset cần thay thế ở trong bảng Relocations. Lưu ý bảng Reloc này là của Obj, khác với Reloc trong PE.

Dựa vào Symbol index và Offset, thêm bảng Symbol dưới đây, có thể link được địa chỉ cho các biến chính xác để tạo thành PE.

Sau khi thực hiện link xong, kết quả nhận được file exe. Có thể thấy vị trí của 2 biến ban đầu đã được thay thế bằng địa chỉ tuyệt đối.

Ngoài ra, PE kèm theo bằng Reloc cho riêng nó, gồm 2 biến trên, cần tính toán địa chỉ cần tính lại trong trường hợp Imagebase thay đổi. Hiện tại trong mã được biên dịch đang có imagebase là 0x400000.

Từ đây, có 1 vấn đề xảy ra trong việc xây dựng shellcode. Sau khi copy shellcode vào tiến trình/file đích, địa chỉ tuyệt đối của các biến chắc chắn sẽ không còn đúng nữa. Yêu cầu đặt ra là phải tính toán lại chính xác địa chỉ mới trong shellcode.
Kỹ thuật delta trong shellcode
Sử dụng đoạn code ở phần 2, biên dịch thành file exe và đẩy vào x32dbg. Vì imagebase khi load vào bị thay đổi, nên địa chỉ tương ứng cũng bị thay đổi.

Phương pháp đề ra là tính toán độ lệnh của imagebase, hay nói cách khác là độ lệch của địa chỉ tuyệt đối trước và sau, từ đó tính toán ra địa chỉ mới.
addr_newB = (addr_newA – addr_oldA) + addr_oldB
addr_old đều có thể lấy được sẵn trong code, vấn đề đặt ra là tìm addr_new. Việc này có thể thực hiện thông qua qua 2 phương pháp dưới đây.
- Sử dụng lệnh push 1 nhãn, ngay sau đó pop nhãn đó ra, từ đó có được địa chỉ hiện tại của nhãn đó.
- Sử dụng lệnh call 1 nhãn đặt sau lệnh call, lệnh này sẽ push địa chỉ của nhãn vào stack, sau đó thực hiện pop để lấy ra địa chỉ của nhãn này.
Giả sử pop địa chỉ nhãn vào eax, phần sau lệnh trên cần tính hiệu giữa địa chỉ mới của nhãn và cũ của nhãn.
sub eax, offset label
Như đã nói ở phần 2, offset label khi biên dịch sẽ được fix cứng địa chỉ tuyệt đối, vì vậy khi shellcode được sử dụng, eax sẽ là địa chỉ mới, offset label đã được fix cứng địa chỉ cũ, phép tính trên tạo ra delta, độ chênh lệch giữa 2 imagebase, hay nói cách khác là 2 biến.
Để tính toán địa chỉ mới cho 1 biến, chỉ cần cộng delta với địa chỉ cũ.
lea edx, [eax + offset value]
Như vậy có thể tính toán được địa chỉ mới của biến và edx. Dưới đây là mã asm đầy đủ mô tả kỹ thuật delta.
.386
.model flat
option casemap:none
.code
_main proc
push start_code ; or call start_code
start_code:
pop ebx
sub ebx, start_code
push ebp
mov ebp, esp
sub esp, 50h
xor eax, eax
mov [ebp - 04h], eax ; create local variable
lea edx, [ebx + offset msgCode] ; load address msgCode to edx
mov esp, ebp
pop ebp
ret
_main endp
msgCode db "HelloCode", 0ah
.data
mov eax, ebx
msgData db "HelloData", 0ah
end _main
PEB và cách truy cập WinAPI thông qua PEB
Giống như các biến, WinAPI cũng gặp vấn đề về việc truy cập địa chỉ để gọi hàm. Thông thường 1 chương trình asm muốn sử dụng WinAPI cần include các thư viện cần thiết, tuy nhiên shellcode chỉ được chứa các opcode mà không bao gồm các thư viện đi kèm.
Giải pháp đề ra là truy cập địa chỉ WinAPI thông qua PEB.
Có thể tham khảo cấu trúc đầy đủ tại link PEB – NtDoc, hoặc với phiên bản rút ngon hơn PEB (winternl.h) – Win32 apps | Microsoft Learn. Phần dưới sẽ sử dụng các cấu trúc của microsoft đưa ra, tuy nhiên để hiểu rõ bản chất nên sử dụng ntDoc.
Cấu trúc PEB và offset để tính toán LDR như sau.
typedef struct _PEB {
BYTE Reserved1[2]; // +0x0
BYTE BeingDebugged; // +0x2
BYTE Reserved2[1]; // +0x3
PVOID Reserved3[2]; // +0x4
PPEB_LDR_DATA Ldr; // +0xc
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
Cấu trúc LDR như sau.
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8]; // 0x0
PVOID Reserved2[3]; // 0x8
LIST_ENTRY InMemoryOrderModuleList; //0x14
} PEB_LDR_DATA, *PPEB_LDR_DATA;
Cấu trúc của LIST_ENTRY
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; //0x0
struct _LIST_ENTRY *Blink; //0x4
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
Và nơi chứa thông tin về DLL được load vào process: LDR_DATA_TABLE_ENTRY
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2]; //0x0
LIST_ENTRY InMemoryOrderLinks; //0x8
PVOID Reserved2[2]; //0x10
PVOID DllBase; //0x18
PVOID Reserved3[2]; //0x1c
UNICODE_STRING FullDllName; //0x24
BYTE Reserved4[8];
PVOID Reserved5[3];
#pragma warning(push)
#pragma warning(disable: 4201) // we'll always use the Microsoft compiler
union {
ULONG CheckSum;
PVOID Reserved6;
} DUMMYUNIONNAME;
#pragma warning(pop)
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
Dưới đây là sơ đồ thể hiện sự liên kết giữa các struct trên, có bổ sung 1 số tưởng nhưng chỉ cần tập trung vào InMemoryOrderModuleList.

Windows sử dụng double-link list để liên kết các module.

Nhìn vào 2 sơ đồ trên, cần tính toán địa chỉ của LDR_DATA_TABLE_ENTRY. Thông qua LDR có thể tính toán được địa chỉ LIST_ENTRY, từ đó tính ra LDR_DATA_TABLE_ENTRY.
Để lấy địa chỉ PEB trên masm, sử dụng câu lệnh sau.
assume fs:nothing
mov esi, [fs:30h] ;esi: address of PEB
assume fs:error
Tham khảo hàm lấy base dll sau.
GetModuleBase proc ; arg1: dll name; return base dll
push ebp
mov ebp, esp
sub esp, 50h
mov edi, [ebp + 8]
assume fs:nothing
mov eax, fs:[30h] ;Get PEB
assume fs:error
mov eax, [eax + 0ch] ;*LDR
mov esi, [eax + 14h] ;LIST_ENTRY InMemoryOrderModuleList
xor ecx, ecx
LoopGetModuleBase:
mov esi, [esi] ;pointer to flink
mov ecx, esi
sub ecx, 8h ;pointer to _LDR_DATA_TABLE_ENTRY
add ecx, 24h ;pointer to UNICODE_STRING FullDllName;
mov ecx, [ecx + 4h] ;pointer to PWSTR Buffer;
push ecx
push edi
call CompareUnicodeString
add esp, 8
test eax, eax
jz LoopGetModuleBase
mov eax, [esi - 8h + 18h] ;Pointer to PVOID DllBase;
mov esp, ebp
pop ebp
ret
GetModuleBase endp
Khi đã có base dll, cần truy cập bảng export của PE để lấy địa chỉ API cần thiết. Tham khảo PE format tại PE Format – Win32 apps | Microsoft Learn.
Vì dll đã được load lên mem, nên thay vì đọc trong bảng export của PE, truy cập thẳng vào Data Directory của Export để lấy VA. Data Directories có cấu trúc như sau.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Export Directory có index là 1 trong mảng này, chỉ cần truy cập, lấy VA cộng với base dll sẽ trỏ tới Export directory table.
Mã C mô tả quá trình truy cập tương tự như x64 sau.

Cấu trúc mảng IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //0x0
DWORD TimeDateStamp; //0x4
WORD MajorVersion; //0x8
WORD MinorVersion;
DWORD Name; //0xc // RVA tên DLL
DWORD Base; //0x10
DWORD NumberOfFunctions; //0x14
DWORD NumberOfNames; //0x18
DWORD AddressOfFunctions;//0x1c // RVA array function RVA
DWORD AddressOfNames; //0x20 // RVA array tên hàm
DWORD AddressOfNameOrdinals; //0x24 // RVA array ordinal
} IMAGE_EXPORT_DIRECTORY;
Có 3 trường quan trọng cần quan tâm là 3 mảng RVA liên quan tới function RVA, tên hàm và oridinal. Cần truy cập và tìm đúng tên hàm cần, sau đó sử dụng index của hàm để tính ra function RVA tương ứng.
Names[i]
↓
Ordinals[i]
↓
Functions[ Ordinals[i] ]
Hàm lấy địa chỉ API bao gồm các bước chính.
- Tìm VA tới _IMAGE_EXPORT_DIRECTORY
- Tìm VA tới AddressOfFunctions, AddressOfNames, AddressOfNameOrdinals
- Duyệt mảng AddressOfNames, so sánh với tên API cần tìm, sử dụng 1 biến tăng dần để tìm index mảng.
- Duyệt mảng AddressOfNameOrdinals thông qua index tìm được ở trước để lấy ra oridinal.
- Duyệt mảng AddressOfFunctions để lấy địa chỉ thông qua index ở oridinal.
Dưới đây là mã asm tham khảo hàm tìm kiếm địa chỉ.
GetFuncAddr proc ;arg1: base dll ;arg2: winapiname ;return address of api
push ebp
mov ebp, esp
sub esp, 50h
xor eax, eax
mov [ebp - 4], eax ;base dll
mov [ebp - 8], eax ;RVA to AddressOfFunctions
mov [ebp - 12], eax ;RVA to AddressOfNameOridinal
mov esi, [ebp + 8] ;base dll
mov [ebp - 4], esi
add esi, [esi + 3ch] ;pointer to nt header
mov esi, [esi + 78h]
mov eax, [ebp - 4]
add esi, eax ;pointer to _IMAGE_EXPORT_DIRECTORY
lea eax, [esi + 20h] ;RVA to AddressOfNames array
lea edx, [esi + 1ch] ;RVA to AddressOfFunctions
mov [ebp - 8], edx
lea edx, [esi + 24h] ;RVA to AddressOfNameOridinal
mov [ebp - 12], edx
mov esi, [eax] ;RVA AddressOfNames[0]
add esi, [ebp - 4] ;VA AddressOfNames[0]
xor ecx, ecx
mov edi, [ebp + 12] ;arg2 winapi name
LoopFindNameFunc:
mov eax, [esi]
add eax, [ebp - 4]
push edi
push eax
call CompareString
add esp, 8
add esi, 4 ;AddressOfNames[i + 1]
inc ecx ;inc index
test eax, eax ;Check result compare
jz LoopFindNameFunc
mov edx, [ebp - 12] ;RVA to AddressOfNameOridinal
mov eax, [ebp - 4] ;base dll
add eax, [edx] ;VA AddressOfNameOridinal
lea edx, [ecx * 2] ;calc index of oridinals - word
add eax, edx
xor edx, edx
mov dx, [eax] ;store oridinal to dx
dec edx ;dec index
lea edx, [edx * 4] ;calc offset (dword)
mov eax, [ebp - 8] ;RVA address func
mov eax, [eax]
add eax, [ebp - 4] ;VA address func
add eax, edx ;VA function find
mov eax, [eax] ;RVA function find
add eax, [ebp - 4] ;VA function
mov esp, ebp
pop ebp
ret
GetFuncAddr endp
Dựa vào các kĩ thuật trên, có thể tải và gọi bất kì API thuộc thư viện nào để xây dựng shellcode với chức năng tùy chỉnh.
Dump shellcode từ exe và thử nghiệm inject
Lệnh build nhanh asm -> exe
ml main.asm
Lệnh dump section text
dumpbin /RAWDATA /SECTION:.text main.exe > raw.txt
Mã python tách riêng phần opcode ra file bin.
# dump_shellcode.py
from pathlib import Path
EXE_PATH = "main.exe"
OUT_PATH = "text.bin"
POINTER_TO_RAW = 0x400
SIZE_OF_RAW = 0x400
data = Path(EXE_PATH).read_bytes()
chunk = data[POINTER_TO_RAW:POINTER_TO_RAW + SIZE_OF_RAW]
Path(OUT_PATH).write_bytes(chunk)
print(f"[+] Wrote {len(chunk)} bytes to {OUT_PATH}")
Viết thử 1 chương trình Alloc -> WriteMem -> CreateThread để test shellcode. Kết quả nhận được MessageBox của shellcode.
