Skip to content

Call Graph Analysis

ifBars edited this page Jan 22, 2026 · 1 revision

Call Graph Analysis

MLVScan.Core includes sophisticated call graph analysis that tracks how suspicious methods are invoked throughout your assembly. Instead of reporting individual findings for every suspicious pattern, the scanner consolidates related findings into call chains that show the complete attack path.

Why Call Chains Matter

Traditional static analysis tools report each suspicious pattern individually:

  • ❌ "Found P/Invoke to shell32.dll at line 42"
  • ❌ "Found call to suspicious method at line 100"
  • ❌ "Found entry point at line 5"

This creates noise and makes it hard to understand the actual threat. MLVScan consolidates these into a single finding:

"Detected high-risk DllImport of shell32.dll with suspicious function ShellExecuteEx - Hidden in Southwards.ShellExecuteEx, invoked from: OnInitializeMelon"

With full call chain visibility:

[ENTRY] NoMoreTrash.NoMoreTrashMod.OnInitializeMelon:150
        Entry point calls ShellExecuteEx
    → [DECL] Southwards.ShellExecuteEx
    →         P/Invoke declaration imports ShellExecuteEx from shell32.dll

How It Works

1. Declaration Registration

When the scanner encounters a suspicious method declaration (like a P/Invoke), it registers it with the CallGraphBuilder:

callGraphBuilder.RegisterSuspiciousDeclaration(
    method: methodDef,
    triggeringRule: rule,
    codeSnippet: "extern static void ShellExecuteA(...)",
    description: "P/Invoke declaration for shell32.dll"
);

2. Call Site Tracking

As the scanner analyzes method bodies, it tracks calls to suspicious methods:

callGraphBuilder.RegisterCallSite(
    callerMethod: currentMethod,
    calledMethod: suspiciousMethod,
    instructionOffset: 42,
    codeSnippet: "IL_0042: call void Southwards::ShellExecuteA(...)"
);

3. Chain Consolidation

After scanning the entire assembly, BuildCallChainFindings() consolidates all related findings:

var chainFindings = callGraphBuilder.BuildCallChainFindings();

This produces a single ScanFinding with:

  • Location: The entry point where the attack is initiated
  • Description: Concise summary including all callers
  • CallChain: Full attack path with code snippets at each step
  • Severity: Inherited from the triggering rule

Call Chain Structure

Each CallChain contains an ordered list of CallChainNode objects:

Node Types

Type Description Example
EntryPoint The mod's entry point (OnMelonInitialize, Start, etc.) OnMelonInitialize calling your wrapper method
IntermediateCall A call in the middle of the chain Your wrapper calling a helper method
SuspiciousDeclaration The dangerous method itself P/Invoke to shell32.dll

Example Chain

var chain = finding.CallChain;

Console.WriteLine(chain.Summary);
// "Detected high-risk DllImport of shell32.dll with suspicious function ShellExecuteEx - 
//  Hidden in Southwards.ShellExecuteEx, invoked from: OnInitializeMelon"

foreach (var node in chain.Nodes)
{
    Console.WriteLine(node);
}
// Output:
// [ENTRY] NoMoreTrash.NoMoreTrashMod.OnInitializeMelon:150: Entry point calls ShellExecuteEx
// [DECL] Southwards.ShellExecuteEx: P/Invoke declaration imports ShellExecuteEx from shell32.dll

Entry Point Detection

MLVScan automatically identifies well-known entry points:

MelonLoader Entry Points

  • OnMelonInitialize, OnLateInitializeMelon
  • OnApplicationStart, OnApplicationQuit
  • OnSceneWasLoaded, OnSceneWasInitialized
  • OnUpdate, OnLateUpdate, OnFixedUpdate
  • OnGUI

Unity MonoBehaviour Entry Points

  • Awake, Start, OnEnable, OnDisable
  • Update, LateUpdate, FixedUpdate

Other Entry Points

  • Static constructors (.cctor)
  • Any method marked as an entry point by your custom rules

When a call originates from one of these methods, it's marked as EntryPoint in the call chain, making it immediately clear where the malicious flow begins.

Accessing Call Chain Data

Check if a Finding has a Call Chain

var findings = scanner.Scan("mod.dll");

foreach (var finding in findings)
{
    if (finding.HasCallChain)
    {
        Console.WriteLine($"Found consolidated finding with {finding.CallChain!.Nodes.Count} nodes");
    }
}

Get Detailed Description

if (finding.HasCallChain)
{
    Console.WriteLine(finding.CallChain!.ToDetailedDescription());
}

Output:

Detected high-risk DllImport of shell32.dll with suspicious function ShellExecuteEx - Hidden in Southwards.ShellExecuteEx, invoked from: OnInitializeMelon

Call chain:
[ENTRY] NoMoreTrash.NoMoreTrashMod.OnInitializeMelon:150: Entry point calls ShellExecuteEx
  -> [DECL] Southwards.ShellExecuteEx: P/Invoke declaration imports ShellExecuteEx from shell32.dll

Get Combined Code Snippets

if (finding.HasCallChain)
{
    var snippet = finding.CallChain!.ToCombinedCodeSnippet();
    Console.WriteLine(snippet);
}

This combines all code snippets from each node in the chain, giving you a complete view of the IL instructions involved in the attack.

Benefits

For Users

  • Reduced Noise: One consolidated finding instead of 3-5 separate findings
  • Clear Threat Context: See exactly how the malicious code is invoked
  • Better Decision Making: Understand if it's dead code, reflection-invoked, or directly called from entry points

For Tool Developers

  • Better UX: Display attack paths visually in your UI
  • Accurate Reporting: Report the actual entry point location, not the P/Invoke declaration
  • Easier Triage: Group related findings together automatically

For Mod Developers

  • Clearer Guidance: Understand the full scope of the flagged pattern
  • Faster Fixes: See exactly which call sites need attention
  • Dead Code Detection: If a finding has no callers, it's marked as "may be dead code"

Advanced Usage

Custom Entry Points

If you're building a scanner for a custom mod framework, you can extend the entry point detection in CallGraphBuilder:

private bool IsLikelyEntryPoint(MethodDefinition method)
{
    var name = method.Name;
    
    // Your custom framework's entry points
    if (name.StartsWith("OnMyFramework") || name == "MyFrameworkInit")
        return true;
        
    // Fall back to default detection
    return base.IsLikelyEntryPoint(method);
}

Filtering by Chain Depth

Find only deeply nested attack chains:

var deepChains = findings
    .Where(f => f.HasCallChain && f.CallChain!.Nodes.Count >= 3)
    .ToList();

Filtering by Entry Point

Find attacks that start in specific entry points:

var onInitFindings = findings
    .Where(f => f.HasCallChain && 
                f.CallChain!.Nodes.Any(n => 
                    n.NodeType == CallChainNodeType.EntryPoint && 
                    n.Location.Contains("OnInitializeMelon")))
    .ToList();

Performance Considerations

Call graph analysis adds minimal overhead:

  • Registration: O(1) for each declaration and call site
  • Consolidation: O(n) where n is the number of call sites
  • Memory: Stores only method keys and locations, not full method bodies

For typical mods (< 10,000 methods), the overhead is negligible (< 10ms).

See Also