Skip to content

Latest commit

 

History

History
425 lines (350 loc) · 19.8 KB

File metadata and controls

425 lines (350 loc) · 19.8 KB

ORPHANED_CODE_DISCOVERY_WORKFLOW

You are scanning Ghidra binaries for orphaned code — valid instructions that exist between known functions but were never marked as functions by auto-analysis. Apply all changes directly in Ghidra via MCP tools. Do not create or edit filesystem files.

Critical Rules

  1. Never auto-create without review: The scanner finds candidates. You classify and confirm each one before calling create_function.
  2. Iterative: After creating functions, re-run the scanner — new gaps appear as existing function boundaries shift.
  3. Minimal intervention: Create functions and set a triage plate comment. Full documentation is a separate task (use FUNCTION_DOC_WORKFLOW_V5.md later).
  4. Multi-binary safe: Always confirm program context before writing. Use get_current_program_info() to verify.

Step 0: Select Scope

Ask the user which scope to scan:

Scope Description How
Current binary Only the active program Run scanner once
Version folder All binaries in the same version folder as the current program list_project_files() to find sibling binaries, iterate with switch_program()
All project binaries Every binary in the Ghidra project list_project_files("/") recursively, iterate with switch_program()

For multi-binary scopes, process one binary at a time:

for each binary in scope:
    switch_program(binary_name)
    run scanner (Step 1)
    triage candidates (Step 2)
    create approved functions (Step 3)
    report (Step 4)

Step 1: Run Orphaned Code Scanner

Execute the scanner script via run_script_inline. This scans gaps between consecutive known functions in executable memory segments.

The scanner performs three passes per gap:

  • Pass 1: Check for already-disassembled instructions not in any function (highest confidence)
  • Pass 2: Check raw bytes for known function prologue patterns (variable confidence)
  • Pass 3: Fallback — any non-padding bytes with a RET that Pass 2 missed (lowest confidence, catches MOVSX/MOVZX/byte-MOV starts)
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.*;
import ghidra.program.model.address.*;
import java.util.*;

