Skip to content

Call Stack Analysis

Once EDRs lost the ntdll stubs to direct and indirect syscalls (E4, E5), they moved the inspection point down — to the call stack itself. At any sensitive moment (a memory-protection flip, a remote thread, an LSASS handle grant) the sensor can ask: who called this? — and walk the frames backward from the syscall boundary. A legitimate program’s stack reads like a tree of signed modules; an implant’s stack reads like a confession. This module covers how x64 stacks actually work (frames, unwind metadata, RtlVirtualUnwind), how EDRs classify stacks, why frame spoofing exists and why it is fragile — and gives you a working stack classifier so the concept stops being a diagram and becomes code you can run.

Call stack analysis is the EDR practice of walking the frames of a thread’s stack at a sensitive call and classifying the modules that appear there. On x64 Windows, frames are described not by saved RBP chains (the x86 pattern) but by unwind metadata: every function declares (in .pdata/.xdata) how its epilogue restores the stack, and RtlVirtualUnwind can reconstruct the caller chain from any instruction pointer. That metadata is what makes modern stack walks complete — and also what makes spoofed stacks detectable: forged frames must still satisfy the unwinder.

A thread stack at the moment of NtProtectVirtualMemory:
higher addresses
┌────────────────────────────────────┐
│ kernel32!VirtualProtectStub │ frame 3 (signed, expected)
├────────────────────────────────────┤
│ kernelbase!VirtualProtectEx │ frame 2 (signed, expected)
├────────────────────────────────────┤
│ ntdll!NtProtectVirtualMemory │ frame 1 (signed, syscall stub)
├────────────────────────────────────┤
│ syscall → kernel transition │ ── boundary ──
└────────────────────────────────────┘
lower addresses
An implant's stack at the same boundary:
┌────────────────────────────────────┐
│ implant.exe!inject_stage+0x1c │ frame 2 (UNSIGNED — flagged)
├────────────────────────────────────┤
│ ntdll!NtProtectVirtualMemory │ frame 1 (clean, E5's work)
└────────────────────────────────────┘

The sensor’s rule set writes itself: unsigned frames anywhere in the unwind at a sensitive call → suspicious; unsigned frame adjacent to the kernel boundary → high confidence; unwind metadata that doesn’t resolve → higher still.

Stack analysis is the modern answer to the syscall bypass generation, and it is currently the highest-signal userland-side detection most vendors run [verify per vendor — implementations are not public]. For the operator, this means the E5-era habit of validating only the top frame is obsolete: the whole unwind is read. For the defender, stack telemetry is powerful but subtle — it produces its own false-positive classes (JIT runtimes, packers, legit hooking frameworks), so understanding the mechanics is what separates a tuned detection from noise. Interviews at detection-engineering and red-team roles converge here: “how would you detect a syscall bypass” has a one-word answer (“stacks”) and a thousand-word follow-up, and this module is the follow-up.

There is a second, less obvious reason: stack analysis is the first detection layer that inspects data describing control flow rather than control flow itself. Everything earlier in this series watched code execute; stacks watch the bookkeeping of execution. That shift generalizes — the same unwind machinery powers crash dumps, profilers, and CFG (Control Flow Guard) — so the time spent here pays across the whole Windows security surface.

Every non-leaf x64 function carries RUNTIME_FUNCTION entries in .pdata: begin/end RVAs and a pointer to unwind info in .xdata. The unwind info encodes prologue effects — which registers were pushed, how much RSP advanced, where the return address lives. Given an instruction pointer inside a function, RtlVirtualUnwind computes: the frame’s size, the return address location, and (with UNW_FLAG_CHAININFO) a jump to another function’s unwind — the mechanism behind tail calls and thunks. The stack walk is therefore not heuristic RBP-chasing (that is x86 nostalgia): it is table-driven, exact, and — critically for spoofing — verifiable.

.pdata (module): [begin_rva, end_rva, unwind_rva] × N functions
.xdata: version/flags | prologue size | push regs | frame size ...
Walk: RIP → find RUNTIME_FUNCTION (binary search) → .xdata →
compute caller frame → caller RIP → repeat
No .pdata entry → leaf function OR forged/foreign frame (a signal itself)

Two properties matter for detection. First, legitimacy is structural: a real call chain has unwind metadata for every frame, and the metadata agrees with the stack contents. Second, forgeries must fake the metadata too — a spoofed frame whose “return address” points mid-function (not after a call instruction) is visible to sensors that validate return-address provenance [verify: vendor-specific rigor].

At a sensitive moment, the sensor (in-process hook, ETW TI callback, or kernel sensor with thread context) captures the thread’s stack — via RtlCaptureStackBackTrace in userland sensors, or by unwinding from the trap frame in kernel sensors — and applies rules in increasing cost order:

  1. Top-frame module check — is the frame at the kernel boundary in a signed, expected module? (Defeated by E5; kept as a cheap first filter.)
  2. Full-unwalk module set — collect all modules in the unwind; flag unknown/unsigned ones; flag unexpected pairs (e.g., amsi.dll frames below a PowerShell script host are normal, the same frames below lsass.exe are not).
  3. Depth and gap analysis — legit call chains rarely exceed ~30 frames [verify: heuristic]; unwalkable gaps (no .pdata entry) suggest forged or foreign code.
  4. Return-address provenance — does each return address point after a call instruction in its module? Mid-function returns are the spoofing tell.
  5. Historical/behavioral join — does this thread’s earlier behavior (allocations, writes) match the story the stack tells?
Why CFG and XFG make stack forgeries harder

Control Flow Guard (CFG) validates indirect call targets against a bitmap of valid destinations at call time; eXtended CFG (XFG) hashes the call-site + target type into the validation, making gadget reuse harder [verify XFG rollout scope]. For stack spoofing, the relevant interaction is indirect: many spoofing chains route through gadgets in signed modules, and CFG narrows which gadgets can serve. Modern builds compile system binaries with CFG on by default, so a spoofing chain that predates CFG may simply have no valid routing on current Windows. This is the same pattern as E3→E4→E5: the defense is not a new sensor but a platform invariant that makes the old trick structurally invalid.

Stack classification must tolerate legitimate weirdness: JIT runtimes (V8, .NET) unwind through dynamically generated code with their own unwind registration; instrumentation products (APM agents, debuggers) inject frames; packers with valid unwind data still exist. This is why stack detections ship as correlated signals (stack shape + behavior + prevalence), not standalone verdicts — a lesson that generalizes to every evasion-vs-detection pair in this series.

Sensor ruleWhat it readsDefeated byNotes
Top-frame checkFrame at kernel boundaryE5 indirect syscallsCheap, kept as filter
Full module setAll unwind framesFrame spoofing (partial)Primary modern signal
Depth heuristicFrame countStack flattening [verify reliability]Tuned per product
Provenance checkReturn addr after a callMid-function gadget chainsHard to spoof cleanly
Unwind validation.pdata/.xdata agreementForged frames lacking metadataTable-driven, exact
Behavioral joinStack + prior thread behaviorCareful sequencingE2’s chain rule again

stackclass.py is a working stack classifier: it simulates stacks from four programs (legit app, direct-syscall implant, indirect-syscall implant, spoofed implant), unwinds them through a model of .pdata metadata, and applies the five sensor rules from the table above — printing per-rule verdicts. It makes the scoring of stacks concrete and lets you test how much spoofing is enough (spoiler: not enough, usually).

{1-6}
"""
stackclass.py — call-stack classifier: how EDRs read unwinds.
Simulates x64 stack walks for 4 program shapes and applies the
5 detection rules (top-frame, module set, depth, provenance, unwind).
Pure stdlib. Python 3.8+.
"""
from dataclasses import dataclass
@dataclass
class Frame:
module: str # image name
func: str
signed: bool = True
after_call: bool = True # return addr sits after a call insn
has_unwind: bool = True # .pdata entry exists
STACKS = {
# 1. legitimate application
"legit_app": [
Frame("ntdll.dll", "NtProtectVirtualMemory"),
Frame("kernelbase.dll", "VirtualProtectEx"),
Frame("implant-legit.exe", "main+0x8a", signed=True),
],
# 2. direct syscall implant (E4)
"direct_syscall": [
Frame("implant.exe", "syscall_stub+0x7", signed=False),
Frame("implant.exe", "inject_stage+0x1c", signed=False),
Frame("implant.exe", "worker_thread+0x40", signed=False),
],
# 3. indirect syscall implant (E5)
"indirect_syscall": [
Frame("ntdll.dll", "NtQueryIntervalProfile+0x12"), # gadget
Frame("ntdll.dll", "gadget_thunk"),
Frame("implant.exe", "inject_stage+0x1c", signed=False),
Frame("implant.exe", "worker_thread+0x40", signed=False),
],
# 4. spoofed implant: forged signed frames on top of one real frame
"spoofed": [
Frame("ntdll.dll", "NtQueryIntervalProfile+0x12"),
Frame("kernelbase.dll", "VirtualProtectStub"), # forged
Frame("rpcrt4.dll", "Invoke+0x30", after_call=False),# gadget, mid-func
Frame("implant.exe", "inject_stage+0x1c", signed=False),
],
# 5. broken forgery: frame without unwind metadata
"spoofed_broken": [
Frame("ntdll.dll", "NtQueryIntervalProfile+0x12"),
Frame("unknown_region", "???+0x0", signed=False, has_unwind=False),
Frame("implant.exe", "inject_stage+0x1c", signed=False),
],
}
KNOWN_SIGNED = {"ntdll.dll", "kernelbase.dll", "kernel32.dll", "rpcrt4.dll"}
def rule_topframe(frames):
return ("pass" if frames[0].signed
else "FLAG: unsigned frame at kernel boundary")
def rule_moduleset(frames):
bad = [f for f in frames if not f.signed]
if not bad:
return "pass"
if len(frames) - len(bad) == 1 and frames[0].signed:
return "SUSPECT: single unsigned frame below signed frames"
return f"FLAG: {len(bad)} unsigned frame(s) in unwind"
def rule_depth(frames):
if len(frames) > 30:
return "SUSPECT: unusually deep stack"
return "pass"
def rule_provenance(frames):
bad = [f for f in frames if not f.after_call]
return (f"FLAG: {len(bad)} return addr(s) not after a call"
if bad else "pass")
def rule_unwind(frames):
bad = [f for f in frames if not f.has_unwind]
return (f"FLAG: {len(bad)} frame(s) with no unwind metadata"
if bad else "pass")
RULES = [("top-frame", rule_topframe), ("module-set", rule_moduleset),
("depth", rule_depth), ("provenance", rule_provenance),
("unwind", rule_unwind)]
def classify(name, frames):
verdicts = [(n, fn(frames)) for n, fn in RULES]
flags = sum(v[1].startswith(("FLAG", "SUSPECT")) for v in verdicts)
verdict = "clean" if flags == 0 else (
"high-confidence" if sum(1 for v in verdicts
if v[1].startswith("FLAG")) >= 2
else "suspect")
print(f"--- {name} ---")
for f in frames:
print(f" {f.module}!{f.func}"
+ ("" if f.signed else " [UNSIGNED]")
+ ("" if f.after_call else " [mid-function ret]")
+ ("" if f.has_unwind else " [no .pdata]"))
for n, v in verdicts:
print(f" {n:<11} {v}")
print(f" VERDICT: {verdict}\n")
if __name__ == "__main__":
for name, frames in STACKS.items():
classify(name, frames)
  • Frame (lines 15–20) — the three properties sensors actually verify: signature state, return-address provenance (after_call), and unwind metadata (has_unwind). Every rule consumes only these — which is why forging a stack means faking all three, not just the module name.
  • The five stacks (lines 23–52) — the arms race as data. Compare indirect_syscall (one unsigned frame, below two clean ones) with spoofed (clean top, forged middle, one unsigned bottom): the spoof moves the signal, it does not delete it.
  • rule_provenance (lines 68–72) — the strongest rule: a return address that doesn’t sit after a call instruction in its module is structurally impossible in a genuine chain. The rpcrt4.dll!Invoke+0x30 gadget in the spoofed stack trips it — mid-function gadgets are the standard spoofing primitive, and this rule is their nemesis.
  • classify (lines 88–105) — verdict aggregation: zero flags = clean, two-plus FLAGs = high-confidence detection, otherwise suspect. Note the spoofed stack scores suspect, not clean — modeling why vendors treat spoofing as detection-friendly: it usually adds anomalies.

The sequence in a live implant: thread starts → E3/E4/E5 prepare a clean boundary → first sensitive call (say, NtProtectVirtualMemory on a page) fires the sensor’s stack capture → the unwinder reconstructs frames → rules score the stack → high-confidence flags trigger blocking or telemetry; suspect scores add correlation weight. Everything the operator does afterward happens under stacks that either tell the truth or lie detectably. Defenders see the same event twice: once as the operation (kernel callback) and once as the context (stack shape) — and the pairing is what makes modern EDRs hard to satisfy with single-layer evasion. The operational lesson: design the call chain so that even a truthful unwind is boring — shallow, signed-adjacent, and behaviorally consistent with the thread’s history.

  1. Recon — confirm stack telemetry is plausible: EDR generation, in-process hooks, TI provider presence (E2/E3 checks)
  2. Flatten — design call chains shallow: direct thunk to boundary, no deep helper towers, no unsigned frames above the worker
  3. Bound — E5 indirect syscalls put ntdll at the top; keep exactly ONE unsigned frame below (the thunk’s caller)
  4. Consistency — ensure thread history matches the story: allocations and writes in the same thread that later calls the boundary
  5. Avoid gadgets — mid-function returns trip provenance rules; prefer full Microsoft-authored stub copies over gadget chains
  6. Validate — self-check with RtlCaptureStackBackTrace in your own process; assert the shape before each sensitive call
  7. Expect correlation — even a clean-shaped stack joins behavioral history: the sequence rule (E2) is the last wall, and it is architectural

Goal: stackcheck.c — a Windows x64 tool that captures its own stack with RtlCaptureStackBackTrace, resolves each frame to a module via GetModuleHandleEx, and prints a classified unwind — the exact primitive a sensor runs, in ~120 lines you can compile and step through. Includes a deliberate deep/unsigned-frame demo so you can watch the classifier trip on your own code.

Design decisions: (1) capture-and-classify, never hook — the tool is a sensor emulator for learning; (2) module resolution via GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) — the documented way to map an address to its image; (3) verdicts match stackclass.py’s rule set so the simulator and the real tool agree.

