Identifying main() and Key Execution Paths

25 minIn Progress

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:

  1. Initializes the C runtime library (heap, I/O, locale)
  2. Sets up structured exception handling
  3. Calls __security_init_cookie (stack canary initialization)
  4. Retrieves command-line arguments
  5. Then calls the actual main() (or WinMain for 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:

  1. Navigate to entry by double-clicking it in the Symbol Tree
  2. Read the decompiler output -- the CRT init function calls several setup functions
  3. Look for the pattern: multiple setup calls followed by one "real" call that receives arguments
  4. The call to main() is typically the last significant CALL before ExitProcess or 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:

  1. Open Window > Defined Strings (or press Ctrl+Shift+S for string search)
  2. Find a known string (a URL, mutex name, file path seen in sandbox output)
  3. Double-click to navigate to its data location
  4. Press X to find which function references the string
  5. 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:

ValueConstantWhen It FiresMalware Relevance
1DLL_PROCESS_ATTACHDLL is first loaded into a processMost common -- malware initializes here
0DLL_PROCESS_DETACHDLL is unloadedCleanup, anti-forensics
2DLL_THREAD_ATTACHA new thread is created in the host processRarely used by malware
3DLL_THREAD_DETACHA thread exitsRarely 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 > Exports for 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:

PriorityFunction CategoryWhy It MattersTime Investment
CriticalC2 communicationReveals infrastructure (domains, IPs, ports) for blocking30-40% of analysis time
CriticalConfiguration decryptionExtracts IOCs (C2 addresses, mutex names, keys)15-20%
HighPersistence mechanismsDocuments how to find and remove the infection10-15%
MediumData collection/exfiltrationDetermines the impact and what data was compromised10-15%
MediumAnti-analysis checksHelps understand evasion tactics and analyst counter-measures5-10%
LowInitialization / cleanupUsually standard setup code with little intelligence value5% or skip

Step 3: Understand the C2 Loop

The operational loop is where the core malicious behavior lives. Focus here:

  1. Beacon -- the malware sends a check-in to the C2 server (often HTTP POST with system info)
  2. Receive -- the C2 responds with a command (often an integer command ID)
  3. Dispatch -- a switch/if-else routes the command ID to handler functions
  4. Execute -- the handler performs the action (download, upload, screenshot, keylog, etc.)
  5. Report -- results are sent back to the C2
  6. 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

  1. Open a malware sample (or PMA Lab binary) in Ghidra
  2. Navigate to entry and trace through the CRT initialization
  3. Identify main() using at least two of the methods described above
  4. Rename it to main (press L)
  5. List every function called from main() and assign each a category: init, anti-analysis, persistence, C2, collection, or cleanup
  6. Open the Function Call Graph for main() and identify the orchestrator vs. utility functions
  7. Prioritize the top 3 functions for deeper analysis based on the triage framework
Try it in the shell
Practise this lesson's tooling on its sample in an emulated analyst shell. Output is pre-recorded — nothing executes.

Suggested triage steps

  1. 1

    Start from the imports

    FindResourceA and LoadResource point straight at the interesting path — this binary's real payload is in its resources.

  2. 2

    Follow the dropped path

    The file the resource is written to, which is the next link in the execution chain.

  3. 3

    Find the execution call

    WinExec is the end of the path you have been tracing.

  4. 4

    Check your reading against capa

    Drop-and-execute, recovered independently of your own trace.

analyst@lab:~emulated · nothing executes

MAA analyst shell — emulated. Nothing executes.

Type 'help', or click a step on the left.

$