-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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
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"
);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(...)"
);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
Each CallChain contains an ordered list of CallChainNode objects:
| 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
|
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.dllMLVScan automatically identifies well-known entry points:
-
OnMelonInitialize,OnLateInitializeMelon -
OnApplicationStart,OnApplicationQuit -
OnSceneWasLoaded,OnSceneWasInitialized -
OnUpdate,OnLateUpdate,OnFixedUpdate OnGUI
-
Awake,Start,OnEnable,OnDisable -
Update,LateUpdate,FixedUpdate
- 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.
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");
}
}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
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.
- 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
- 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
- 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"
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);
}Find only deeply nested attack chains:
var deepChains = findings
.Where(f => f.HasCallChain && f.CallChain!.Nodes.Count >= 3)
.ToList();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();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).
- API Reference - Full API documentation
- Detection Rules - List of all detection rules
- Contributing - How to implement custom rules with call graph support