{1-3}
/* stackcheck.c — capture + classify your own x64 stack.
Build (MSVC): cl /W4 /O2 stackcheck.c
Build (mingw): x86_64-w64-mingw32-gcc -O2 -o stackcheck.exe stackcheck.c
Educational: prints a classified unwind of the calling thread. */
#include <windows.h>
#include <stdio.h>
#include <string.h>
#define MAX_FRAMES 32
typedef struct {
void *addr;
char module[MAX_PATH];
int is_signed; /* modeled: known system module */
} Frame;
/* model of a signed-module allowlist (extend as needed) */
static const char *SIGNED[] = {
"ntdll.dll", "kernel32.dll", "kernelbase.dll", "rpcrt4.dll",
"ucrtbase.dll", "msvcrt.dll", "KERNEL32.DLL", "ntdll.DLL", NULL
};
static int classify_module(const char *m) {
for (int i = 0; SIGNED[i]; i++)
if (_stricmp(m, SIGNED[i]) == 0) return 1;
return 0;
}
static int capture(Frame *fr, int max) {
void *addrs[MAX_FRAMES];
USHORT n = RtlCaptureStackBackTrace(0, max, addrs, NULL);
for (USHORT i = 0; i < n; i++) {
fr[i].addr = addrs[i];
HMODULE h = NULL;
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)addrs[i], &h);
if (h && GetModuleFileNameA(h, fr[i].module, MAX_PATH)) {
char *base = strrchr(fr[i].module, '\\');
memmove(fr[i].module, base ? base + 1 : fr[i].module,
strlen(base ? base + 1 : fr[i].module) + 1);
fr[i].is_signed = classify_module(fr[i].module);
} else {
strcpy(fr[i].module, "<unknown-region>");
fr[i].is_signed = 0;
}
}
return n;
}
static void report(Frame *fr, int n) {
printf("captured %d frames:\n", n);
int unsigned_cnt = 0, unknown = 0;
for (int i = 0; i < n; i++) {
printf(" %02d %-22s %p%s\n", i, fr[i].module, fr[i].addr,
fr[i].is_signed ? "" : " [UNSIGNED]");
if (!fr[i].is_signed) unsigned_cnt++;
if (strcmp(fr[i].module, "<unknown-region>") == 0) unknown++;
}
printf("rules:\n");
printf(" module-set %s\n",
unsigned_cnt == 0 ? "pass"
: (unsigned_cnt == 1 && fr[0].is_signed)
? "SUSPECT: one unsigned frame below signed frames"
: "FLAG: multiple unsigned frames");
if (unknown)
printf(" unwind FLAG: %d frame(s) with no module/region\n",
unknown);
printf(" verdict %s\n",
(unsigned_cnt == 0 && unknown == 0) ? "clean"
: (unsigned_cnt >= 2 || unknown) ? "high-confidence"
: "suspect");
}
/* deep chain helper: pads the stack to test the depth heuristic */
__attribute__((noinline)) static void level5(Frame *fr, int *n) { *n = capture(fr, MAX_FRAMES); }
__attribute__((noinline)) static void level4(Frame *fr, int *n) { level5(fr, n); }
__attribute__((noinline)) static void level3(Frame *fr, int *n) { level4(fr, n); }
__attribute__((noinline)) static void level2(Frame *fr, int *n) { level3(fr, n); }
__attribute__((noinline)) static void level1(Frame *fr, int *n) { level2(fr, n); }
int main(void) {
Frame fr[MAX_FRAMES];
int n = 0;
printf("=== shallow chain ===\n");
n = capture(fr, MAX_FRAMES);
report(fr, n);
printf("\n=== deep chain (5 helper levels) ===\n");
level1(fr, &n);
report(fr, n);
return 0;
}
  • capture (lines 39–58) — RtlCaptureStackBackTrace(0, max, addrs, NULL) returns the calling thread’s return-address chain: index 0 is the caller of capture, walking outward. GetModuleHandleEx with FROM_ADDRESS maps each address to its image — the module-set rule, implemented.
  • report (lines 61–85) — applies module-set and unwind rules with the same verdict logic as stackclass.py (pass/SUSPECT/FLAG aggregation). The <unknown-region> case models a frame in memory with no image backing — the unwalkable gap.
  • The level1..5 chain (lines 88–92) — a deliberate deep chain: run it and watch frame counts grow while all modules stay signed (depth rule has nothing to flag — showing why depth alone is a weak signal without unsigned frames).
