Identifying main() and Key Execution Paths
The Entry Point Is Not main()
One of the most common beginner mistakes is assuming that the PE entry point (entry in Ghidra) is the main() function. It is not. The entry point is the C Runtime (CRT) initialization code that the compiler inserts automatically. This CRT startup code performs housekeeping before your actual program logic runs:
- Initializes the C runtime library (heap, I/O, locale)
- Sets up structured exception handling
- Calls
__security_init_cookie(stack canary initialization) - Retrieves command-line arguments
- Then calls the actual
main()(orWinMainfor GUI applications)
Understanding this distinction is critical because analyzing CRT code is a waste of time -- you need to quickly identify main() and focus your effort there.
Finding main() in Ghidra
Method 1: Follow the CRT Call Chain
This is the most reliable method:
- Navigate to
entryby double-clicking it in the Symbol Tree - Read the decompiler output -- the CRT init function calls several setup functions
- Look for the pattern: multiple setup calls followed by one "real" call that receives arguments
- The call to
main()is typically the last significant CALL beforeExitProcessor the function returns
In the decompiler, the CRT entry often looks like:
void entry(void) {
__set_app_type(1);
__security_init_cookie();
__tmainCRTStartup(); // <-- Follow this call
}
Inside __tmainCRTStartup, you will eventually find:
retval = main(argc, argv, envp); // <-- This is your target
exit(retval);
Method 2: Look for Argument Setup (Assembly Level)
In the Listing view, the CRT pushes standard parameters before calling main():
PUSH envp ; environment pointer
PUSH argv ; argument array
PUSH argc ; argument count
CALL FUN_00401000 ; <-- This is main()
ADD ESP, 0xC ; clean up 3 parameters (cdecl)
PUSH EAX
CALL exit
The key indicator: three PUSHes followed by a CALL, then the return value is passed to exit().
Method 3: String Cross-References
If you know the malware uses specific strings (from prior behavioral analysis), use those as anchors:
- Open Window > Defined Strings (or press
Ctrl+Shift+Sfor string search) - Find a known string (a URL, mutex name, file path seen in sandbox output)
- Double-click to navigate to its data location
- Press
Xto find which function references the string - That function is part of the core logic -- trace its callers upward to find
main()
Analyst Tip: The Defined Strings window is one of the most powerful starting points for malware analysis. Suspicious strings like URLs, IP addresses, registry paths, or command strings immediately reveal which functions contain interesting behavior.
Method 4: Function Signature Recognition
main() has a well-known signature. Look for functions matching:
int main(int argc, char **argv, char **envp)
In Ghidra, this appears as a function with 3 parameters where:
- First parameter is an integer (argument count)
- Second and third parameters are pointers (argument and environment arrays)
DLL Entry Points and Exported Functions
DllMain
For DLL malware, the entry point is DllMain:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
The fdwReason parameter determines when code executes:
| Value | Constant | When It Fires | Malware Relevance |
|---|---|---|---|
| 1 | DLL_PROCESS_ATTACH | DLL is first loaded into a process | Most common -- malware initializes here |
| 0 | DLL_PROCESS_DETACH | DLL is unloaded | Cleanup, anti-forensics |
| 2 | DLL_THREAD_ATTACH | A new thread is created in the host process | Rarely used by malware |
| 3 | DLL_THREAD_DETACH | A thread exits | Rarely used by malware |
In Ghidra, look for a switch or if-else on the second parameter:
BOOL DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
if (reason == 1) { // DLL_PROCESS_ATTACH
start_malware_thread(); // <-- Malicious initialization
}
return TRUE;
}
Exported Functions
DLL malware often exposes functionality through exports rather than (or in addition to) DllMain:
- Check
Symbol Tree > Exportsfor exported function names - Malware DLLs commonly export functions with names like
ServiceMain,Install,Start, or ordinal-only exports - These can be invoked by
rundll32.exe:rundll32.exe malware.dll,ExportedFunc
Use XRefs on exports to understand what each exported function does -- these are the true entry points for DLL-based malware.
Mapping Execution Paths
Once you locate main() (or DllMain / exported functions), the next task is to map the program's overall execution flow.
Step 1: Identify the Execution Phases
Most malware follows a predictable lifecycle:
int main(int argc, char **argv) {
// Phase 1: Initialization
initialize_globals();
decrypt_configuration();
// Phase 2: Environment checks (anti-analysis)
if (is_debugger_present() || is_virtual_machine()) {
ExitProcess(0);
}
// Phase 3: Persistence installation
install_registry_run_key();
// Phase 4: Main operational loop
while (TRUE) {
beacon_to_c2();
command = receive_command();
dispatch_command(command);
Sleep(beacon_interval);
}
}
Step 2: Prioritize Analysis Effort
Not all code paths deserve equal attention. Triage based on intelligence value:
| Priority | Function Category | Why It Matters | Time Investment |
|---|---|---|---|
| Critical | C2 communication | Reveals infrastructure (domains, IPs, ports) for blocking | 30-40% of analysis time |
| Critical | Configuration decryption | Extracts IOCs (C2 addresses, mutex names, keys) | 15-20% |
| High | Persistence mechanisms | Documents how to find and remove the infection | 10-15% |
| Medium | Data collection/exfiltration | Determines the impact and what data was compromised | 10-15% |
| Medium | Anti-analysis checks | Helps understand evasion tactics and analyst counter-measures | 5-10% |
| Low | Initialization / cleanup | Usually standard setup code with little intelligence value | 5% or skip |
Step 3: Understand the C2 Loop
The operational loop is where the core malicious behavior lives. Focus here:
- Beacon -- the malware sends a check-in to the C2 server (often HTTP POST with system info)
- Receive -- the C2 responds with a command (often an integer command ID)
- Dispatch -- a switch/if-else routes the command ID to handler functions
- Execute -- the handler performs the action (download, upload, screenshot, keylog, etc.)
- Report -- results are sent back to the C2
- Sleep -- the malware waits before the next beacon cycle
Using the Function Call Graph
Ghidra's Window > Function Call Graph provides a visual map of calling relationships:
- Hub nodes (many outgoing edges) are orchestrator functions like
main()or command dispatchers - Popular nodes (many incoming edges) are utility functions: decryption, string operations, send/receive
- Leaf nodes (no outgoing calls to user functions) are the actual capability implementations
- Isolated nodes (no callers) may be dead code, or functions triggered only by specific C2 commands
The Function Call Trees window (Window > Function Call Trees) shows a text-based tree of:
- Incoming Calls -- who calls this function?
- Outgoing Calls -- what does this function call?
This is faster than the graph view for tracing a specific function's context.
Practical Exercise
- Open a malware sample (or PMA Lab binary) in Ghidra
- Navigate to
entryand trace through the CRT initialization - Identify
main()using at least two of the methods described above - Rename it to
main(pressL) - List every function called from
main()and assign each a category: init, anti-analysis, persistence, C2, collection, or cleanup - Open the Function Call Graph for
main()and identify the orchestrator vs. utility functions - Prioritize the top 3 functions for deeper analysis based on the triage framework
