Process Hollowing

Posted on Feb 23, 2022

Injecting code into explorer.exe or notepad.exe is not trivial to evade detection as these processes generally do not generate any network activity. The svchost.exe system process is a shared service process that allows several services to share this process to reduce resource consumption, which usually generates network activity. The svchost.exe process runs under a SYSTEM integrity level, and that will prevent us from injecting inside it from a lower integrity level. Instead, we could create a process called svchost.exe in a suspended state and inject it inside this process. Note that we don’t have to choose svchost.exe to process hollowing successfully.

Once the process is created, we would need to locate the EntryPoint of the executable and overwrite its in-memory content with our payload/shellcode and then resume the process and execute our shellcode inside the memory. However, Address Space Layout Randomization (ASLR) makes this procedure tricky. We need to use ZwQueryInformationProcess or NtQueryInformationProcess to get information about the target process such as the Process Environment Block (PEB), and from the PEB we can obtain the image base address of the process and parse the Portable Executable (PE) headers to locate the EntryPoint of the executable.

Let’s see this in action! :D

CreateProcessA

First, we need to call CreateProcessA and pass the path of svchost.exe to lpcommandline, which is C:\Windows\System32\svchost.exe. We will also need to tell this function to start in a suspended state. A suspended process is temporarily turned off and can be restarted in the same state.

NtQueryInformationProcess

We then call ZwQueryInformationProcess or NtQueryInformationProcess with then pass ProcessBasicInformation to the ProcessInformationClass to obtain a pointer to the Process Environment Block (PEB) structure.

Process Environment Block (PEB)

The PEB will contain the ImageBaseAddress of the newly created process, which can be accessed by adding PebBaseAddress+0x10. We can use Windbg to dissect the data structures. Here we can see that the ImageBaseAddress is 0x10 bytes away from the PROCESS_ENVIRONMENT_BLOCK (PEB)

0:006> dt _PEB
combase!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void

We can attach svchost.exe to Windbg and dissect these data structures. Here we can see that the ImageBaseAddress is 00007ff74d270000.

0:001> !peb
PEB at 000000b56c543000
    InheritedAddressSpace:    No
    ReadImageFileExecOptions: No
    BeingDebugged:            Yes
    ImageBaseAddress:         00007ff74d270000
    NtGlobalFlag:             0
    NtGlobalFlag2:            0
    Ldr                       00007ffbafc9a4c0
    Ldr.Initialized:          Yes
    Ldr.InInitializationOrderModuleList: 000002b2722048a0 . 000002b272204f00
    Ldr.InLoadOrderModuleList:           000002b272204a10 . 000002b272206eb0
    Ldr.InMemoryOrderModuleList:         000002b272204a20 . 000002b272206ec0
<...snipped...>

ReadProcessMemory

Since this is a remote process we will need to use ReadProcessMemory to read the PebBaseAddress+0x10 to give us the ImageBaseAddress.

IMAGE_DOS_HEADER

We can use the ImageBaseAddress at 00007ff74d270000 to dissect the IMAGE_DOS_HEADER.

0:001> dt _IMAGE_DOS_HEADER 00007ff74d270000
ntdll!_IMAGE_DOS_HEADER
   +0x000 e_magic          : 0x5a4d
   +0x002 e_cblp           : 0x90
   +0x004 e_cp             : 3
   +0x006 e_crlc           : 0
   +0x008 e_cparhdr        : 4
   +0x00a e_minalloc       : 0
   +0x00c e_maxalloc       : 0xffff
   +0x00e e_ss             : 0
   +0x010 e_sp             : 0xb8
   +0x012 e_csum           : 0
   +0x014 e_ip             : 0
   +0x016 e_cs             : 0
   +0x018 e_lfarlc         : 0x40
   +0x01a e_ovno           : 0
   +0x01c e_res            : [4] 0
   +0x024 e_oemid          : 0
   +0x026 e_oeminfo        : 0
   +0x028 e_res2           : [10] 0
   +0x03c e_lfanew         : 0n232

Here the e_lfanew value is converted to hex.

