Hidden in PEB Sight: Hiding Windows API Imports With a Custom Loader

In this post, we look at different techniques to hide Windows API imports in a program in order to fly under the radar of static analysis tools. Especially, we show a method to hide those imports by dynamically walking the process environment block (PEB) and parsing kernel32.dll in-memory to find its exported functions. Let’s dive in!

Basic shellcode injection

Say we want to write a small program injecting a shellcode into the first instance it finds of notepad.exe. In the most basic case, the layout of the code looks like this:

  • Find the PID of notepad.exe
  • Get a handle to it using OpenProcess
  • Allocate a writable and executable page in the target process memory
  • Write our shellcode to it
  • Call CreateRemoteThread and give it the address where the shellcode is located in memory

This will cause a new thread to spawn in notepad.exe and to execute our shellcode. Here is the sample code:

The function find_process is not relevant but included here for completeness:

This works as intended – but if we take a look at the generated executable file, we can very clearly see the functions from the Windows API we use:

  • CreateToolhelp32Snapshot, Process32First and Process32Next to find our target process.
  • OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread for code injection.

These are highly suspicious and represent the typical behavior of a program attempting to enumerate running processes in order to inject code in one of them. The easiest way to see these imports is to open the file in VirusTotal or PeStudio.

Imports from kernel32.dll as shown by VirusTotal
Imports as shown by PeStudio. The functions we use are on a blacklist and even already mapped to MITRE ATT&CK!

In this situation, we’re 100% sure to get caught even by the weakest antivirus perforrming static analysis. Can we do any better?

Resolving imports with GetProcAddress

The Windows API exposes a method allowing us to dynamically retrieve the address of a function: GetProcAddress, whose prototype is:

FARPROC GetProcAddress(
  HMODULE hModule,
  LPCSTR  lpProcName
);

The first parameter can be retrieved via a call to GetModuleHandleA. Let’s see what things look like if we rewrite our previous injection code to dynamically resolve function names using a call to GetProcAddress. For every function call we wish to hide, we need to:

  • Define a type representing a function pointer
  • Call GetProcAddress

Example for OpenProcess:

It makes the code a bit heavier, but whatever. Here’s the full program using this technique:

We’re a little better on the imports side because now we are only importing GetProcAddress and GetModuleHandleW. The detection rate is slightly better (10/69 instead of 13/69 for our first sample), but importing those two functions is still pretty suspicious. Plus, simply checking the strings of our binary still gives us away…

$ strings sample2.exe | grep -E '(CreateRemote|Write|Virtual)'                                                 

VirtualAllocEx
WriteProcessMemory
CreateRemoteThread

Use the PEB, Luke

We need to go a level deeper and… dynamically resolve the address of GetProcAddress and GetModuleHandleW. How do we do that? Meet the PEB!

The PEB (Process Environment Block) is an in-memory data structure containing a bunch of information about the current running process, including the DLL its has loaded as well as their location in memory. The plan is as follows:

  • Read the PEB. This is relatively easy because the CPU register FS:[0x18] has a pointer to it. We can also use the undocumented constant NT_TIB from the Windows API.
  • Use the PEB to find the base address of kernel32.dll in memory. This gets a bit hairy because we need to iterate through data structures internal to the Windows loader such as PEB_LDR_DATA and LDR_DATA_TABLE_ENTRY.
  • Once we know where kernel32.dll is in memory, interpret it as a PE file (because, like all DLLs and EXEs, it is a PE file), and walk through its export table to find the function names we want to resolve. Essentially, we need to:
    • Find the export table
    • Find the index of the exported function we want to resolve
    • Use this index to index the ordinals table
    • Use this result to index the functions table, giving us a pointer to the exported function

Not critically hard, but a bit hairy as well. I’m not going to lie, it took me a few evenings to get it right… 🙂

We can now use these as a building block to dynamically find any function!

Result:

kernel32.dll @ 00007FFF023B0000
OpenProcess @ 00007FFF023CA1A0

We can therefore use this to dynamically find GetProcAddress and GetModuleHandleW, in order to use them to dynamically find other functions.

Here’s the full code using this technique: https://gist.github.com/christophetd/37141ba273b447ff885c323c0a7aff93

If we load the executable we obtain in a static analysis tool, we now don’t have any more visible suspicious GetProcAddress import!

But we didn’t get any better hiding our strings:

$ strings sample3.exe | grep -E '(CreateRemote|Write|Virtual)'
VirtualAllocEx
WriteProcessMemory
VirtualProtectEx
CreateRemoteThread

What used to be a string introduced by a function call OpenProcess(xx) is now a string introduced in the binary by a string literal when we perform the dynamic import. Can we do any better?

No strings attached

In order to hide our suspicious strings, we can proceed as follows:

  • Use the same method as we used to resolve GetProcAddress to also resolve other functions
  • Obfuscate somehow our strings (such as “OpenProcess”), for instance by XOR’ing them prior to the compilation and XOR’ing them again when performing the comparison against the exported function names of kernel32.dll.

A more complete description of this method, written by LloydLabs, is available here. In our case, the piece of code to change would be:

// Before
if (!_strcmpi(funcName, export_name)) {

// After
if (!_strcmpi(funcName, DECRYPT(export_name))) {

And what’s in the resolve_imports method:

// Before
dynamic::GetProcAddress = (GetProcAddressPrototype) find_dll_export(kernel32_base, "GetProcAddress");

// After
char str[] = {0x5,0x27,0x36,0x12,0x30,0x2d,0x21,0x3,0x26,0x26,0x30,0x27,0x31,0x31}
dynamic::GetProcAddress = (GetProcAddressPrototype) find_dll_export(kernel32_base, DECRYPT(str));

… assuming DECRYPT is a function or macro XOR’ing each byte with 0x42.

I won’t provide the full code for this, mainly because it’s 1:40am at the time of writing. Let’s say it’s left as an exercise to the reader!

Wrapping up

What have we achieved? We managed to import and use Windows API functions in a way that is not detectable by static analysis tools. Along the way, we learned about the PEB and about the structure of a PE file. And hopefully, we had some fun – at least, I did. 🙂

Quick notes:

  • Everything we did would still be caught by dynamic analysis.
  • The code snippets work only for 64-bit processes. PM me if you’d like me to provide you with a 32-bit version.

I hope you liked this post! Comments below or tweets to @christophetd are always welcome.

2+

3 thoughts on “Hidden in PEB Sight: Hiding Windows API Imports With a Custom Loader

  1. Pingback:

  2. Pingback:

  3. I learned a lot, thanks for sharing.

    0

Leave a Reply

Your email address will not be published. Required fields are marked *