$ x86_64-w64-mingw32-gcc -O2 -o stackcheck.exe stackcheck.c
PS> .\stackcheck.exe
=== shallow chain ===
captured 6 frames:
00 stackcheck.exe 00007FF6A3B11020 [UNSIGNED]
...
=== deep chain (5 helper levels) ===
captured 11 frames:
00 stackcheck.exe ...
...
# debugger exercise: bp on RtlCaptureStackBackTrace; inspect frames;
# then VirtualAlloc an RWX region, copy a ret-gadget there, and call
# through it — the report should show <unknown-region> + FLAG lines.

The RWX-gadget exercise is the payoff: you will have manufactured, in your own process, exactly the shape sensors flag — safely, on your own machine, with the classification printing in front of you.

stackcheck.exe is a sensor emulator — benign by design. The pattern worth defending against is its abusive cousin: tooling that captures stacks repeatedly before sensitive operations (pre-flight validation like E5’s improvements list suggests). Defender-side: repeated RtlCaptureStackBackTrace-shaped behavior is invisible (it’s in-process), but its consumers are visible — implant tooling that branches on stack shape tends to leave the RWX regions, section maps, and handle chains that E2’s sequence rules catch. Detection engineering takeaway: stack telemetry is strongest as a join (stack shape × prior behavior), never standalone — that is precisely how stackclass.py models it.

  1. Return-address provenance: for each frame, read the bytes before the return address (addr - 5 for a near call) and verify an E8/FF opcode — the strongest rule, implemented for real.
  2. Unwind metadata walk: parse .pdata of the module containing each frame (the PE parsing from E3/E9 ports directly) and verify .xdata resolves — replacing the <unknown-region> heuristic with the actual mechanism.
  3. Cross-thread history: keep per-thread frame-count baselines and flag sudden depth changes (thread reuse abuse).
  4. Add a CFG check: query whether each indirect-call target is in the valid-call bitmap [verify API exposure — ProcessDynamicCodePolicy level detail].
  5. Port the classifier to consume remote thread stacks (kernel-mode exercise; pair with M8’s rootkit reading list).