public class FindOrphanedCode extends GhidraScript {
    @Override
    public void run() throws Exception {
        FunctionManager fm = currentProgram.getFunctionManager();
        Memory mem = currentProgram.getMemory();
        Listing listing = currentProgram.getListing();

        println("=== Orphaned Code Scanner v2 ===");
        println("Program: " + currentProgram.getName());
        println("Known functions: " + fm.getFunctionCount());
        println("");

        List<Function> funcs = new ArrayList<>();
        FunctionIterator fi = fm.getFunctions(true);
        while (fi.hasNext()) funcs.add(fi.next());

        int candidateNum = 0;
        int gapsScanned = 0;
        int paddingGaps = 0;
        int pass3Hits = 0;

        for (int i = 0; i < funcs.size() - 1; i++) {
            Function cur = funcs.get(i);
            Function next = funcs.get(i + 1);

            Address gapStart = cur.getBody().getMaxAddress().add(1);
            Address gapEnd = next.getEntryPoint();

            if (!gapStart.getAddressSpace().equals(gapEnd.getAddressSpace())) continue;
            if (gapStart.compareTo(gapEnd) >= 0) continue;

            long gapSize = gapEnd.subtract(gapStart);
            if (gapSize < 3) continue;

            MemoryBlock block = mem.getBlock(gapStart);
            if (block == null || !block.isExecute()) continue;

            gapsScanned++;

            // Pass 1: Already-disassembled instructions not in any function
            Instruction instr = listing.getInstructionAt(gapStart);
            if (instr == null) instr = listing.getInstructionAfter(gapStart);

            boolean foundInPass1 = false;
            while (instr != null && instr.getAddress().compareTo(gapEnd) < 0) {
                String mnem = instr.getMnemonicString();
                if (mnem.equals("NOP") || mnem.equals("INT3") || mnem.equals("HLT") || mnem.equals("??")) {
                    instr = listing.getInstructionAfter(instr.getAddress());
                    continue;
                }

                candidateNum++;
                foundInPass1 = true;
                Address cAddr = instr.getAddress();
                StringBuilder preview = new StringBuilder();
                Instruction p = instr;
                int instrCount = 0;
                for (int j = 0; j < 4 && p != null && p.getAddress().compareTo(gapEnd) < 0; j++) {
                    if (j > 0) preview.append("; ");
                    preview.append(p.toString());
                    p = listing.getInstructionAfter(p.getAddress());
                    instrCount++;
                }
                while (p != null && p.getAddress().compareTo(gapEnd) < 0) {
                    instrCount++;
                    p = listing.getInstructionAfter(p.getAddress());
                }

                println(String.format("#%d | HIGH (already disassembled, %d instrs) | 0x%s | gap=%d | between: %s .. %s | %s",
                    candidateNum, instrCount, cAddr.toString(), gapSize,
                    cur.getName(), next.getName(), preview.toString()));
                break;
            }
            if (foundInPass1) continue;

            // Pass 2: Raw byte prologue pattern scan
            int readSize = (int)Math.min(gapSize, 4096);
            byte[] bytes = new byte[readSize];
            mem.getBytes(gapStart, bytes);

            boolean allPad = true;
            for (byte b : bytes) {
                if (b != (byte)0xCC && b != (byte)0x90 && b != 0x00) { allPad = false; break; }
            }
            if (allPad) { paddingGaps++; continue; }

            int off = 0;
            while (off < readSize && isPad(bytes[off])) off++;
            if (off >= readSize) { paddingGaps++; continue; }

            String prologue = identifyPrologue(bytes, off, readSize);

            int retPos = -1;
            for (int r = off; r < readSize; r++) {
                if (bytes[r] == (byte)0xC3 || bytes[r] == (byte)0xC2) { retPos = r; break; }
            }
            if (retPos < 0) continue;

            int estSize = retPos - off + 1;

            // Pass 2 hit: known prologue
            if (prologue != null) {
                String confidence = getConfidence(prologue, estSize);
                if (confidence == null) continue;

                candidateNum++;
                Address addr = gapStart.add(off);
                StringBuilder hex = new StringBuilder();
                for (int h = off; h < Math.min(off + 20, readSize); h++) {
                    hex.append(String.format("%02X ", bytes[h]));
                }

                println(String.format("#%d | %s (undisassembled) | 0x%s | ~%d bytes | prologue: %s | between: %s .. %s | %s",
                    candidateNum, confidence, addr.toString(), estSize,
                    prologue, cur.getName(), next.getName(),
                    hex.toString().trim()));
                continue;
            }

            // Pass 3: Unknown prologue fallback — non-padding bytes with RET
            if (estSize >= 5) {
                pass3Hits++;
                candidateNum++;
                Address addr = gapStart.add(off);
                StringBuilder hex = new StringBuilder();
                for (int h = off; h < Math.min(off + 24, readSize); h++) {
                    hex.append(String.format("%02X ", bytes[h]));
                }

                // Count potential sub-functions: how many C3/C2 (RET) bytes in the gap
                int retCount = 0;
                for (int r = off; r < readSize; r++) {
                    if (bytes[r] == (byte)0xC3 || bytes[r] == (byte)0xC2) retCount++;
                }
                String multi = retCount > 1 ? " | MULTI-RET(" + retCount + ")" : "";

                println(String.format("#%d | REVIEW (unknown prologue, pass 3) | 0x%s | ~%d bytes | gap=%d | first: 0x%02X | between: %s .. %s%s | %s",
                    candidateNum, addr.toString(), estSize, gapSize,
                    bytes[off] & 0xFF, cur.getName(), next.getName(),
                    multi, hex.toString().trim()));
            }
        }

        println("");
        println(String.format("=== DONE: %d candidates | %d gaps scanned | %d pure padding | %d pass-3 (review) ===",
            candidateNum, gapsScanned, paddingGaps, pass3Hits));
    }