0:001> ?0n232
Evaluate expression: 232 = 00000000`000000e8

IMAGE_NT_HEADERS

The ImageBaseAddress + e_lfanew value should give us the _IMAGE_NT_HEADERS.

0:001> dt _IMAGE_NT_HEADERS 00007ff74d270000+0xe8
Symbol _IMAGE_NT_HEADERS not found.
0:001> dt _IMAGE_NT_HEADERS64 00007ff74d270000+0xe8
ntdll!_IMAGE_NT_HEADERS64
   +0x000 Signature        : 0x4550
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER64

_IMAGE_OPTIONAL_HEADER64

The ImageBaseAddress + e_lfanew + OptionalHeader should give us access to the _IMAGE_OPTIONAL_HEADER64 which contains the Relative Virtual Address (RVA) of the AddressOfEntryPoint.

0:001> dt _IMAGE_OPTIONAL_HEADER64 00007ff74d270000+0xe8+0x018
ntdll!_IMAGE_OPTIONAL_HEADER64
   +0x000 Magic            : 0x20b
   +0x002 MajorLinkerVersion : 0xe ''
   +0x003 MinorLinkerVersion : 0x14 ''
   +0x004 SizeOfCode       : 0x6600
   +0x008 SizeOfInitializedData : 0x5a00
   +0x00c SizeOfUninitializedData : 0
   +0x010 AddressOfEntryPoint : 0x4e80
   +0x014 BaseOfCode       : 0x1000
   +0x018 ImageBase        : 0x00007ff7`4d270000
   +0x020 SectionAlignment : 0x1000
   +0x024 FileAlignment    : 0x200
   +0x028 MajorOperatingSystemVersion : 0xa
   +0x02a MinorOperatingSystemVersion : 0
   +0x02c MajorImageVersion : 0xa
   +0x02e MinorImageVersion : 0
   +0x030 MajorSubsystemVersion : 0xa
   +0x032 MinorSubsystemVersion : 0
   +0x034 Win32VersionValue : 0
   +0x038 SizeOfImage      : 0x11000
   +0x03c SizeOfHeaders    : 0x400
   +0x040 CheckSum         : 0x1c364
   +0x044 Subsystem        : 2
   +0x046 DllCharacteristics : 0xc160
   +0x048 SizeOfStackReserve : 0x80000
   +0x050 SizeOfStackCommit : 0x4000
   +0x058 SizeOfHeapReserve : 0x100000
   +0x060 SizeOfHeapCommit : 0x1000
   +0x068 LoaderFlags      : 0
   +0x06c NumberOfRvaAndSizes : 0x10
   +0x070 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY

We will need to call ReadProcessMemory again to read the IMAGE_DOS_HEADER then obtain the _IMAGE_NT_HEADERS and finally obtain _IMAGE_OPTIONAL_HEADER64 to get the AddressOfEntryPoint.

Virtual Address of EntryPoint

To get access to the Virtual Address of the EntryPoint we can add ImageBaseAddress + AddressOfEntryPoint. In this example we get the value 00007ff74d274e80 which is the EntryPoint for svchost.exe.

0:001> dd 00007ff74d270000+0x4e80
00007ff7`4d274e80  28ec8348 000087e8 c4834800 ff66e928
00007ff7`4d274e90  ccccffff cccccccc cccccccc cccccccc
00007ff7`4d274ea0  cccccccc 6666cccc 00841f0f 00000000
00007ff7`4d274eb0  890d3b48 75000071 c1c14810 c1f76610
00007ff7`4d274ec0  0175ffff c9c148c3 0162e910 cccc0000
00007ff7`4d274ed0  cccccccc 38ec8348 24648348 33450020
00007ff7`4d274ee0  c03345c9 34d615ff c0330000 38c48348
00007ff7`4d274ef0  ccccccc3 ffcccccc 0034bb25 cccccc00

WriteProcessMemory and ResumeThread

We can now use WriteProcessMemory to overwrite the original in-memory content with our shellcode and call ResumeThread to resume the execution flow of the program, which will cause it to execute our shellcode.

PoC

A PoC has been made in Rust using NTAPI (ntdll.dll) rather than using winapi (kernel32.dll / kernelbase.dll).

https://github.com/memN0ps/arsenal-rs/tree/main/process_hollowing-rs

Detection on Virus Total

Detection at the time of writing.

Detection

References