Skip to main content

Overview

xAnalyzer’s function tracking feature enables the plugin to predict and identify indirect function calls that don’t directly name their target. By tracking MOV instructions that manipulate function pointers, xAnalyzer can resolve calls to registers and dynamic pointers, providing full API information even for obfuscated calling patterns.
This feature is controlled by the “Track Undefined Functions” option in the configuration menu. When enabled, it dramatically improves analysis accuracy for code using indirect calls.

What Are Indirect Calls?

Indirect calls don’t specify the target function directly in the instruction:
call kernel32.CreateFileW      ; Target is explicit

How Function Tracking Works

xAnalyzer maintains a vector of MOV instructions that manipulate potential function pointers:
1

MOV Instruction Detection

During analysis, xAnalyzer identifies MOV instructions that could involve function pointers:
if (conf.track_undef_functions && IsMovFunctPointer(&bii, CurrentAddress)) {
    INSTRUCTIONSTACK mov_inst = {0};
    GuiGetDisassembly(CurrentAddress, szDisasmText);
    
    strcpy_s(mov_inst.Instruction, bii.instruction);
    strcpy_s(mov_inst.GuiInstruction, szDisasmText);
    DecomposeMovInstruction(mov_inst.GuiInstruction, 
                           mov_inst.DestinationRegister, 
                           mov_inst.SourceRegister);
    vectorFunctPointer.insert(vectorFunctPointer.begin(), mov_inst);
}
2

Pointer Vector Storage

Valid MOV instructions are stored in a vector (limited to 100 entries):
vector<INSTRUCTIONSTACK> vectorFunctPointer;  // Global stack
#define INSTRUCTION_FUNCT_POINTER_MAXSIZE 100
3

Indirect Call Resolution

When a CALL to a register or pointer is encountered, xAnalyzer searches the vector:
bool FindFunctionFromPointer(char *szDisasmText, char *szFunctionName) {
    duint index = 0;
    while (!found && index < vectorFunctPointer.size()) {
        INSTRUCTIONSTACK inst = vectorFunctPointer.at(index);
        if (strcmp(inst.DestinationRegister, addr) == 0) {
            if (strstr(inst.SourceRegister, "<") != nullptr) {
                string funct = StripFunctionNamePointer(inst.SourceRegister);
                szAPIFunction = funct;
                found = true;
            }
        }
        index++;
    }
    return found;
}
4

API Definition Lookup

Once the function name is resolved, normal API analysis proceeds:
if (conf.track_undef_functions && 
    FindFunctionFromPointer(szDisasmText, szFunctionName) && 
    SearchApiFileForDefinition(szFunctionName, szAPIDefinition, true)) {
    if (SetFunctionParams(&ai, szFunctionName))
        SetAutoCommentIfCommentIsEmpty(&inst, szLabelAPIFunction, ...);
}

Tracked Call Patterns

xAnalyzer can resolve various indirect calling conventions:
; Store function pointer in register
mov eax, user32.MessageBoxA
push 0
push offset caption
push offset text
push 0
call eax                        ; ← Resolved to MessageBoxA

; xAnalyzer comments:
; HANDLE hWnd = NULL
; LPCTSTR lpText
; LPCTSTR lpCaption
; UINT uType = MB_OK
; MessageBoxA

MOV Instruction Filtering

Not all MOV instructions are tracked. xAnalyzer filters based on:

Exclusion Criteria

bool IsMovFunctPointer(const BASIC_INSTRUCTION_INFO *bii, duint CurrentAddress)
{
    bool isMov = strstr(bii->instruction, "mov") != nullptr;
    
    // Exclude:
    // 1. Non-MOV instructions
    // 2. Function prologues/epilogues
    // 3. Stack frame setup instructions
    
    return (isMov && 
            !IsProlog(bii, CurrentAddress) && 
            !IsEpilog(bii));
}

✅ Tracked

  • mov eax, kernel32.CreateFileW
  • mov ecx, [00403000]
  • mov edx, [eax+4]
  • mov ebx, offset sub_401000

❌ Ignored

  • push ebp (prolog)
  • mov ebp, esp (prolog)
  • mov esp, ebp (epilog)
  • pop ebp (epilog)

Configuration

Function tracking is controlled by a configuration option:

Enabling/Disabling

In x64dbg:
  1. Navigate to Plugins → xAnalyzer → Options
  2. Check/uncheck “Track Undefined Functions”
  3. The setting is saved to the configuration file
Function tracking is enabled by default as of xAnalyzer 2.5.0. This feature was introduced to improve analysis of modern code that frequently uses indirect calls.

Data Structures