    private boolean isPad(byte b) {
        return b == (byte)0xCC || b == (byte)0x90;
    }

    private String identifyPrologue(byte[] b, int off, int len) {
        // Multi-byte patterns (check first)
        if (match(b, off, len, 0x8B, 0xFF, 0x55, 0x8B, 0xEC)) return "HOTPATCH+FRAME";
        if (match(b, off, len, 0x55, 0x8B, 0xEC)) return "PUSH_EBP+FRAME";
        if (match(b, off, len, 0x83, 0xEC)) return "SUB_ESP_IMM8";
        if (match(b, off, len, 0x81, 0xEC)) return "SUB_ESP_IMM32";

        // Two-byte 0x0F prefix instructions (MOVSX, MOVZX, conditional)
        if (match(b, off, len, 0x0F, 0xBE)) return "MOVSX_R32_R8";
        if (match(b, off, len, 0x0F, 0xBF)) return "MOVSX_R32_R16";
        if (match(b, off, len, 0x0F, 0xB6)) return "MOVZX_R32_R8";
        if (match(b, off, len, 0x0F, 0xB7)) return "MOVZX_R32_R16";

        if (off < len) {
            int fb = b[off] & 0xFF;
            // Standard frame setup
            if (fb == 0x55) return "PUSH_EBP";
            // Callee-save registers
            if (fb == 0x56) return "PUSH_ESI";
            if (fb == 0x57) return "PUSH_EDI";
            if (fb == 0x53) return "PUSH_EBX";
            if (fb == 0x51) return "PUSH_ECX";
            // Push immediate
            if (fb == 0x6A) return "PUSH_IMM8";
            if (fb == 0x68) return "PUSH_IMM32";
            // MOV patterns
            if (fb == 0xB8) return "MOV_EAX_IMM32";
            if (fb == 0xA1) return "MOV_EAX_MEM";
            if (fb == 0x8B) return "MOV_R32_RM32";
            if (fb == 0x8A) return "MOV_R8_RM8";
            if (fb == 0x89) return "MOV_RM32_R32";
            if (fb == 0x88) return "MOV_RM8_R8";
            // Register zeroing / comparison
            if (fb == 0x33) return "XOR_R32_RM32";
            if (fb == 0x31) return "XOR_RM32_R32";
            if (fb == 0x3B) return "CMP_R32_RM32";
            if (fb == 0x85) return "TEST_R32_R32";
            if (fb == 0xF6) return "TEST_RM8_IMM8";
            if (fb == 0x80) return "ALU_RM8_IMM8";
            // Control flow
            if (fb == 0xE8) return "CALL_REL32";
            if (fb == 0xE9) return "JMP_REL32";
        }
        return null;
    }

    private boolean match(byte[] data, int off, int limit, int... pat) {
        if (off + pat.length > limit) return false;
        for (int i = 0; i < pat.length; i++) {
            if ((data[off + i] & 0xFF) != (pat[i] & 0xFF)) return false;
        }
        return true;
    }

    private String getConfidence(String prologue, int size) {
        if (size < 2) return null;
        if (prologue.contains("FRAME") || prologue.equals("HOTPATCH+FRAME")) return "HIGH";
        if (prologue.startsWith("SUB_ESP") && size >= 5) return "MEDIUM";
        if (prologue.startsWith("PUSH_E") && size >= 4) return "MEDIUM";
        if (prologue.equals("PUSH_ECX") && size >= 4) return "MEDIUM";
        if (prologue.startsWith("MOVSX") || prologue.startsWith("MOVZX")) return "MEDIUM";
        if (prologue.equals("MOV_R8_RM8") && size >= 5) return "MEDIUM";
        if (prologue.equals("XOR_R32_RM32") && size >= 4) return "MEDIUM";
        if (prologue.equals("TEST_R32_R32") && size >= 4) return "MEDIUM";
        if (size >= 5) return "LOW";
        return null;
    }
}

