Native API
Adversaries may interact with the native OS application programming interface (API) to execute behaviors. Native APIs provide a controlled means of calling low-level OS services within the kernel, such as those involving hardware/devices, memory, and processes. Adversaries abuse these APIs to execute code while bypassing higher-level defensive sensors, AMSI, and user-mode API hooks. Common attack patterns include: direct syscall invocation (bypassing ntdll.dll hooks entirely), process injection via NT memory APIs (NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx, RtlCreateUserThread), API unhooking by re-mapping a clean copy of ntdll.dll from disk, and spawning processes via NtCreateProcess or NtCreateProcessEx rather than the standard Win32 CreateProcess. Real-world actors including Cobalt Strike, Medusa Group, and tools like SysWhispers leverage direct syscalls specifically to evade EDR user-mode hooks.
What is T1106 Native API?
Native API (T1106) maps to the Execution tactic — the adversary is trying to run malicious code in MITRE ATT&CK.
This page provides production-ready detection logic for Native API, covering the data sources and telemetry it touches: Process: Process Creation, Process: OS API Execution, Module: Module Load, Microsoft Defender for Endpoint. The queries below are rated high severity at medium confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.
MITRE ATT&CK
- Tactic
- Execution
- Technique
- T1106 Native API
- Canonical reference
- https://attack.mitre.org/techniques/T1106/
// T1106 — Native API abuse: cross-process injection and direct NT API usage
// Signal 1: CreateRemoteThread API calls from suspicious initiating processes
let HighRiskParents = dynamic(["winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe",
"mshta.exe", "wscript.exe", "cscript.exe", "regsvr32.exe", "rundll32.exe",
"msiexec.exe", "werfault.exe", "explorer.exe"]);
let ProtectedTargets = dynamic(["lsass.exe", "csrss.exe", "winlogon.exe", "smss.exe", "wininit.exe"]);
let TrustedSecurityTools = dynamic(["MsMpEng.exe", "SenseIR.exe", "SenseCnC.exe", "kavtray.exe",
"bdservicehost.exe", "CylanceSvc.exe", "cb.exe"]);
let InjectionEvents =
DeviceEvents
| where Timestamp > ago(24h)
| where ActionType in~ ("CreateRemoteThreadApiCall", "MemoryRemoteProtect",
"NtAllocateVirtualMemoryApiCall", "InjectIntoProcess")
| extend TargetProcess = tostring(AdditionalFields.TargetProcessName)
| extend GrantedAccess = tostring(AdditionalFields.GrantedAccess)
| where
// Office/script interpreters injecting into anything
InitiatingProcessFileName has_any (HighRiskParents)
// Any process injecting into security-critical system processes
or (TargetProcess has_any (ProtectedTargets)
and not (InitiatingProcessFileName has_any (TrustedSecurityTools)))
| extend Signal = "cross_process_injection"
| project Timestamp, DeviceName, AccountName, Signal, ActionType,
InitiatingProcessFileName, InitiatingProcessCommandLine,
InitiatingProcessParentFileName, TargetProcess, GrantedAccess,
InitiatingProcessId, AdditionalFields;
// Signal 2: ntdll.dll loaded from non-standard path (API unhooking technique)
let UnhookingEvents =
DeviceImageLoadEvents
| where Timestamp > ago(24h)
| where FileName =~ "ntdll.dll"
| where not (FolderPath has_any ("\\Windows\\System32\\", "\\Windows\\SysWOW64\\",
"\\Windows\\WinSxS\\"))
| extend Signal = "ntdll_unhooking"
| project Timestamp, DeviceName, AccountName, Signal,
ActionType = "ImageLoad", InitiatingProcessFileName,
InitiatingProcessCommandLine, InitiatingProcessParentFileName,
TargetProcess = "", GrantedAccess = "",
InitiatingProcessId, AdditionalFields = todynamic(pack("DllPath", FolderPath));
union InjectionEvents, UnhookingEvents
| sort by Timestamp desc Detects Native API abuse through two complementary signals. Signal 1 monitors DeviceEvents for cross-process injection ActionTypes (CreateRemoteThreadApiCall, MemoryRemoteProtect, NtAllocateVirtualMemoryApiCall, InjectIntoProcess) where the initiating process is a high-risk Office/script interpreter or the target is a protected system process such as lsass.exe. Signal 2 monitors DeviceImageLoadEvents for ntdll.dll being loaded from non-standard filesystem locations, which is a reliable indicator of API unhooking (adversaries map a clean ntdll copy to bypass EDR hooks). Results are unioned to give analysts a single view of native API abuse activity.
Data Sources
Required Tables
False Positives
- Endpoint security products (AV, EDR, DLP agents) that legitimately use process injection for in-memory scanning or API hooking — exclude by InitiatingProcessFileName matching known security vendor executables
- Game anti-cheat engines (BattlEye, EasyAntiCheat, Vanguard) that inject into game processes for integrity monitoring — baseline these on gaming workstations
- Software DRM and licensing systems that use code injection to verify license state at runtime
- Legitimate debuggers (WinDbg, x64dbg, Visual Studio debugger) that use NtOpenProcess and write memory for debugging — expected on developer machines
- Virtualization and sandboxing tools (VMware Tools, VirtualBox Guest Additions) that load modified ntdll copies or interact with process memory for guest-host communication
Sigma rule & cross-platform mapping
The detection logic for Native API (T1106) above is provided in a vendor-neutral
form so you can deploy it on any SIEM. The same logic is shipped here as native
KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the
following logsource:
logsource:
category: process_creation
product: windows Browse the community-maintained Sigma rules for this technique:
Platform-specific guides for T1106
References (13)
- https://attack.mitre.org/techniques/T1106/
- https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
- https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls
- https://www.cyberbit.com/blog/endpoint-security/malware-mitigation-when-direct-system-calls-are-used/
- https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/
- https://github.com/jthuraisamy/SysWhispers2
- https://github.com/klezVirus/SysWhispers3
- https://undocumented.ntinternals.net/
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1106/T1106.md
- https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread
- https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon
- https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
- https://security.stackexchange.com/questions/270586/direct-system-calls-detection-edr
Testing Methodology
Validate this detection against 4 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.
- Test 1NtAllocateVirtualMemory Direct Call via PowerShell P/Invoke
Expected signal: Sysmon Event ID 1: Process Create for powershell.exe with CommandLine containing 'NtAllocateVirtualMemory'. MDE DeviceEvents: NtAllocateVirtualMemoryApiCall ActionType from the powershell.exe process. PowerShell ScriptBlock Log Event ID 4104: full script content including the DllImport declaration and the API call.
- Test 2Process Injection via NtCreateRemoteThread (C# Executable)
Expected signal: Sysmon Event ID 8 (CreateRemoteThread): SourceImage=powershell.exe, TargetImage=notepad.exe, StartAddress pointing to null (suspended thread with null entry point). Sysmon Event ID 10 (ProcessAccess): SourceImage=powershell.exe, TargetImage=notepad.exe, GrantedAccess=0x1F0FFF. MDE DeviceEvents: CreateRemoteThreadApiCall with InitiatingProcessFileName=powershell.exe.
- Test 3API Unhooking — Remap ntdll.dll from Disk
Expected signal: Sysmon Event ID 7 (ImageLoad): The ntdll.dll module is already loaded, but if the unhooking completed (in a real attack), a second load from a temp path would appear. PowerShell ScriptBlock Log Event ID 4104: full script showing ntdll.dll path access and byte comparison. MDE DeviceFileEvents: ReadFile operation on C:\Windows\System32\ntdll.dll from powershell.exe.
- Test 4Direct Syscall Execution via Inline Assembly (SysWhispers2-style)
Expected signal: If compiled: Sysmon Event ID 1 (Process Create) for syscall_test.exe; Sysmon Event ID 11 (File Create) for the .exe in %TEMP%; MDE DeviceFileEvents for the VirtualAlloc RWX allocation. The byte pattern 4C 8B D1 B8 xx 00 00 00 0F 05 C3 in the allocated memory region is the direct syscall stub signature. If not compiled: PowerShell ScriptBlock Event ID 4104 captures the stub bytes for signature validation.
Response Playbook
Triage
- Identify the initiating process — was it a user-facing application (Office, browser), a script interpreter (wscript.exe, cscript.exe), or an unexpected system binary? Cross-reference with the user's role: would this user normally run this application?
- Examine the target process — injection into lsass.exe, csrss.exe, or winlogon.exe is near-always malicious; injection into svchost.exe or explorer.exe is suspicious but has legitimate cases (DLL injection by security software)
- For CreateRemoteThread events: check StartModule and StartFunction fields — legitimate injection typically shows a known DLL (kernel32.dll, ntdll.dll) as the start module; NULL or unknown module indicates shellcode execution
- Check the GrantedAccess mask for ProcessAccess events: 0x1F0FFF (PROCESS_ALL_ACCESS), 0x0028 (PROCESS_VM_WRITE + PROCESS_VM_OPERATION), or 0x0438 are injection-grade access rights; 0x0400 (PROCESS_QUERY_INFORMATION) alone is benign
- Review parent-child process lineage: document the full chain. Malicious injection often starts with a document opened in Office → spawns wscript/cmd → loads shellcode that injects into a system process
- Check DeviceImageLoadEvents for the initiating process to identify if ntdll.dll was loaded from an unusual path — this is a strong indicator of API unhooking (the precursor to direct syscall execution)
- Look for concurrent network activity: did the injected process (or the injector) make outbound connections shortly before or after the injection event? C2 communication following injection is a key escalation indicator
Containment
- If LSASS injection confirmed: immediately isolate the endpoint via EDR network isolation — assume credentials are compromised and initiate emergency credential rotation for all accounts that logged onto the affected device
- If Office process is the injector: disable the affected user account, revoke active tokens in Entra ID/Azure AD, and check email for the phishing document or malicious attachment that initiated execution
- If lateral movement is suspected (multiple hosts showing similar injection patterns): expand isolation to all affected hosts before credential rotation to prevent re-compromise during the reset window
- Terminate the malicious process chain using EDR live response: kill the injected thread if the EDR supports targeted thread termination, otherwise kill the entire process — document the PIDs before termination
- If the injector loaded ntdll.dll from a non-standard path (API unhooking): hash and preserve the file before deletion — it is likely a dropped payload or a modified ntdll copy used as part of the evasion kit
- Block any identified C2 IP addresses and domains at the perimeter firewall and DNS sinkhole while investigation is ongoing
Evidence Collection
- Process memory dump of both the injecting process and the target process using EDR live response or ProcDump: `procdump.exe -ma <PID> C:\evidence\<process>_<PID>.dmp` — capture before process termination
- Full Sysmon Event ID 8 (CreateRemoteThread) record including StartAddress, StartModule, StartFunction — the start address in the target process's virtual address space identifies the injected shellcode entry point
- Sysmon Event ID 10 (ProcessAccess) records for the injection window: GrantedAccess, SourceProcessGUID, TargetProcessGUID — use GUIDs to correlate with other Sysmon events for the same process lifecycle
- Sysmon Event ID 7 (ImageLoad) for the injecting process: captures all DLLs loaded, including any non-standard ntdll.dll copy from disk (API unhooking evidence)
- Sysmon Event ID 3 (NetworkConnect) associated with the target process PID after injection — documents C2 communication origin
- Windows Event ID 4688 (Security log, process creation) or Sysmon Event ID 1: capture full command line of the injecting process and its parent chain
- File system artifacts: any dropped files in %TEMP%, %APPDATA%, or ProgramData written immediately before the injection event (Sysmon Event ID 11)
- ETW (Event Tracing for Windows) traces from the Microsoft-Windows-Kernel-Process provider if live response is available — captures NT API call sequences including direct syscall patterns
- MDE DeviceEvents table export for ActionType fields related to memory operations: NtAllocateVirtualMemoryApiCall, MemoryRemoteProtect, CreateRemoteThreadApiCall for the affected DeviceName and timeframe
Escalation Criteria
- ! Injection into lsass.exe — treat as confirmed credential theft; escalate immediately and initiate enterprise-wide credential rotation
- ! API unhooking detected (ntdll.dll loaded from non-standard path) combined with any cross-process memory write — indicates a sophisticated attacker specifically evading your EDR sensor
- ! Direct syscall indicators: shellcode in a remote process with a StartModule of NULL and no recognizable StartFunction — the injected code bypassed the Win32 API layer entirely
- ! Multiple hosts (3+) showing the same injection pattern within a 4-hour window — indicates automated lateral movement, not a targeted single-host compromise
- ! Initiating process is a service account or SYSTEM context — implies the attacker has already elevated privileges before the injection step
- ! C2 communication observed from the target process immediately following injection — completes the injection-to-execution-to-C2 chain, confirming full compromise
Investigation Guide
Forensic Artifacts
- >
Process memory: injected shellcode regions are typically marked RWX (read/write/execute) or transition from RW to RX — use `!address` in WinDbg or Volatility's `malfind` plugin on memory dumps to identify these regions - >
PE headers: the injected PE in memory may have mismatched VirtualAddress and FileOffset fields compared to a legitimate copy — `pe_carve` plugins in memory forensics tools identify these - >
PEB (Process Environment Block): manipulated processes may have a spoofed or incomplete PEB.Ldr module list — native API injection often does not update the PEB, making the injected module invisible to EnumProcessModules - >
Syscall stubs: direct syscall shellcode contains a recognizable pattern — `mov r10, rcx; mov eax, <syscall_number>; syscall; ret` — this byte sequence (4C 8B D1 B8 xx 00 00 00 0F 05 C3) can be carved from memory dumps - >
ntdll.dll on disk: if API unhooking was performed, compare the in-memory ntdll.dll for the suspicious process byte-for-byte against the clean on-disk copy at C:\Windows\System32\ntdll.dll — any differences in the .text section indicate hook removal - >
Prefetch: C:\Windows\Prefetch\<INJECTOR>.EXE-*.pf — records which DLLs were accessed, including if a non-standard ntdll.dll path was referenced - >
MFT ($MFT): check for temporary files written to disk in the seconds before the injection event — stage-1 droppers often write shellcode or a loader PE before calling injection APIs - >
Windows Error Reporting: C:\ProgramData\Microsoft\Windows\WER\ReportArchive — crashed injected processes generate WER reports containing memory state at time of crash
Tuning Guidance
Native API detection is inherently noisy because the same APIs are used by both malicious and legitimate software. Start with the most specific signals first: (1) Null StartModule in CreateRemoteThread events is the highest-confidence indicator — build an allowlist for any legitimate occurrences before broadly alerting. (2) For LSASS access events, build a permanent allowlist of your security tool executables (by SHA256 hash, not just filename) and alert on everything else. (3) The ntdll-from-non-standard-path signal is low-volume and high-confidence — investigate every hit. For ProcessAccess (Sysmon 10) events, avoid alerting on all injection-grade access masks globally as this will overwhelm analysts; instead restrict to either a protected target list (lsass, csrss, winlogon) or a high-risk initiator list. On environments with heavy legitimate use of native APIs (game development studios, security research teams, driver development), consider restricting detection to non-developer machines using device group tagging in MDE. Ensure Sysmon is configured with a schema version ≥ 4.22 to capture CreateRemoteThread StartModule and StartFunction fields — without these fields the shellcode indicator hunting query cannot function. Enable MDE's API Call Visibility feature (requires MDE Plan 2) to increase ActionType telemetry fidelity.
Hunting Queries
Hunt for non-security processes opening LSASS with memory read/write access rights — the prerequisite step before NtReadVirtualMemory-based credential theft. Access masks 0x1010 (PROCESS_QUERY_LIMITED_INFORMATION + PROCESS_VM_READ), 0x1410, and 0x1FFFFF (PROCESS_ALL_ACCESS) are commonly used in credential dumping tools. This differs from the main detection by focusing specifically on the OpenProcess call to LSASS rather than thread creation.
// Hunt for processes accessing LSASS with memory read/write rights — credential theft via native API
DeviceEvents
| where Timestamp > ago(7d)
| where ActionType == "OpenProcessApiCall"
| extend TargetProcess = tostring(AdditionalFields.TargetProcessName)
| extend GrantedAccess = tostring(AdditionalFields.GrantedAccess)
| where TargetProcess =~ "lsass.exe"
| where GrantedAccess in ("0x1010", "0x1410", "0x1fffff", "0x143a", "0x0010", "0x0410")
| where not (InitiatingProcessFileName has_any ("MsMpEng.exe", "SenseIR.exe", "TaskMgr.exe",
"perfmon.exe", "procexp.exe", "procexp64.exe", "lsm.exe", "csrss.exe"))
| summarize AccessCount=count(), FirstSeen=min(Timestamp), LastSeen=max(Timestamp),
AccessMasks=make_set(GrantedAccess) by DeviceName, AccountName,
InitiatingProcessFileName, InitiatingProcessCommandLine
| sort by AccessCount desc index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=10
| where TargetImage LIKE "%lsass.exe"
| eval GrantedAccess=lower(GrantedAccess)
| where match(GrantedAccess, "(0x1010|0x1410|0x1fffff|0x143a|0x0010|0x0410)")
| eval SourceImage=lower(SourceImage)
| where NOT match(SourceImage, "(msmpeng\.exe|senseir\.exe|taskmgr\.exe|perfmon\.exe|procexp|lsm\.exe|csrss\.exe)")
| stats count as AccessCount, earliest(_time) as FirstSeen, latest(_time) as LastSeen,
values(GrantedAccess) as AccessMasks by host, User, SourceImage, SourceCommandLine
| sort - AccessCount Hunt for ntdll.dll being loaded from any path outside System32, SysWOW64, or WinSxS. This is a reliable indicator of API unhooking — adversaries map a fresh, unhooked copy of ntdll from disk or a dropped file to restore original syscall stubs before making native API calls that would otherwise be caught by EDR hooks. This pattern is used by Cobalt Strike's `ntdll_unhook` feature and tools like Hell's Gate, Tartarus Gate, and SysWhispers.
// Hunt for ntdll.dll loaded from non-standard filesystem paths — API unhooking indicator
DeviceImageLoadEvents
| where Timestamp > ago(7d)
| where FileName =~ "ntdll.dll"
| where not (FolderPath has_any (
"\\Windows\\System32\\",
"\\Windows\\SysWOW64\\",
"\\Windows\\WinSxS\\"
))
| project Timestamp, DeviceName, AccountName, FolderPath,
InitiatingProcessFileName, InitiatingProcessCommandLine,
InitiatingProcessParentFileName, SHA256
| sort by Timestamp desc index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=7
| where FileName="ntdll.dll" OR lower(ImageLoaded) LIKE "%ntdll.dll"
| eval ImageLoaded=lower(ImageLoaded)
| where NOT (match(ImageLoaded, "(windows\\system32|windows\\syswow64|windows\\winsxs)"))
| table _time, host, User, Image, CommandLine, ImageLoaded, Hashes
| sort - _time Hunt for CreateRemoteThread events with a null or missing StartModule, indicating the remote thread's entry point is shellcode rather than a legitimate exported DLL function. Legitimate injections (DLL injection via LoadLibrary) always have kernel32.dll as the StartModule with LoadLibraryA/W as StartFunction. Missing StartModule is a near-definitive indicator of shellcode injection via native APIs (NtCreateThreadEx pointing directly into allocated memory).
// Hunt for unusual processes creating remote threads — broader than main detection, catches long-tail injectors
DeviceEvents
| where Timestamp > ago(7d)
| where ActionType == "CreateRemoteThreadApiCall"
| extend TargetProcess = tostring(AdditionalFields.TargetProcessName)
| extend StartModule = tostring(AdditionalFields.StartModule)
| extend StartFunction = tostring(AdditionalFields.StartFunction)
// Flag when StartModule is null/empty — shellcode, not a known DLL
| extend ShellcodeIndicator = isempty(StartModule) or StartModule == ""
| summarize ThreadCount=count(), Targets=make_set(TargetProcess),
HasShellcode=max(toint(ShellcodeIndicator)),
Modules=make_set(StartModule), Functions=make_set(StartFunction),
FirstSeen=min(Timestamp), LastSeen=max(Timestamp)
by DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine
| where HasShellcode == 1 or ThreadCount > 3 or array_length(Targets) > 2
| sort by HasShellcode desc, ThreadCount desc index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=8
| eval StartModule=coalesce(StartModule, "UNKNOWN")
| eval ShellcodeIndicator=if(StartModule="UNKNOWN" OR StartModule="-", 1, 0)
| stats count as ThreadCount, values(TargetImage) as Targets, max(ShellcodeIndicator) as HasShellcode,
values(StartModule) as Modules, values(StartFunction) as Functions,
earliest(_time) as FirstSeen, latest(_time) as LastSeen
by host, User, SourceImage, SourceCommandLine
| where HasShellcode=1 OR ThreadCount>3 OR mvcount(Targets)>2
| sort - HasShellcode ThreadCount Atomic Red Team Tests
Uses PowerShell Add-Type to P/Invoke ntdll.dll's NtAllocateVirtualMemory directly, bypassing the standard VirtualAlloc Win32 API. This simulates the first stage of shellcode injection — allocating executable memory using a native API that may not be hooked by some security tools. The allocation is in the current process and contains no payload; the purpose is to generate the telemetry signature, not execute code.
Command
$NativeCode = @"
using System;
using System.Runtime.InteropServices;
public class NtMemory {
[DllImport("ntdll.dll")]
public static extern int NtAllocateVirtualMemory(
IntPtr ProcessHandle,
ref IntPtr BaseAddress,
IntPtr ZeroBits,
ref IntPtr RegionSize,
uint AllocationType,
uint Protect);
}
"@
Add-Type -TypeDefinition $NativeCode
$baseAddr = [IntPtr]::Zero
$size = [IntPtr]0x1000
$status = [NtMemory]::NtAllocateVirtualMemory(
[System.Diagnostics.Process]::GetCurrentProcess().Handle,
[ref]$baseAddr, [IntPtr]::Zero, [ref]$size,
0x3000, # MEM_COMMIT | MEM_RESERVE
0x40) # PAGE_EXECUTE_READWRITE
Write-Host "NtAllocateVirtualMemory NTSTATUS: 0x$($status.ToString('X8'))"
Write-Host "Allocated RWX memory at: 0x$($baseAddr.ToString('X16'))" Cleanup
# Memory is freed when PowerShell process exits Expected Telemetry
Sysmon Event ID 1: Process Create for powershell.exe with CommandLine containing 'NtAllocateVirtualMemory'. MDE DeviceEvents: NtAllocateVirtualMemoryApiCall ActionType from the powershell.exe process. PowerShell ScriptBlock Log Event ID 4104: full script content including the DllImport declaration and the API call.
Expected Detection
MDE signal triggers on NtAllocateVirtualMemoryApiCall ActionType. PowerShell-based hunting queries will flag the Add-Type usage with ntdll function imports. Analysts should correlate with the PowerShell ScriptBlock log to see the full API call chain.
Compiles and executes a C# program inline via csc.exe that opens a target process (notepad.exe must be running) and creates a remote thread using NtCreateThreadEx — the native API equivalent of CreateRemoteThread. This tests detection of cross-process native API injection without delivering a payload — the remote thread immediately returns. Requires notepad.exe to be running; the test launches one first.
Command
Start-Process notepad.exe
Start-Sleep -Seconds 2
$notepadPid = (Get-Process notepad | Select-Object -First 1).Id
$CSharp = @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class NtInject {
[DllImport("ntdll.dll")]
static extern int NtCreateThreadEx(
out IntPtr hThread, uint DesiredAccess, IntPtr ObjectAttributes,
IntPtr ProcessHandle, IntPtr StartAddress, IntPtr Parameter,
bool CreateSuspended, int StackZeroBits, int SizeOfStackCommit,
int SizeOfStackReserve, IntPtr BytesBuffer);
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(uint access, bool inheritHandle, int pid);
[DllImport("kernel32.dll")]
static extern bool CloseHandle(IntPtr h);
public static void Run(int pid) {
IntPtr hProc = OpenProcess(0x1F0FFF, false, pid);
IntPtr hThread = IntPtr.Zero;
int status = NtCreateThreadEx(out hThread, 0x1FFFFF, IntPtr.Zero, hProc,
IntPtr.Zero, IntPtr.Zero, true, 0, 0, 0, IntPtr.Zero);
Console.WriteLine("NtCreateThreadEx status: 0x" + status.ToString("X8"));
CloseHandle(hThread); CloseHandle(hProc);
}
}
"@
Add-Type -TypeDefinition $CSharp
[NtInject]::Run($notepadPid) Cleanup
Stop-Process -Name notepad -ErrorAction SilentlyContinue Expected Telemetry
Sysmon Event ID 8 (CreateRemoteThread): SourceImage=powershell.exe, TargetImage=notepad.exe, StartAddress pointing to null (suspended thread with null entry point). Sysmon Event ID 10 (ProcessAccess): SourceImage=powershell.exe, TargetImage=notepad.exe, GrantedAccess=0x1F0FFF. MDE DeviceEvents: CreateRemoteThreadApiCall with InitiatingProcessFileName=powershell.exe.
Expected Detection
Both KQL and SPL main detection queries fire: KQL CreateRemoteThreadApiCall ActionType from powershell.exe; SPL EventCode=8 with HighRiskSource=1 (powershell.exe in parent list). RiskScore=60 from SPL query. Analyst should correlate Sysmon 8 and 10 events by SourceProcessGUID.
Demonstrates the API unhooking technique used by tools like Hell's Gate and Cobalt Strike's `ntdll_unhook`. Opens a new handle to C:\Windows\System32\ntdll.dll, reads sections from the clean on-disk copy, and compares them to the in-memory ntdll.dll to identify EDR hooks. This PowerShell implementation reports differences but does not overwrite memory — it generates the diagnostic telemetry without performing the actual unhook.
Command
$OnDiskNtdll = [System.IO.File]::ReadAllBytes("C:\Windows\System32\ntdll.dll")
$InMemoryNtdll = [System.Runtime.InteropServices.Marshal]::ReadByte
# Read in-memory ntdll base address via loaded module list
$ntdllModule = [System.Diagnostics.Process]::GetCurrentProcess().Modules |
Where-Object { $_.ModuleName -ieq "ntdll.dll" } | Select-Object -First 1
$baseAddr = $ntdllModule.BaseAddress
$moduleSize = $ntdllModule.ModuleMemorySize
$inMemBytes = New-Object byte[] $moduleSize
[System.Runtime.InteropServices.Marshal]::Copy($baseAddr, $inMemBytes, 0, $moduleSize)
# Compare first 0x500 bytes of .text section (offset ~0x1000 in PE)
$differences = 0
for ($i = 0x1000; $i -lt 0x1500; $i++) {
if ($OnDiskNtdll[$i] -ne $inMemBytes[$i]) { $differences++ }
}
Write-Host "[*] ntdll.dll loaded at: 0x$($baseAddr.ToString('X16'))"
Write-Host "[*] Bytes differing between disk and memory (potential hooks): $differences"
Write-Host "[*] If differences > 0, EDR hooks are present in ntdll .text section" Cleanup
# No files written, no cleanup required Expected Telemetry
Sysmon Event ID 7 (ImageLoad): The ntdll.dll module is already loaded, but if the unhooking completed (in a real attack), a second load from a temp path would appear. PowerShell ScriptBlock Log Event ID 4104: full script showing ntdll.dll path access and byte comparison. MDE DeviceFileEvents: ReadFile operation on C:\Windows\System32\ntdll.dll from powershell.exe.
Expected Detection
Hunting query for ntdll.dll loaded from non-standard paths fires in full attack scenario. In this safe test, the PowerShell ScriptBlock log shows the diagnostic comparison which analysts can use to determine if EDR hooks were present. A real attack would follow this reconnaissance step with memory overwrites via NtProtectVirtualMemory.
Uses a pre-compiled SysWhispers2-generated stub to invoke NtQuerySystemInformation via direct syscall (syscall instruction, bypassing ntdll.dll entirely). This tests detection of the direct-syscall evasion pattern used by Cobalt Strike BOFs, Havoc C2, and custom implants. The syscall number for NtQuerySystemInformation (0x36 on Windows 10 22H2) is embedded directly — no call through ntdll.dll occurs. Uses a .NET shim to execute the assembly stub.
Command
# Compile and execute a tiny C program that makes a direct syscall
# NtQuerySystemInformation syscall number 0x36 (Windows 10 22H2 — verify for your build)
$CCode = @"
#include <windows.h>
#include <stdio.h>
typedef NTSTATUS (NTAPI *pNtQSI)(ULONG, PVOID, ULONG, PULONG);
__declspec(noinline) NTSTATUS DirectSyscall(ULONG cls, PVOID buf, ULONG len, PULONG rlen) {
// Direct syscall stub: mov r10,rcx; mov eax,0x36; syscall; ret
unsigned char stub[] = {0x4C,0x8B,0xD1,0xB8,0x36,0x00,0x00,0x00,0x0F,0x05,0xC3};
PVOID mem = VirtualAlloc(NULL,sizeof(stub),MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy(mem, stub, sizeof(stub));
pNtQSI fn = (pNtQSI)mem;
NTSTATUS s = fn(cls, buf, len, rlen);
VirtualFree(mem, 0, MEM_RELEASE);
return s;
}
int main() {
char buf[256] = {0}; ULONG rlen = 0;
NTSTATUS s = DirectSyscall(5, buf, sizeof(buf), &rlen);
printf("Direct NtQuerySystemInformation syscall NTSTATUS: 0x%08X\n", s);
return 0;
}
"@
$TmpDir = "$env:TEMP\syscall_test"
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
$CFile = "$TmpDir\syscall_test.c"
$ExeFile = "$TmpDir\syscall_test.exe"
$CCode | Out-File -FilePath $CFile -Encoding ASCII
if (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\*\*\VC\Tools\MSVC\*\bin\Hostx64\x64\cl.exe") {
$cl = (Get-ChildItem "C:\Program Files (x86)\Microsoft Visual Studio" -Recurse -Filter "cl.exe" | Select-Object -First 1).FullName
& $cl /nologo $CFile /Fe:$ExeFile /link /SUBSYSTEM:CONSOLE 2>&1
if (Test-Path $ExeFile) { & $ExeFile }
} else {
Write-Host "[!] MSVC cl.exe not found — skipping compile. Telemetry: PowerShell ScriptBlock log captures the syscall stub bytes for detection validation."
Write-Host "[*] Syscall stub bytes that would be allocated: 4C 8B D1 B8 36 00 00 00 0F 05 C3"
} Cleanup
Remove-Item "$env:TEMP\syscall_test" -Recurse -Force -ErrorAction SilentlyContinue Expected Telemetry
If compiled: Sysmon Event ID 1 (Process Create) for syscall_test.exe; Sysmon Event ID 11 (File Create) for the .exe in %TEMP%; MDE DeviceFileEvents for the VirtualAlloc RWX allocation. The byte pattern 4C 8B D1 B8 xx 00 00 00 0F 05 C3 in the allocated memory region is the direct syscall stub signature. If not compiled: PowerShell ScriptBlock Event ID 4104 captures the stub bytes for signature validation.
Expected Detection
MDE ETWTI sensors may flag the RWX VirtualAlloc followed by code execution from that region. The direct syscall stub byte sequence is a known Yara signature (e.g., Win64.DirectSyscall). This test validates whether your EDR's kernel-mode sensors detect syscalls that bypass user-mode ntdll hooks — if not flagged, the EDR relies solely on user-mode hooks and is vulnerable to this bypass.