SourceIDWhat firesNotes
EDR sensor—Stack-shape classification at sensitive calls (vendor internal)The layer this module models
Sysmon10ProcessAccess at the sensitive moment (handle grants)The event whose context includes stacks
ETWThreat-IntelligenceKernel-correlated memory ops with unwind context [verify access model]Microsoft’s structural answer
Sysmon8CreateRemoteThread — stack of the new thread startThread-start stacks are a classic check
Wazuh[verify rule IDs vs ruleset]Joins Sysmon 8/10 chainsSequence + shape correlation
Suricatasid:1000001 (custom)Downstream network behaviorUnchanged by stack games

Defender guidance: enable vendor stack-based detections where offered; tune against JIT-heavy estates before enforcing; and pair every stack rule with a behavioral join — the false-positive classes (JIT, instrumentation) are exactly the noise a standalone rule ships with.

  • Design truthful-but-boring stacks: shallow chains, one unsigned frame (your thunk caller), everything else signed — the goal is a stack that would classify clean, not one that must lie.
  • Prefer Microsoft-authored bytes at the boundary: the E5 stub-copy trick keeps register setup inside ntdll’s own bytes; gadget chains near the boundary are provenance-rule bait.
  • Avoid mid-function returns at the boundary: call into gadget regions puts a non-call-site address in the unwind — the strongest sensor rule watches exactly that.
  • Keep thread history consistent: allocate, write, and protect memory in the same thread that executes — split-personality threads (alloc in A, execute in B) read as story mismatch in behavioral joins.
  • Frame spoofing, honestly: it moves the unsigned frames deeper and usually adds mid-function returns — treat it as research, not a default; test against a full-unwind model (this module’s code) before shipping.
  • Re-validate per phase: module loads change unwind metadata (new .pdata regions); a stack that was clean at phase 1 may not be at phase 3.