Step 2: Triage Candidates

Classify each candidate into one of these types based on scanner output and quick inspection:

Type A: Import Thunk / Trampoline

  • Pattern: Single JMP instruction (5 bytes), gap = 5
  • Confidence: HIGH (disassembled, 1 instr)
  • Action: create_function → set plate comment: "Import thunk — redirects to <target>"
  • Priority: Low (mechanical redirect, no logic to document)

Type B: Already Disassembled — Real Function

  • Pattern: HIGH (already disassembled, N instrs) where N > 1
  • Confidence: Very high — Ghidra analyzed the code but never created a function boundary
  • Action: create_function → quick decompile to classify → set triage plate comment
  • Priority: Highest (these are missed functions with real logic)
  • Verify: Decompile after creation to confirm it's coherent code, not a jump table fragment

Type C: Undisassembled — Standard Prologue (HIGH)

  • Pattern: PUSH_EBP+FRAME or HOTPATCH+FRAME in raw bytes
  • Confidence: High — standard x86 function prologue
  • Action: create_function (will auto-disassemble) → decompile → set triage plate comment
  • Priority: High (almost certainly a real function)

Type D: Undisassembled — Callee-Save / Operand Prologue (MEDIUM)

  • Pattern: PUSH_ESI, PUSH_EDI, PUSH_EBX, PUSH_ECX, SUB_ESP, MOVSX_*, MOVZX_*, MOV_R8_RM8, XOR_R32_RM32, TEST_R32_R32
  • Confidence: Medium — common but not unique to function starts
  • Action: create_function → decompile → verify the code makes sense as a standalone function
  • Priority: Medium
  • Watch for: Tail-call targets (code reachable only via JMP from another function — should NOT be a separate function)
  • Common sub-patterns:
    • MOVSX EAX, CL; MOV AL, [EAX+table]; AND EAX, mask; RET — CRT character classification (MBCS _ismbblead, _ismbbkana, etc.)
    • MOV AL, [ECX]; PUSH ESI; MOVSX ESI, AL; ... — CRT string/character utility
    • XOR EAX, EAX; ... — register zeroing before conditional logic

