File size: 5,783 Bytes
ae4930d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# Ghidra headless post-script: export decompiled C + (optionally) locate patch sites.
# Runs inside Ghidra's Jython under analyzeHeadless. No machine-specific paths baked in.
#
# Usage (via analyzeHeadless):
#   -postScript exportDecomp.py <outdir> [<comma-separated file offsets, hex or dec>]
# Offsets may also be supplied via the GHIDRA_PATCH_OFFSETS env var (same format).
# Examples of offsets: "0x024F74,0x0E740E"  or  "155508,947726"
import os
from ghidra.app.decompiler import DecompInterface
from ghidra.util.task import ConsoleTaskMonitor

args = getScriptArgs()
outdir = args[0] if args and len(args) > 0 else os.path.join(os.getcwd(), "ghidra_out")
try:
    os.makedirs(outdir)
except:
    pass

def _parse_offsets(s):
    out = []
    if not s:
        return out
    for tok in s.replace(";", ",").split(","):
        tok = tok.strip()
        if not tok:
            continue
        try:
            out.append(int(tok, 16) if tok.lower().startswith("0x") else int(tok, 0))
        except:
            pass
    return out

# Patch-site file offsets: CLI arg 2 wins, else env var, else none.
offs_src = args[1] if args and len(args) > 1 else os.environ.get("GHIDRA_PATCH_OFFSETS", "")
TARGET_OFFSETS = _parse_offsets(offs_src)

prog = currentProgram
fm = prog.getFunctionManager()
mem = prog.getMemory()
imageBase = prog.getImageBase()

dec = DecompInterface()
dec.openProgram(prog)
monitor = ConsoleTaskMonitor()

# --- file offset -> virtual address (robust across Ghidra versions) ---
def off_to_addr(off):
    for b in mem.getBlocks():
        try:
            sis = b.getSourceInfos()
        except:
            sis = []
        for si in sis:
            fb = -1
            try:
                fb = si.getFileBytesOffset()
            except:
                try:
                    fb = si.getFileBytesOffsetFromVA()  # fallback name on some builds
                except:
                    fb = -1
            try:
                ln = si.getLength()
            except:
                ln = 0
            if fb is not None and fb != -1 and fb <= off < fb + ln:
                return b.getStart().add(off - fb)
    return None

# --- virtual address -> file offset (for authoring a patch from a decompiler VA) ---
def addr_to_off(addr):
    try:
        info = mem.getAddressSourceInfo(addr)
        if info is not None:
            fo = info.getFileOffset()
            if fo is not None and fo != -1:
                return fo
    except:
        pass
    return None

patch_report = []
target_funcs = set()
for off in TARGET_OFFSETS:
    addr = off_to_addr(off)
    if addr is None:
        patch_report.append("file 0x%06X -> (no VA mapping)" % off)
        continue
    f = fm.getFunctionContaining(addr)
    fname = f.getName() if f else "(no function)"
    fentry = f.getEntryPoint().toString() if f else "-"
    patch_report.append("file 0x%06X -> VA %s  in func %s @ %s" % (off, addr, fname, fentry))
    if f:
        target_funcs.add(f)

if TARGET_OFFSETS:
    with open(os.path.join(outdir, "patch_sites.txt"), "w") as fh:
        fh.write("ImageBase: %s\n" % imageBase)
        fh.write("\n".join(patch_report))
        fh.write("\n")

# --- decompile one function to C ---
def decompile(f):
    try:
        res = dec.decompileFunction(f, 60, monitor)
        if res and res.decompileCompleted():
            return res.getDecompiledFunction().getC()
    except Exception as e:
        return "// decompile error: %s\n" % str(e)
    return "// decompile failed\n"

# --- dump the target patch-site functions first (most important), with VA+file offset banner ---
if target_funcs:
    with open(os.path.join(outdir, "patch_functions.c"), "w") as fh:
        for f in target_funcs:
            entry = f.getEntryPoint()
            fo = addr_to_off(entry)
            fo_s = ("0x%X" % fo) if fo is not None else "?"
            fh.write("\n/* ===== FUNCTION %s @ VA %s (file %s) ===== */\n" % (f.getName(), entry, fo_s))
            fh.write(decompile(f))
            fh.write("\n")

# --- caller/callee counts (used to RANK targets: heavily-called helpers are
#     often the gate; mirrors auto-re-agent's function picker ranking). ---
def caller_count(f):
    try:
        return len(f.getCallingFunctions(monitor))
    except:
        return 0

def callee_count(f):
    try:
        return len(f.getCalledFunctions(monitor))
    except:
        return 0

# --- dump ALL functions + an index. functions.txt columns:
#     entry-VA <tab> name <tab> callers=<n> <tab> callees=<n> [<tab> THUNK]
# Also emit by_callers.txt: non-thunk functions sorted by caller count desc. ---
funcs = list(fm.getFunctions(True))
idx = open(os.path.join(outdir, "functions.txt"), "w")
allc = open(os.path.join(outdir, "all_decomp.c"), "w")
ranked = []
count = 0
for f in funcs:
    ep = f.getEntryPoint()
    nc = caller_count(f)
    if f.isThunk():
        idx.write("%s\t%s\tcallers=%d\tcallees=0\tTHUNK\n" % (ep, f.getName(), nc))
        continue
    cc = callee_count(f)
    idx.write("%s\t%s\tcallers=%d\tcallees=%d\n" % (ep, f.getName(), nc, cc))
    ranked.append((nc, cc, ep.toString(), f.getName()))
    allc.write("\n/* ===== %s @ %s  (callers=%d callees=%d) ===== */\n"
               % (f.getName(), ep, nc, cc))
    allc.write(decompile(f))
    count += 1
idx.close()
allc.close()

ranked.sort(reverse=True)  # by caller count desc, then callee count
with open(os.path.join(outdir, "by_callers.txt"), "w") as rh:
    rh.write("# callers\tcallees\tVA\tname  (rank targets from the top)\n")
    for nc, cc, va, nm in ranked:
        rh.write("%d\t%d\t%s\t%s\n" % (nc, cc, va, nm))

print("EXPORT DONE: %d functions decompiled -> %s" % (count, outdir))
print("  ranked candidates -> by_callers.txt")
for line in patch_report:
    print("  " + line)