We have a secure channel. Now we need to survive. To understand how to hide malware, you first need to understand how security tools (EDR/AV) try to find it.
The Old Way: Win32 Injection
In the early days, if you wanted to inject shellcode into another process, you used the standard Windows API (Win32):
OpenProcess(): Get a handle to the target.VirtualAllocEx(): Allocate memory in the target.WriteProcessMemory(): Copy your shellcode.CreateRemoteThread(): Execute it.
This is noisy. EDRs hook these functions in user-mode (kernel32.dll). When you call OpenProcess, the EDR checks the arguments. "Oh, you're trying to open lsass.exe? Blocked."
The Bypass: System Calls (Syscalls)
To bypass these hooks, we go lower. We talk directly to the Kernel.
The functions in ntdll.dll are just wrappers. All they really do is set up a "ticket number" (called a System Service Number or SSN) in the EAX register and execute the syscall instruction.
Finding the SSN: Walking the PEB
How do we know that NtOpenProcess is SSN 0x26? We can't hardcode it because it changes with every Windows update. We must find it dynamically.
We do this by manually parsing the Process Environment Block (PEB).
- Find NTDLL: We read the
GSregister (x64) to find the PEB, then walk theLdr(Loader Data) list to find the base address ofntdll.dll. - Parse Exports: We read the Export Directory of
ntdll.dllto find the address ofZwOpenProcess. - Read the Bytes: We look at the first few bytes of the function.
mov r10, rcx mov eax, <SSN> ; We steal this number! syscall
Our Custom Implementation
We didn't just copy-paste SysWhispers (the standard tool for this). We modified it heavily to break signatures.
1. Custom Hashing Algorithm
Standard tools use well-known hashing algorithms (like DJB2) to find function names. EDRs know these constants. We implemented a custom, rolling XOR-rotate hash.
// syscalls.cpp
DWORD ComputeFunctionHash(PCSTR FunctionName) {
DWORD Hash = HASH_SEED; // Random seed per build
// ... custom bitwise operations ...
Hash = ROTATE_OP(Hash) ^ XOR_MAGIC;
return Hash;
}
This means our agent's "Import Table" (hashes) looks completely random and different for every build.
2. Obfuscated PEB Walking
Standard shellcode walks the PEB in a predictable way. We added "junk code" and obfuscated logic. Instead of comparing strings like "ntdll.dll", we compare hashes of the DLL names, and we split the checks into multiple non-linear steps to confuse emulators.
3. Junk Assembly (Polymorphism)
EDRs scan for the syscall stub patterns.
Standard SysWhispers stub:
mov eax, [SSN]
syscall
ret
Our stub injects random "junk" instructions that do nothing but change the byte signature:
mov [rsp+8], rcx ; Save registers (Standard)
xor r11, r11 ; Junk
add r11, 1 ; Junk
dec r11 ; Junk
mov eax, [SSN]
syscall
nop ; Padding
ret
This breaks static signatures that look for the exact byte sequence of a syscall stub.
The Evolution: Indirect Syscalls
Even with all this, executing syscall in our own memory is suspicious ("Mark of the Syscall").
So we use Indirect Syscalls.
Instead of executing syscall ourselves, we find a syscall instruction inside ntdll.dll and JMP to it.
sequenceDiagram
participant Agent
participant NTDLL
participant Kernel
Note over Agent: 1. Set SSN (EAX)
Note over Agent: 2. Set Arguments
Agent->>NTDLL: 3. JMP to "syscall" instruction
NTDLL->>Kernel: 4. CPU executes syscall
Kernel-->>NTDLL: 5. Returns to NTDLL
NTDLL-->>Agent: 6. Returns to Agent
This technique, popularized by tools like SysWhispers3 and research by KlezVirus, ensures that the call stack looks legitimate. The kernel sees the return address pointing to ntdll.dll, not our malware.
In the next post, we'll use these stealthy techniques to weaponize the agent.