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.
Definition
Section titled “Definition”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.
Why It Matters
Section titled “Why It Matters”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.
Detailed Explanation
Section titled “Detailed Explanation”How the x64 unwinder actually works
Section titled “How the x64 unwinder actually works”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].
What EDRs classify, concretely
Section titled “What EDRs classify, concretely”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:
- 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.)
- Full-unwalk module set — collect all modules in the unwind; flag unknown/unsigned ones; flag unexpected pairs (e.g.,
amsi.dllframes below aPowerShellscript host are normal, the same frames belowlsass.exeare not). - Depth and gap analysis — legit call chains rarely exceed ~30 frames [verify: heuristic]; unwalkable gaps (no
.pdataentry) suggest forged or foreign code. - Return-address provenance — does each return address point after a call instruction in its module? Mid-function returns are the spoofing tell.
- 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.
The false-positive problem
Section titled “The false-positive problem”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.
Common Techniques
Section titled “Common Techniques”| Sensor rule | What it reads | Defeated by | Notes |
|---|---|---|---|
| Top-frame check | Frame at kernel boundary | E5 indirect syscalls | Cheap, kept as filter |
| Full module set | All unwind frames | Frame spoofing (partial) | Primary modern signal |
| Depth heuristic | Frame count | Stack flattening [verify reliability] | Tuned per product |
| Provenance check | Return addr after a call | Mid-function gadget chains | Hard to spoof cleanly |
| Unwind validation | .pdata/.xdata agreement | Forged frames lacking metadata | Table-driven, exact |
| Behavioral join | Stack + prior thread behavior | Careful sequencing | E2’s chain rule again |
Full Working Code
Section titled “Full Working Code”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 the5 detection rules (top-frame, module set, depth, provenance, unwind).Pure stdlib. Python 3.8+."""from dataclasses import dataclass
@dataclassclass 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)Code Walkthrough
Section titled “Code Walkthrough”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) withspoofed(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 acallinstruction in its module is structurally impossible in a genuine chain. Therpcrt4.dll!Invoke+0x30gadget 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.
How It Reaches The Target
Section titled “How It Reaches The Target”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.
Exploitation Chain
Section titled “Exploitation Chain”- Recon — confirm stack telemetry is plausible: EDR generation, in-process hooks, TI provider presence (E2/E3 checks)
- Flatten — design call chains shallow: direct thunk to boundary, no deep helper towers, no unsigned frames above the worker
- Bound — E5 indirect syscalls put ntdll at the top; keep exactly ONE unsigned frame below (the thunk’s caller)
- Consistency — ensure thread history matches the story: allocations and writes in the same thread that later calls the boundary
- Avoid gadgets — mid-function returns trip provenance rules; prefer full Microsoft-authored stub copies over gadget chains
- Validate — self-check with RtlCaptureStackBackTrace in your own process; assert the shape before each sensitive call
- Expect correlation — even a clean-shaped stack joins behavioral history: the sequence rule (E2) is the last wall, and it is architectural
Building Your Own Tool
Section titled “Building Your Own Tool”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;}Walkthrough
Section titled “Walkthrough”capture(lines 39–58) —RtlCaptureStackBackTrace(0, max, addrs, NULL)returns the calling thread’s return-address chain: index 0 is the caller ofcapture, walking outward.GetModuleHandleExwithFROM_ADDRESSmaps 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 asstackclass.py(pass/SUSPECT/FLAGaggregation). The<unknown-region>case models a frame in memory with no image backing — the unwalkable gap.- The
level1..5chain (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).
Compilation and test procedure
Section titled “Compilation and test procedure”$ x86_64-w64-mingw32-gcc -O2 -o stackcheck.exe stackcheck.cPS> .\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.
Detection considerations
Section titled “Detection considerations”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.
Improvements to add
Section titled “Improvements to add”- Return-address provenance: for each frame, read the bytes before the return address (
addr - 5for a nearcall) and verify anE8/FFopcode — the strongest rule, implemented for real. - Unwind metadata walk: parse
.pdataof the module containing each frame (the PE parsing from E3/E9 ports directly) and verify.xdataresolves — replacing the<unknown-region>heuristic with the actual mechanism. - Cross-thread history: keep per-thread frame-count baselines and flag sudden depth changes (thread reuse abuse).
- Add a CFG check: query whether each indirect-call target is in the valid-call bitmap [verify API exposure —
ProcessDynamicCodePolicylevel detail]. - Port the classifier to consume remote thread stacks (kernel-mode exercise; pair with M8’s rootkit reading list).
Detection — Defender View
Section titled “Detection — Defender View”| Source | ID | What fires | Notes |
|---|---|---|---|
| EDR sensor | — | Stack-shape classification at sensitive calls (vendor internal) | The layer this module models |
| Sysmon | 10 | ProcessAccess at the sensitive moment (handle grants) | The event whose context includes stacks |
| ETW | Threat-Intelligence | Kernel-correlated memory ops with unwind context [verify access model] | Microsoft’s structural answer |
| Sysmon | 8 | CreateRemoteThread — stack of the new thread start | Thread-start stacks are a classic check |
| Wazuh | [verify rule IDs vs ruleset] | Joins Sysmon 8/10 chains | Sequence + shape correlation |
| Suricata | sid:1000001 (custom) | Downstream network behavior | Unchanged 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.
Evasion Techniques
Section titled “Evasion Techniques”- 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:
callinto 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
.pdataregions); 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.
Lab Practice
Section titled “Lab Practice”- Run
stackclass.py; tabulate the five stacks’ verdicts; then craft aspoofed_v2stack that passes provenance (allafter_call=True) and note what still flags it — that residual is the current research frontier. - 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. - In x64dbg, breakpoint
RtlProtectVirtualMemory… ratherNtProtectVirtualMemoryin your own process; single-step out and manually unwind two frames using the.pdataviewer (WinDbg’s!unwindor x64dbg’s trace) — connect the metadata tostackclass.py’s model. - 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.
- False-positive study: run a JIT-heavy app (any Chromium-based browser) under your
stackcheck.exein 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.
Interview Questions
Section titled “Interview Questions”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.
Related Modules
Section titled “Related Modules”E5 (Indirect System Call Analysis) · E4 (Direct System Call Analysis) · E2 (EDR Internal Architecture) · BT10 (Detecting Evasion)