Probabilistic Call Stack: A Deep Dive into Non-Deterministic Execution Paths
Abstract
This article explores the concept of Probabilistic Call Stacks (also known as Non-Deterministic Call Stacks), a technique that creates varying execution paths to reach the same final payload. By randomizing the call chain at runtime, each execution produces a unique stack signature, making pattern-based detection more challenging. This document provides a comprehensive analysis of the implementation, discusses the underlying concepts, examines code patterns, and suggests potential improvements for both offensive research and defensive detection strategies.
PoC: https://github.com/CyberSecurityUP/Probabilistic-Call-Stack-PoC
Table of Contents
Introduction
Understanding Call Stack Fingerprinting
The Probabilistic Approach
Architecture Overview
Implementation Deep Dive
Path Analysis
Stack Trace Capture Mechanism
Potential Improvements
Detection Strategies
Conclusion
1. Introduction
Endpoint Detection and Response (EDR) solutions employ various techniques to identify malicious behavior. One such technique involves analyzing the call stack at the moment suspicious API calls are made. The call stack reveals the sequence of function calls that led to a particular point in execution, providing valuable context about the code's behavior.
Traditional malware often exhibits predictable call patterns. When a payload executes, the stack trace typically shows a consistent path from the entry point to the malicious function. EDRs can fingerprint these patterns and flag executions that match known signatures.
Probabilistic Call Stacks aim to break this determinism by introducing randomness into the execution path. While the final behavior remains identical (the same payload executes), the journey to reach that payload varies with each execution, producing different stack signatures.
Use Cases
EDR Development: Testing detection capabilities against polymorphic execution patterns
Red Team Operations: Understanding how call stack analysis works and its limitations
Security Research: Studying the relationship between execution paths and detection mechanisms
Malware Analysis: Recognizing this technique when analyzing samples
2. Understanding Call Stack Fingerprinting
What is a Call Stack?
The call stack is a data structure that stores information about the active subroutines (functions) in a program. When function A calls function B, the return address (where to continue in A after B finishes) is pushed onto the stack. This creates a chain of return addresses representing the execution path.
EDR Call Stack Analysis
EDRs hook critical Windows APIs (e.g., VirtualAlloc, CreateRemoteThread, NtWriteVirtualMemory) and capture the call stack when these functions are invoked. The analysis typically involves:
Return Address Validation: Checking if return addresses point to legitimate code sections
Module Attribution: Identifying which modules (DLLs, EXEs) appear in the stack
Pattern Matching: Comparing the stack structure against known malicious patterns
Depth Analysis: Examining the call depth and presence of expected intermediate functions
The Fingerprinting Problem
When malware uses a consistent execution flow, the call stack becomes a reliable fingerprint:
Every execution produces the same stack, making detection straightforward. The EDR simply needs to flag this specific pattern.
3. The Probabilistic Approach
Core Concept
Instead of a single deterministic path, we create multiple equivalent paths that all lead to the same destination. At runtime, one path is selected randomly:
Each path may have:
Different nesting depths
Different intermediate function names
Different auxiliary operations (timing, memory, system queries)
Different call mechanisms (direct, pointer-based, recursive)
Mathematical Perspective
If we have N distinct paths, each with potentially different characteristics, the probability of seeing the exact same call stack twice is significantly reduced. For our implementation with 13 paths, where some paths (like Path K and M) have internal variability:
Path K: 4 possible depths (1-4 recursion levels)
Path M: 4 possible routes (2 branch points with 2 options each)
This creates effectively: 7 fixed paths + 4 (Path K variants) + 4 (Path M variants) = 15+ unique stack signatures
4. Architecture Overview
Component Diagram
File Structure
5. Implementation Deep Dive
5.1 Configuration and Global State
Analysis:
NUM_WRAPPERS: Defines the number of available paths. Increasing this value adds more randomization potential.MAX_STACK_DEPTH: Maximum frames to capture during stack trace. 64 is sufficient for most scenarios.ENABLE_STACK_TRACE: Compile-time toggle for stack visualization. Disable in production for stealth.PayloadFunc: Function pointer typedef enables indirect calling mechanisms.
5.2 The Payload Function
Analysis:
This is the convergence point - all paths lead here
Uses
MessageBoxAas a benign, visible indicator of executionIn real scenarios, this could be any operation: shellcode execution, API call, etc.
The payload itself is deterministic; only the path to reach it varies
5.3 Auxiliary Functions
Auxiliary functions serve two purposes:
Add stack depth - More functions in the call chain
Add behavioral noise - Different API calls create different patterns
API Diversity Benefits:
aux_small_delay
Sleep()
Timing variation
aux_get_time
GetSystemTime()
System interaction
aux_heap_operation
HeapAlloc/HeapFree
Memory operations
aux_query_perf
QueryPerformanceCounter
High-precision timing
aux_thread_info
GetCurrentThreadId/ProcessId
Process context
5.4 Path Selection Mechanism
Analysis:
Function pointer array enables runtime path selection
rand() % NUM_WRAPPERSprovides uniform distributionThe indirection through function pointers adds a layer of obfuscation
Easy to extend: just add new functions to the array
6. Path Analysis
6.1 Path A: Direct (Minimal Depth)
Stack Depth: 1 wrapper level Call Stack:
Characteristics:
Simplest path with minimal overhead
Single auxiliary call for slight variation
Useful as a baseline for comparison
6.2 Path B: Nested (2 Levels)
Stack Depth: 2 wrapper levels Call Stack:
Characteristics:
Introduces basic nesting concept
Different auxiliary functions at each level
Demonstrates how depth increases stack uniqueness
6.3 Path C: Deep (3 Levels)
Stack Depth: 3 wrapper levels Characteristics:
Linear descent through three named levels
Memory and timing operations mixed in
6.4 Path D: Indirect (Function Pointer)
Stack Depth: 1 wrapper level (but with indirection) Characteristics:
Stores payload address in function pointer before calling
The call through pointer may appear differently in some analysis tools
Demonstrates indirect execution patterns
6.5 Path E: Recursive (Variable 1-3 Levels)
Stack Depth: Variable (2-4 wrapper levels) Characteristics:
First source of internal variability
Same path selection can produce different depths
Recursion creates multiple instances of the same function on the stack
Stack shows repeated
wrapper_path_E_recursiveentries
6.6 Path H: Tower (5 Levels)
Stack Depth: 6 wrapper levels Call Stack:
Characteristics:
Deep linear path with unique function at each level
Different auxiliary operation per level creates rich behavioral pattern
Significantly different stack signature from shallow paths
6.7 Path I: Deep (6 Levels)
Stack Depth: 7 wrapper levels Characteristics:
Deepest linear path (excluding variable paths)
Inline API calls rather than auxiliary function calls
Memory is allocated and held across the nested call
6.8 Path J: Function Pointer Chain (5 Links)
Stack Depth: 6 wrapper levels Characteristics:
Each level calls the next through a function pointer
Combines depth with indirection
Local function pointer variables add to stack frame complexity
6.9 Path K: Mixed Recursion + Nesting (4-7 Levels)
Stack Depth: Variable (4-7 wrapper levels) Characteristics:
Combines two patterns: recursion followed by fixed nesting
Random recursion depth (1-4) plus 2 fixed nested levels
Creates 4 distinct possible stack depths
Most complex variable-depth path
6.10 Path L: Staircase (7 Levels) - System Info Path
Stack Depth: 8 wrapper levels (deepest fixed path) API Calls:
1
GetCurrentThreadId, GetCurrentProcessId
2
GetSystemTimeAsFileTime
3
GetSystemInfo
4
GlobalMemoryStatusEx
5
GetComputerNameA
6
GetEnvironmentVariableA
7
(none - final)
Characteristics:
Deepest fixed-depth path
Uses diverse system information APIs
Creates a rich behavioral profile alongside deep stack
6.11 Path M: Branching (5 Levels, 4 Routes)
Stack Depth: 6 wrapper levels (fixed) Possible Routes:
Characteristics:
Most complex path structure
Two independent branch points create 4 possible stack signatures
Same depth, different function sequences
Particularly effective against signature-based detection
7. Stack Trace Capture Mechanism
Implementation
Key Components
RtlCaptureStackBackTrace: Windows API that captures return addresses from the stackParameter 1: Frames to skip (0 = start from current)
Parameter 2: Maximum frames to capture
Parameter 3: Output array for addresses
Returns: Number of frames captured
SymInitialize/SymFromAddr/SymCleanup: Debug Help Library functionsInitialize symbol handler for the process
Resolve addresses to function names
Requires
dbghelp.liband debug symbols for full resolution
Example Output
8. Potential Improvements
8.1 Cryptographically Secure Randomness
Current:
Improved:
Benefit: Unpredictable selection even if execution time is known.
8.2 Dynamic Wrapper Generation
Instead of static wrapper functions, generate them at runtime:
Benefit: Infinite path variations without code bloat.
8.3 JIT Wrapper Compilation
For advanced scenarios, generate wrapper code in executable memory:
Benefit: Stack shows addresses in non-module memory, harder to attribute.
8.4 Timing-Based Path Selection
Add temporal variation:
Benefit: Even with fixed RNG seed, paths vary based on execution timing.
8.5 More Auxiliary Diversity
Add more system API calls to auxiliary functions:
Benefit: More diverse behavioral patterns, harder to correlate.
8.6 Anti-Analysis Timing
Add variable delays to frustrate dynamic analysis:
8.7 Path Weight Configuration
Allow unequal probability distribution:
Benefit: Favor deeper paths to maximize stack signature variation.
9. Detection Strategies
Understanding how to detect this technique is crucial for EDR developers.
9.1 Behavioral Clustering
Instead of exact stack matching, cluster similar behaviors:
9.2 Statistical Analysis
Monitor for unusual stack depth variance:
Detection heuristic: Flag processes where the same final API is reached via significantly different stack depths across multiple observations.
9.3 Return Address Module Analysis
9.4 Function Name Pattern Detection
The wrapper function names follow patterns:
Detection: Flag modules containing multiple functions matching wrapper-like naming patterns.
9.5 Call Graph Analysis
Build a call graph and identify:
Multiple entry points to same sensitive function
Unusual fan-in patterns
Functions that only exist to call other functions
9.6 Entropy-Based Detection
Measure randomness in execution patterns:
10. Conclusion
Probabilistic Call Stacks represent an interesting approach to evading stack-based detection mechanisms. By introducing randomness at the execution flow level, attackers can generate unique signatures for each run while maintaining identical malicious behavior.
Key Takeaways
Effectiveness varies: This technique is most effective against signature-based detection that relies on exact stack matches. Behavioral analysis remains effective.
Complexity trade-off: More paths and deeper nesting increase evasion potential but also increase code size and complexity.
Not a silver bullet: This is one layer in a defense-evasion strategy, not a complete solution.
Detection is possible: Statistical analysis, behavioral clustering, and call graph analysis can identify this pattern.
For EDR Developers
Move beyond exact stack matching
Implement statistical analysis of stack variance
Focus on behavioral endpoints, not intermediate paths
Consider entropy-based anomaly detection
For Security Researchers
This technique demonstrates the cat-and-mouse nature of security
Understanding evasion techniques improves detection capabilities
The code provides a foundation for further research and testing
References
Windows API Documentation: RtlCaptureStackBackTrace
DbgHelp Library: Symbol Handler Functions
MSDN: Structured Exception Handling and Stack Unwinding
Academic Papers on Control Flow Integrity
Last updated