Type E: Undisassembled — Atypical Start (LOW)

  • Pattern: MOV_R32_RM32, MOV_EAX_MEM, PUSH_IMM8, CALL_REL32, MOV_RM32_R32, MOV_RM8_R8, CMP_R32_RM32, ALU_RM8_IMM8, etc.
  • Confidence: Low — could be data, jump table entries, or code fragments
  • Action: Inspect first. Use inspect_memory_content or disassemble_bytes to preview. Only create if the disassembly forms a coherent function.
  • Priority: Low
  • Red flags:
    • Bytes that look like address tables (consecutive 4-byte aligned values near the program's image base)
    • Very short sequences (< 8 bytes) that could be data padding
    • JMP_REL32 alone (likely a thunk, but verify target exists)

Type G: Undisassembled — Unknown Prologue (REVIEW)

  • Pattern: Pass 3 hit — non-padding bytes with a RET instruction, but first byte doesn't match any known prologue pattern
  • Confidence: Requires manual review — the scanner cannot classify the start bytes
  • Action: Always inspect first with disassemble_bytes or inspect_memory_content. Check MULTI-RET(N) flag — if N > 1, the gap likely contains multiple adjacent functions
  • Priority: Low (but can contain real functions that use unusual instruction sequences)
  • Common causes:
    • Compiler-specific optimizations (computed jumps, table-driven dispatchers)
    • Functions starting with rare opcodes (e.g., ENTER, LEA, XCHG, BSR/BSF)
    • Inline data followed by real code (the "function" starts mid-gap, not at the first non-padding byte)
  • Red flags: Same as Type E — verify it's not a data table before creating

Type F: Getter / Converter / Thin Wrapper

  • Pattern: Any type, but estimated size < 15 bytes
  • Sub-patterns:
    • MOV EAX,[addr]; RET (global getter, ~6 bytes)
    • MOV EAX,[ESP+4]; <op>; RET (single-arg converter, ~8-10 bytes)
    • PUSH imm; CALL rel32; RET (thin wrapper, ~8-12 bytes)
  • Action: create_function → set descriptive name if purpose is obvious from the instruction pattern
  • Priority: Medium (small but can be important accessors)

Step 3: Create Functions (Batch)

Process candidates in order: Type B first, then C, D, F, A, E, G.

For each approved candidate:

1. create_function(address)
2. decompile_function(address) — quick sanity check
3. set_comment(type='plate') with triage metadata:

Triage plate comment format (plain text):

[TRIAGE] Orphaned code discovered by scanner.
Type: <A|B|C|D|E|F|G> — <type description>
Confidence: <HIGH|MEDIUM|LOW>
Size: ~N bytes / N instructions
Neighboring: <prev_function> .. <next_function>
Status: Awaiting full documentation (FUNCTION_DOC_WORKFLOW_V5)

For Type A thunks, use a shorter comment:

Import thunk — redirects to <target_function_name> at <target_address>

Batch creation pattern

Process in batches of 5-10 candidates. After each batch:

  • Verify no creation errors
  • Spot-check 1-2 decompilations for sanity
  • Continue to next batch

Do NOT attempt full V5 documentation during this workflow. The goal is function boundary creation and triage classification only.

Step 4: Report

After processing all candidates for a binary, output:

=== ORPHANED CODE REPORT: <program_name> ===
Created: N functions
  Type A (thunks): N
  Type B (disassembled): N
  Type C (standard prologue): N
  Type D (callee-save prologue): N
  Type E (atypical): N
  Type F (getter/wrapper): N
  Type G (unknown prologue): N
Skipped: N (reason: data/jump table/fragment)
Needs review: N (ambiguous candidates)
Next: Re-run scanner to check for newly exposed gaps

Multi-binary summary

When processing multiple binaries, output a final summary:

=== PROJECT SCAN COMPLETE ===
Binaries scanned: N
Total functions created: N
  <binary1>: N created, N skipped
  <binary2>: N created, N skipped
  ...

Step 5: Iterative Re-scan (Optional)

Creating functions changes gap boundaries. A second scanner pass may reveal:

  • New candidates previously hidden inside larger gaps
  • Gaps that were too small before but now contain visible code

Re-run the scanner once after batch creation. If new candidates appear, triage and create. Typically 2 passes are sufficient.

Edge Cases and Warnings

  • Non-contiguous function bodies: A function with body [0x100-0x110, 0x130-0x140] creates a "gap" at 0x111-0x12F that may contain code belonging to that same function. Check xrefs before creating a new function in such gaps.
  • Shared epilogues: Some compilers share POP; RET sequences between functions. Creating a function at the shared block causes overlap errors. If create_function fails with an overlap error, skip that candidate.
  • Switch/case tables: Data tables embedded in .text that look like valid instructions when disassembled. Verify with get_xrefs_to — if only referenced from a single function's switch dispatch, it's a case block, not a separate function.
  • Linker padding: CC CC CC (INT3) between functions is normal debug padding. The scanner already filters this. 00 00 padding is also filtered but could occasionally mask real code starting with ADD [EAX], AL (0x00 0x00) — very unlikely to be intentional code.
  • Code after unconditional JMP: Sometimes Ghidra stops analysis after a JMP, leaving subsequent code undefined. The function starting after the JMP may be legitimate.
  • MULTI-RET gaps: Pass 3 flags gaps containing multiple RET instructions (MULTI-RET(N)). These gaps typically contain N adjacent small functions packed together. After creating the first function, re-scan — the gap will split and the remaining functions become separate candidates. Common with CRT utility clusters (character classification, string helpers).

Output

SCAN COMPLETE: <program_name>
Created: N functions (Type breakdown)
Ready for documentation via FUNCTION_DOC_WORKFLOW_V5