The tracking system uses these structures:
typedef struct stINSTRUCTIONSTACK {
    duint Address;                              // Instruction address
    char Instruction[GUI_MAX_DISASSEMBLY_SIZE]; // Raw instruction
    char GuiInstruction[GUI_MAX_DISASSEMBLY_SIZE]; // Formatted display
    char DestinationRegister[GUI_MAX_DISASSEMBLY_SIZE]; // mov dest
    char SourceRegister[GUI_MAX_DISASSEMBLY_SIZE];      // mov source
} INSTRUCTIONSTACK;

// Global tracking vector
vector<INSTRUCTIONSTACK> vectorFunctPointer;

// Maximum tracked instructions
#define INSTRUCTION_FUNCT_POINTER_MAXSIZE 100

Performance Considerations

Vector Size Limiting

To prevent memory issues, the tracking vector is limited:
if (vectorFunctPointer.size() < INSTRUCTION_FUNCT_POINTER_MAXSIZE) {
    vectorFunctPointer.insert(vectorFunctPointer.begin(), mov_inst);
} else {
    ClearVector(vectorFunctPointer);  // Reset when full
}

Clearing Strategy

The vector is cleared at function boundaries:
// Clear at function end or epilog
if (!selectionAnal && (prolog || epilog)) {
    ClearStack(stackInstructions);
    ClearVector(vectorFunctPointer);
}
This ensures:
  • Memory usage stays bounded
  • Function pointers don’t leak across function boundaries
  • Analysis remains accurate per-function

Example: Resolving Complex Calls

Consider this obfuscated code:
; Function pointer manipulation
00401000:  mov ebx, kernel32.GetModuleHandleA
00401006:  xor eax, eax
00401008:  push eax
0040100A:  call ebx                ; ← Indirect call

; Without tracking:
; call ebx

; With function tracking enabled:
; push NULL                         ; LPCSTR lpModuleName = NULL
; call ebx                          ; GetModuleHandleA
xAnalyzer:
  1. Detects mov ebx, kernel32.GetModuleHandleA at 401000
  2. Stores vector entry: Address 401000, DestinationRegister “ebx”, SourceRegister “kernel32.GetModuleHandleA”
  3. When analyzing call ebx at 40100A, searches vector for “ebx”
  4. Finds the MOV instruction, extracts “GetModuleHandleA”
  5. Looks up API definition and applies parameters

Benefits for Reverse Engineering

Obfuscation Resistance

Defeats simple obfuscation that uses indirect calls to hide API usage

Dynamic Loading Analysis

Track runtime-resolved functions loaded with GetProcAddress

Object-Oriented Code

Resolve virtual function calls in C++ code with vtables

Callback Analysis

Understand callback mechanisms and function pointer patterns

Malware Analysis Use Case

Many malware samples use indirect calls to evade detection:
1

API Hiding

; Store critical APIs in registers
mov esi, kernel32.VirtualAlloc
mov edi, kernel32.CreateThread
; ... benign code ...
call esi  ; ← Allocate memory (tracked)
call edi  ; ← Create thread (tracked)
2

Import Table Obfuscation

; Resolve imports at runtime
mov eax, [IAT_VirtualProtect]
; ... setup arguments ...
call eax  ; ← Change memory protections (tracked)
3

Polymorphic Decryption

; Function pointer changes at runtime
mov ecx, decrypt_routine_1
test al, al
jz skip
mov ecx, decrypt_routine_2
skip:
call ecx  ; ← Decrypt data (tracked to possible routines)
With function tracking, xAnalyzer reveals the true behavior despite obfuscation.

Implementation History

This feature was introduced in xAnalyzer 2.5.0 (Version History from README):
xAnalyzer 2.5.0
- Added function smart tracking feature (Smart prediction and recognition of indirect function calls like: CALL , CALL )
- Added name of function pointers as parameters (the entire function name, if detected, will be used instead of just the address)

Source Code References

  • Function pointer tracking: xanalyzer.cpp:474-488
  • Indirect call resolution: xanalyzer.cpp:401-407, 2612-2652
  • MOV instruction filtering: xanalyzer.cpp:1406-1422
  • Configuration: xanalyzer.cpp:2692, 2708
  • Data structures: xanalyzer.h:35-41
  • Vector management: xanalyzer.cpp:42, 512-513, 551

Known Limitations

Function tracking has some constraints:
  • Limited to 100 stored MOV instructions per function
  • Cannot track pointer arithmetic that modifies addresses
  • Register aliasing may cause missed detections
  • Dynamic runtime modifications won’t be tracked
  • Cross-function pointer tracking is not supported

Performance Impact

Function tracking adds minimal overhead:
  • Memory: ~100 instructions × ~1KB per entry = ~100KB max
  • CPU: Vector search is O(n) with n ≤ 100
  • Analysis time: Typically less than 5% increase
The benefits far outweigh the cost for most reverse engineering tasks.

Build docs developers (and LLMs) love