The Malware Analyst's Cookbook is a great book. In it the authors talked about an interesting technique they called 'process hollowing'.
When I read about it, I was intrigued and played around a bit with the examples from the book. Then recently on a malware analysis investigation, we ran across it in the real world. Asking around a bit, it turns out this is actually a pretty common technique in the wild.
To effectively detect malware that uses process hollowing, we must first understand how this technique is used. Understanding the code patterns to look for is also crucial in order to recognize it when you see it while analyzing malware samples that use this technique.
Process hollowing is a technique used by some malware in which a legitimate process is loaded on the system solely to act as a container for hostile code. At launch, the legitimate code is deallocated and replaced with malicious code. The advantage is that this helps the process hide amongst normal processes better. If you inspect the process and its imports using conventional tools, they all look legit. The PEB is untouched, but the actual code and data of the process have been changed.
What does this look like in the code (i.e. how will you recognize it)? First, the malware starts a legitimate process using CreateProcess using the CREATE_SUSPENDED option in the fdwCreate flags parameter. MSDN tells us the following:
// This function is used to run a new program. It creates a new process // and its primary thread. The new process runs the specified executable // file.
BOOL CreateProcess(
LPCWSTR pszImageName,
LPCWSTR pszCmdLine,
LPSECURITY_ATTRIBUTES psaProcess,
LPSECURITY_ATTRIBUTES psaThread,
BOOL fInheritHandles,
DWORD fdwCreate,
LPVOID pvEnvironment,
LPWSTR pszCurDir,
LPSTARTUPINFOW psiStartInfo,
LPPROCESS_INFORMATION pProcInfo
);
// fdwCreate
// [in] Specifies additional flags that control the priority
// and the creation of the process.
//
// CREATE_SUSPENDED fdwCreate flag
// The primary thread of the new process is created in a suspended state,
// and does not run until the ResumeThread function is called.
The host program is now loaded but no code has been executed yet since it is started in suspended mode. The malware also has a handle to the process it started started through the pProcInfo structure passed to CreateProcess.
While the host process is suspended, the malware first unmaps (or hollows out) the legitimate code from memory in the host process. The ZwUnmapViewOfSection or NtUnmapViewOfSection WIN32 API function may be used to unmap the original code:
// NtUnmapViewOfSection and ZwUnmapViewOfSection are two versions of
// the same Windows Native System Services routine.
// The ZwUnmapViewOfSection routine unmaps a view of a section from
// the virtual address space of a subject process.
// a view can be a whole or partial mapping of a section object in
// the virtual address space of a process.
NTSTATUS ZwUnmapViewOfSection(
__in HANDLE ProcessHandle,
__in_opt PVOID BaseAddress
);
Because the unmap function is a kernel API function, you will often see the malware dynamically resolve it's function address at runtime as follows:
HMODULE handle = GetModuleHandle("ntdll.dll");
funcptr = GetProcAddress(handle, "NtUnmapViewOfSection"));
or
funcptr = GetProcAddress(handle, "ZwUnmapViewOfSection"));
The malware then allocates memory for the new code using VirtualAllocEx. It must ensure the code is marked as writeable and executable using the flProtect parameter. This is one of the giveaways that a process may contain malicious code, however as we'll see in a bit, it isn't completely reliable since the malware can change this setting when it is done filling in the hollowed process memory.
// Reserves or commits a region of memory within the virtual address
// space of a specified process.
LPVOID WINAPI VirtualAllocEx(
__in HANDLE hProcess,
__in_opt LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flAllocationType,
__in DWORD flProtect
);
// Memory Protection Constant PAGE_EXECUTE_READWRITE = 0x40
// Enables execute, read-only, or read/write access to the committed
// region of pages.
The malware then writes it's own new code into the hollow host process using WriteProcessMemory, writing data to the memory allocated in the host process with VirtualAllocEx.
// Writes data to an area of memory in a specified process. The entire
// area to be written to must be accessible or the operation fails.
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesWritten
);
If the malware author is careful, they will change the adjust code and data sections look normal with Read/Execute or Read-only protections using VirtualProtectEx. Thus we can't rely solely on memory protection settings for detection as it is often easily avoided by the malware authors.
// Changes the protection on a region of committed pages in the virtual
// address space of a specified process.
BOOL WINAPI VirtualProtectEx(
__in HANDLE hProcess,
__in LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flNewProtect,
__out PDWORD lpflOldProtect
);
The malware adjusts the remote context (context is just a fancy way of saying, frozen register state) to point to the new code section and may perform other cleanup tasks as necessary. The SetThreadContext function can be used to perform this step.
// Sets the context for the specified thread.
BOOL WINAPI SetThreadContext(
__in HANDLE hThread,
__in const CONTEXT *lpContext
);
Once everything is ready, the malware loader simply resumes the suspended process using ResumeThread.
// Decrements a thread's suspend count. When the suspend count is
// decremented to zero, the execution of the thread is resumed.
DWORD WINAPI ResumeThread(
__in HANDLE hThread
);
Another common characteristic is that the malware loader will incorporate it's own PE and MZ header parsing code in order to effectively take over the role of the system EXE loader. One dead giveaway is when the code tries to match the "MZ" magic header value to confirm it is working with an exe file. This type of header parsing is common in lots of malware tricks, so it isn't necessarily an indication of this specific technique.
The Volatility Framework is an excellent opensource tool for volatile memory forensic analysis. The core features of the tool are pretty robust, and there exist a large number of plugins for adding even more functionality.
One common technique for detecting hollowed processes is by scanning allocated memory for segments that have the RWX protection setting. As mentioned above, if the attacker forgot to fix memory protection flags with VirtualProtectEx, we can find it easily. A Volatility plugin by Michael Hale Leigh called 'malfind.py' or 'malware.py' does this as part of its scanning.
This is actually a useful general technique for detecting potentially malicious code, since certain dll injection and other techniques may be detected this way as well. As noted above, however, careful malware authors can easily avoid this by correcting protection settings after they are done writing to memory.
But, using volatility without any plugins we can dump processes to files and compare them with eachother or with their original file on the filesystem.
In the following example, we suspected that svchost.exe may be used as a host process for process hollowing in the malware we were investigating.
emonti$ vol.py pslist -f example.dump --profile=WinXPSP3x86 |grep svchost
Volatile Systems Volatility Framework 1.4_rc1
0x81f37428 svchost.exe 912 728 16 207 2010-11-10 22:48:22
0x82022da0 svchost.exe 992 728 10 283 2010-11-10 22:48:22
0x822a7da0 svchost.exe 1084 728 87 2243 2010-11-10 22:48:22
0x82264468 svchost.exe 1132 728 5 76 2010-11-10 22:48:22
0x82198160 svchost.exe 1188 728 13 172 2010-11-10 22:48:24
0x82203a78 svchost.exe 1568 728 4 105 2010-11-10 22:48:33
0x8233ad60 svchost.exe 2496 3764 5 156 2011-05-11 22:35:38
At a glance, we can see that the last svchost.exe has a different parent pid and start time from the rest, this stands out. But again, this is not always a conclusive indication of malware, nor is it a reliable method of detecting it since attackers can also take steps to modify this.
Next, we dump the processes using volatility's procexedump command. And copy in a legitimate svchost.exe from the filesystem for comparison.
emonti$ mkdir dumps
emonti$ cp legit_files/svchost.exe dumps/
emonti$ volatility procexedump -f example.dump --dump-dir=dumps --pid=912,992,104,1132,1188,1568,2496 --profile=WinXPSP3x86
Volatile Systems Volatility Framework 1.4_rc1
************************************************************************
Dumping svchost.exe, pid: 912 output: executable.912.exe
************************************************************************
Dumping svchost.exe, pid: 992 output: executable.992.exe
************************************************************************
Dumping svchost.exe, pid: 1132 output: executable.1132.exe
************************************************************************
Dumping svchost.exe, pid: 1188 output: executable.1188.exe
************************************************************************
Dumping svchost.exe, pid: 1568 output: executable.1568.exe
************************************************************************
Dumping svchost.exe, pid: 2496 output: executable.2496.exe
If we compare file sizes, we get further confirmation that something is wrong with pid 2496. Every other instance of svchost.exe has the same file size as the original filesystem file, but 2496 is considerably larger.
emonti$ cd dumps/
emonti$ ls -l
total 288
-rw-r--r-- 1 emonti staff 14336 May 11 18:15 executable.1132.exe
-rw-r--r-- 1 emonti staff 14336 May 11 18:15 executable.1188.exe
-rw-r--r-- 1 emonti staff 14336 May 11 18:15 executable.1568.exe
-rw-r--r-- 1 emonti staff 49152 May 11 18:15 executable.2496.exe
-rw-r--r-- 1 emonti staff 14336 May 11 18:15 executable.912.exe
-rw-r--r-- 1 emonti staff 14336 May 11 18:15 executable.992.exe
-rwxr-xr-x 1 emonti staff 14336 May 11 17:52 svchost.exe
Sometimes another effective way of detecting hollowing and other process tampering is to use fuzzy hashing to compare processes in memory against other same processes, or their original file on the filesystem. The industry standard tool for fuzzy hashing is called 'ssdeep' (by Jesse Kornblum) based on a spam detection algorithm called 'spamsum' (by Andrew Tridgell).
The Malware Analyst's Cookbook contains a recipe for a volatilty plugin to do this. But here's a quick way to do the same thing just using the command-line tools.
First lets see what a normal, legitimate process process looks like when compared to it's original file on disk.
emonti$ volatility pslist -f ../example.dump
Volatile Systems Volatility Framework 1.4_rc1
Offset(V) Name PID PPID Thds Hnds Time
---------- -------------------- ------ ------ ------ ------ -------------------
...
0x8223fc98 cmd.exe 560 500 1 80 2011-05-11 21:22:00
...
emonti$ volatility procexedump -f ../example.dump --pid=560 --dump-dir=.
Volatile Systems Volatility Framework 1.4_rc1
************************************************************************
Dumping cmd.exe, pid: 560 output: executable.560.exe
emonti$ cp ../legit_files/cmd.exe .
emonti$ ls -l executable.560.exe cmd.exe
-rwxr-xr-x 1 emonti TRUSTWAVE\domain users 389120 Apr 14 2008 cmd.exe
-rw-r--r-- 1 emonti TRUSTWAVE\domain users 389120 May 11 18:24 executable.560.exe
emonti$ ssdeep -d -a cmd.exe executable.560.exe
/Users/emonti/dumps/executable.560.exe matches /Users/emonti/dumps/cmd.exe (65)
The fuzzy hash score is 65, indicating that a considerable amount of common content in both files.
Contrast that with a fake instance of cmd.exe:
emonti$ volatility pslist -f ../example.dump
Volatile Systems Volatility Framework 1.4_rc1
Offset(V) Name PID PPID Thds Hnds Time
---------- -------------------- ------ ------ ------ ------ -------------------
...
0x822dca08 cmd.exe 264 500 4 69 2010-11-10 23:07:23
...
emonti$ volatility procexedump -f ../example.dump --pid=264 --dump-dir=.
Volatile Systems Volatility Framework 1.4_rc1
************************************************************************
Dumping cmd.exe, pid: 264 output: executable.264.exe
emonti$ ls -l cmd.exe executable.560.exe executable.264.exe
-rwxr-xr-x 1 emonti TRUSTWAVE\domain users 389120 Apr 14 2008 cmd.exe
-rw-r--r-- 1 emonti TRUSTWAVE\domain users 15360 May 11 18:32 executable.264.exe
-rw-r--r-- 1 emonti TRUSTWAVE\domain users 389120 May 11 18:24 executable.560.exe
emonti$ ssdeep -d -a cmd.exe executable.560.exe executable.264.exe
executable.560.exe matches cmd.exe (65)
executable.264.exe matches cmd.exe (0)
executable.264.exe matches executable.560.exe (0)
The suspicious cmd.exe has a different file size as well as a 0 ssdeep score when compared to the legitimate filesystem file, or another instance of cmd.exe in memory.
Obviously, it is better to use the file from the filesystem, or the same file from another known-good system if possible. We can't necessarily count on any of the processes or files on an infected system being legitimate.