⚠️ What caught me: I spent a week perfecting an indirect-syscall path and never once looked below the ntdll frame — my “clean” stack had nine unsigned frames from helper functions between my worker and the thunk. One stack-aware sensor later, the lesson was permanent: the boundary is checked in both directions, and depth is a shape. Now my rule is “one unsigned frame or redesign” — every helper between the worker and the boundary is a liability I have to justify.

  1. Run stackclass.py; tabulate the five stacks’ verdicts; then craft a spoofed_v2 stack that passes provenance (all after_call=True) and note what still flags it — that residual is the current research frontier.
  2. Build stackcheck.c; run both chains; then add the provenance improvement and re-run against a gadget-call demo (RWX region + ret) — watch the FLAG you manufactured.
  3. In x64dbg, breakpoint RtlProtectVirtualMemory… rather NtProtectVirtualMemory in your own process; single-step out and manually unwind two frames using the .pdata viewer (WinDbg’s !unwind or x64dbg’s trace) — connect the metadata to stackclass.py’s model.
  4. EDR lab (licensed test tenant): trigger a known stack-flagged operation (E4’s direct syscall demo) and compare the alert’s described logic against the five rules — most vendor docs describe rule classes, not rules; map what you can.
  5. False-positive study: run a JIT-heavy app (any Chromium-based browser) under your stackcheck.exe in a child process (or port it to a DLL) and catalog the unsigned-frame patterns legit software produces — this is the tuning homework defenders actually do.

Technical: How does x64 stack unwinding differ from x86 RBP-chains, and why does unwind metadata make stack walks exact rather than heuristic? Technical: Name the five stack-analysis rules this module models and rank them by false-positive risk in a JIT-heavy enterprise. Deep-dive: A spoofed stack passes the top-frame and module-set rules but fails provenance. Explain the mid-function-return detection, then design a gadget chain that would pass it — and name the rule that still catches the result. Conceptual: Why is stack analysis described as inspecting “data describing control flow” rather than control flow, and what detection generalizations does that enable? Scenario: Your indirect-syscall implant flags only on hosts with vendor X. Given only this module’s model, list the stack-shape hypotheses to test, ordered by probability. Practical: Implement the provenance check in pseudocode (read bytes before each return address, verify call opcodes) and state its failure modes on optimized code.

E5 (Indirect System Call Analysis) · E4 (Direct System Call Analysis) · E2 (EDR Internal Architecture) · BT10 (Detecting Evasion)