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-PoCarrow-up-right


Table of Contents

  1. Introduction

  2. Understanding Call Stack Fingerprinting

  3. The Probabilistic Approach

  4. Architecture Overview

  5. Implementation Deep Dive

  6. Path Analysis

  7. Stack Trace Capture Mechanism

  8. Potential Improvements

  9. Detection Strategies

  10. 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:

  1. Return Address Validation: Checking if return addresses point to legitimate code sections

  2. Module Attribution: Identifying which modules (DLLs, EXEs) appear in the stack

  3. Pattern Matching: Comparing the stack structure against known malicious patterns

  4. 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 MessageBoxA as a benign, visible indicator of execution

  • In 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:

  1. Add stack depth - More functions in the call chain

  2. Add behavioral noise - Different API calls create different patterns

API Diversity Benefits:

Function
Windows API Used
Purpose

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_WRAPPERS provides uniform distribution

  • The 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_recursive entries


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


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:

Level
API

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

  1. RtlCaptureStackBackTrace: Windows API that captures return addresses from the stack

    • Parameter 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

  2. SymInitialize / SymFromAddr / SymCleanup: Debug Help Library functions

    • Initialize symbol handler for the process

    • Resolve addresses to function names

    • Requires dbghelp.lib and 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

  1. Effectiveness varies: This technique is most effective against signature-based detection that relies on exact stack matches. Behavioral analysis remains effective.

  2. Complexity trade-off: More paths and deeper nesting increase evasion potential but also increase code size and complexity.

  3. Not a silver bullet: This is one layer in a defense-evasion strategy, not a complete solution.

  4. 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