ghidra
This commit is contained in:
1 parent
6a6553077b
commit
f71a2a3512
23 files changed
+7773
-68
No files matched your search
@@ -0,0 +1,54 @@
|
||||
# re/ — rekordbox `export.pdb` reverse-engineering
|
||||
|
||||
Shared record of the Ghidra/Frida work behind maple's pdb writer. See the
|
||||
top-level `README.md` for the high-level findings; this dir holds the raw
|
||||
artifacts so others can reproduce and extend the analysis.
|
||||
|
||||
## What's here
|
||||
|
||||
`scripts/` — Ghidra headless scripts (run against an analyzed `rekordbox.exe`):
|
||||
- `FindPdb.java` — locate the writer via string + magic-constant xrefs
|
||||
- `DecPdb.java` — decompile an explicit list of addresses
|
||||
- `CallTree.java` — walk a call tree, flag file-I/O / 4096-using functions
|
||||
- `FindHash.java` — hunt the per-track hash: decompile `formatForSearch` + callers + content-string xrefs
|
||||
|
||||
`findings/` — script output (decompiled C + xref maps):
|
||||
- `findpdb.txt` — string xrefs → the export functions; magic-constant hit counts
|
||||
- `decpdb.txt` — decompiled `export.pdb` serializer call stack (DeviceSQL Page Manager)
|
||||
- `calltree.txt` — call tree under the export orchestrator `FUN_14119d2d0`
|
||||
- `hash.txt` — `formatForSearch` + callers + `djmdContent`/`ContentID` xrefs
|
||||
|
||||
## Reproduce (not committed: the binary + Ghidra project)
|
||||
|
||||
`rekordbox.exe` and the analyzed Ghidra project are **not** in the repo
|
||||
(copyright + size). To rebuild from your own rekordbox install (Windows path
|
||||
visible from WSL):
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=~/jdk/jdk-21.0.11+10 # Ghidra needs JDK 21
|
||||
GH=/path/to/ghidra_12.1.2_PUBLIC
|
||||
# one-time: import + analyze (~1h, 10G heap, 8 cores)
|
||||
GHIDRA_HEADLESS_MAXMEM=10G "$GH/support/analyzeHeadless" ~/ghidra_proj rbx \
|
||||
-import "/mnt/c/Program Files/rekordbox/rekordbox 7.0.5/rekordbox.exe" \
|
||||
-overwrite -max-cpu 8 -analysisTimeoutPerFile 7200
|
||||
# then run any script without re-analyzing:
|
||||
"$GH/support/analyzeHeadless" ~/ghidra_proj rbx -process rekordbox.exe -noanalysis \
|
||||
-scriptPath re/scripts -postScript FindPdb.java
|
||||
```
|
||||
|
||||
Runtime→Ghidra address map: `static = 0x140000000 + module_offset` (Ghidra image
|
||||
base `0x140000000`; ASLR runtime base varies — read `rekordbox.exe+0xOFFSET` from
|
||||
the Frida agent, never hardcode it).
|
||||
|
||||
## Live capture (Frida, Windows)
|
||||
|
||||
`../tools/capture.mjs` (Node, `npm i frida`) + `../tools/rbx_pdb_capture.js`
|
||||
(agent). Child-gates so the CIG-protected writer is hooked at spawn:
|
||||
`node tools/capture.mjs --spawn`.
|
||||
|
||||
## Open questions
|
||||
|
||||
- per-track `0x14` (28-bit) — varies per track, not a plain crc32; likely a
|
||||
stored `djmdContent` value. `0x18`/`0x1a` are a per-export constant.
|
||||
- `index_shift` (row `0x02`) — positional (`~0x20`/row, resets per page) but the
|
||||
exact formula isn't pinned. maple writes `0`; rekordbox doesn't.
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,85 @@
|
||||
// Walk the call tree down from the export orchestrator and surface the page
|
||||
// serializer: flag functions that call file-I/O APIs or use the 4096 page size,
|
||||
// then decompile those candidates. Output -> ~/ghidra_proj/calltree.txt
|
||||
import java.io.PrintWriter;
|
||||
import java.util.*;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.decompiler.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.scalar.Scalar;
|
||||
|
||||
public class CallTree extends GhidraScript {
|
||||
static final String ROOT = "0x14119d2d0";
|
||||
static final int MAX_DEPTH = 4;
|
||||
|
||||
boolean hasPageConst(Function f) {
|
||||
InstructionIterator it = currentProgram.getListing().getInstructions(f.getBody(), true);
|
||||
while (it.hasNext()) {
|
||||
Instruction insn = it.next();
|
||||
for (int op = 0; op < insn.getNumOperands(); op++)
|
||||
for (Object o : insn.getOpObjects(op))
|
||||
if (o instanceof Scalar) {
|
||||
long v = ((Scalar) o).getUnsignedValue();
|
||||
if (v == 0x1000L || v == 4096L) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String ioCalls(Function f) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Function c : f.getCalledFunctions(monitor)) {
|
||||
String n = c.getName().toLowerCase();
|
||||
if (n.contains("write") || n.contains("createfile") || n.contains("fopen")
|
||||
|| n.contains("fwrite") || n.contains("mapviewof") || n.contains("setfilepointer")
|
||||
|| n.contains("flushview") || n.contains("fclose"))
|
||||
sb.append(c.getName()).append(" ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
PrintWriter out = new PrintWriter("/home/ntr/ghidra_proj/calltree.txt");
|
||||
FunctionManager fm = currentProgram.getFunctionManager();
|
||||
Function root = fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(ROOT));
|
||||
|
||||
Map<Function, Integer> depth = new LinkedHashMap<>();
|
||||
Deque<Function> q = new ArrayDeque<>();
|
||||
depth.put(root, 0); q.add(root);
|
||||
while (!q.isEmpty() && !monitor.isCancelled()) {
|
||||
Function f = q.poll();
|
||||
int d = depth.get(f);
|
||||
if (d >= MAX_DEPTH) continue;
|
||||
for (Function c : f.getCalledFunctions(monitor))
|
||||
if (!depth.containsKey(c)) { depth.put(c, d + 1); q.add(c); }
|
||||
}
|
||||
out.println("call tree from " + root.getName() + " depth<=" + MAX_DEPTH + ": " + depth.size() + " funcs\n");
|
||||
|
||||
List<Function> candidates = new ArrayList<>();
|
||||
for (Map.Entry<Function, Integer> e : depth.entrySet()) {
|
||||
Function f = e.getKey();
|
||||
boolean page = hasPageConst(f);
|
||||
String io = ioCalls(f);
|
||||
String flags = (page ? "[4096]" : "") + (io.isEmpty() ? "" : "[io:" + io.trim() + "]");
|
||||
out.println(String.format("d%d %-12s size=%-6d %s %s",
|
||||
e.getValue(), f.getEntryPoint(), f.getBody().getNumAddresses(), f.getName(), flags));
|
||||
if (page || !io.isEmpty()) candidates.add(f);
|
||||
}
|
||||
|
||||
DecompInterface di = new DecompInterface();
|
||||
di.openProgram(currentProgram);
|
||||
out.println("\n===== CANDIDATE DECOMPILES (" + candidates.size() + ") =====");
|
||||
for (Function f : candidates) {
|
||||
if (monitor.isCancelled()) break;
|
||||
out.println("\n//===== " + f.getName() + " @ " + f.getEntryPoint() + " =====");
|
||||
DecompileResults r = di.decompileFunction(f, 60, monitor);
|
||||
if (r != null && r.decompileCompleted())
|
||||
out.println(r.getDecompiledFunction().getC());
|
||||
else
|
||||
out.println("// decompile failed");
|
||||
}
|
||||
out.flush(); out.close();
|
||||
println("wrote calltree.txt: " + depth.size() + " funcs, " + candidates.size() + " candidates");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Decompile the pdb export functions found via string xrefs.
|
||||
import java.io.PrintWriter;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.decompiler.*;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
|
||||
public class DecPdb extends GhidraScript {
|
||||
// export.pdb serializer call stack from a live Frida WriteFile backtrace.
|
||||
// module+offset -> Ghidra static = 0x140000000 + offset (base confirmed).
|
||||
static final String[] ADDRS = {
|
||||
"0x1420e883e", // calls WriteFile
|
||||
"0x1420d73a6",
|
||||
"0x1420d6e92",
|
||||
"0x1420d6368",
|
||||
"0x1420eae07",
|
||||
"0x1420e0852",
|
||||
"0x1420de1c4",
|
||||
"0x141d75d21", // export.pdb-specific frame
|
||||
"0x141cad188",
|
||||
"0x141cac585",
|
||||
"0x141b8ba75",
|
||||
"0x141acaf8c",
|
||||
"0x1423c9b3b",
|
||||
};
|
||||
@Override public void run() throws Exception {
|
||||
PrintWriter out = new PrintWriter("/home/ntr/ghidra_proj/decpdb.txt");
|
||||
DecompInterface di = new DecompInterface();
|
||||
di.openProgram(currentProgram);
|
||||
for (String a : ADDRS) {
|
||||
Address addr = currentProgram.getAddressFactory().getAddress(a);
|
||||
Function f = currentProgram.getFunctionManager().getFunctionContaining(addr);
|
||||
if (f == null) { out.println("// no function at " + a); continue; }
|
||||
out.println("\n//================ " + f.getName() + " @ " + f.getEntryPoint() + " ================");
|
||||
DecompileResults r = di.decompileFunction(f, 90, monitor);
|
||||
if (r != null && r.decompileCompleted())
|
||||
out.println(r.getDecompiledFunction().getC());
|
||||
else
|
||||
out.println("// decompile failed: " + (r != null ? r.getErrorMessage() : "null"));
|
||||
}
|
||||
out.flush(); out.close();
|
||||
println("wrote decpdb.txt");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Hunt for the track-row builder / the per-track 8-byte hash (row offsets
|
||||
// 0x14 u4, 0x18 u2, 0x1a u2) and index_shift (0x02). Decompiles the string-hash
|
||||
// function formatForSearch + its callers, and dumps xrefs of track/content/
|
||||
// search-related strings. Output -> ~/ghidra_proj/hash.txt
|
||||
import java.io.PrintWriter;
|
||||
import java.util.*;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.decompiler.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.symbol.*;
|
||||
|
||||
public class FindHash extends GhidraScript {
|
||||
PrintWriter out;
|
||||
DecompInterface di;
|
||||
Set<String> done = new HashSet<>();
|
||||
FunctionManager fm;
|
||||
ReferenceManager rm;
|
||||
|
||||
void dec(Function f, String why) {
|
||||
if (f == null) return;
|
||||
if (!done.add(f.getEntryPoint().toString())) return;
|
||||
out.println("\n//===== " + f.getName() + " @ " + f.getEntryPoint() + " [" + why + "] =====");
|
||||
DecompileResults r = di.decompileFunction(f, 60, monitor);
|
||||
out.println(r != null && r.decompileCompleted() ? r.getDecompiledFunction().getC() : "// decompile failed");
|
||||
}
|
||||
|
||||
Function fn(String a) {
|
||||
return fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(a));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
out = new PrintWriter("/home/ntr/ghidra_proj/hash.txt");
|
||||
di = new DecompInterface(); di.openProgram(currentProgram);
|
||||
fm = currentProgram.getFunctionManager();
|
||||
rm = currentProgram.getReferenceManager();
|
||||
|
||||
// 1. formatForSearch (string hash) + its callers
|
||||
Function fmt = fn("0x141a1afc0");
|
||||
dec(fmt, "formatForSearch");
|
||||
if (fmt != null) {
|
||||
int n = 0;
|
||||
for (Reference ref : rm.getReferencesTo(fmt.getEntryPoint())) {
|
||||
if (n++ >= 10) break;
|
||||
dec(fm.getFunctionContaining(ref.getFromAddress()), "caller of formatForSearch");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. strings that hint at the track/content row builder -> xref functions
|
||||
String[] kw = {"djmdContent", "SearchStr", "formatForSearch", "AnalysisDataPath",
|
||||
"ContentID", "rb_data_status", "rb_local", "dsqlInsert", "AddTrack"};
|
||||
DataIterator dit = currentProgram.getListing().getDefinedData(true);
|
||||
int shown = 0;
|
||||
while (dit.hasNext() && !monitor.isCancelled()) {
|
||||
Data d = dit.next();
|
||||
if (d == null || !d.hasStringValue()) continue;
|
||||
Object v = d.getValue();
|
||||
if (!(v instanceof String)) continue;
|
||||
String s = (String) v;
|
||||
for (String k : kw) {
|
||||
if (s.contains(k)) {
|
||||
out.println("\n// STRING @" + d.getAddress() + " : " + s);
|
||||
int n = 0;
|
||||
for (Reference ref : rm.getReferencesTo(d.getAddress())) {
|
||||
if (n++ >= 5) break;
|
||||
Function c = fm.getFunctionContaining(ref.getFromAddress());
|
||||
out.println("// xref in " + (c != null ? c.getName() + " @" + c.getEntryPoint() : ref.getFromAddress()));
|
||||
}
|
||||
shown++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shown >= 30) break;
|
||||
}
|
||||
out.flush(); out.close();
|
||||
println("wrote hash.txt");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Locate the rekordbox .pdb serializer via known strings + magic-number constants.
|
||||
// Run headless: analyzeHeadless <proj> rbx -process rekordbox.exe -noanalysis \
|
||||
// -scriptPath ~/ghidra_scripts -postScript FindPdb.java
|
||||
// Output goes to ~/ghidra_proj/findpdb.txt (and the log).
|
||||
import java.io.PrintWriter;
|
||||
import java.util.*;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.scalar.Scalar;
|
||||
import ghidra.program.model.symbol.*;
|
||||
|
||||
public class FindPdb extends GhidraScript {
|
||||
|
||||
// Strings that mark the export code paths.
|
||||
static final String[] KEYWORDS = {
|
||||
"export.pdb", "exportExt.pdb", "USBANLZ", "PIONEER",
|
||||
"ANLZ", "HOT CUE BANK", "ORIGINAL ARTIST", "rekordbox/export"
|
||||
};
|
||||
|
||||
// Magic numbers maple currently guesses (various widths/combinations).
|
||||
static final long[] MAGICS = {
|
||||
42343L, 54995L, // 0xA567 / 0xD6D3 per-track markers
|
||||
0xD6D3A567L, 0xA567D6D3L, // combined 32-bit forms
|
||||
788224L, // 0x000C0700 track bitmask
|
||||
1004L, // 0x03EC "strange page" marker
|
||||
0x64L, 0x44L, 0x24L, // page-flag bytes
|
||||
19048L, 30967L // the schema author's original guesses
|
||||
};
|
||||
|
||||
PrintWriter out;
|
||||
|
||||
void emit(String s) { println(s); if (out != null) out.println(s); }
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
out = new PrintWriter("/home/ntr/ghidra_proj/findpdb.txt");
|
||||
Listing listing = currentProgram.getListing();
|
||||
ReferenceManager rm = currentProgram.getReferenceManager();
|
||||
FunctionManager fm = currentProgram.getFunctionManager();
|
||||
|
||||
emit("===== STRING XREFS =====");
|
||||
DataIterator di = listing.getDefinedData(true);
|
||||
while (di.hasNext() && !monitor.isCancelled()) {
|
||||
Data d = di.next();
|
||||
if (d == null || !d.hasStringValue()) continue;
|
||||
Object val = d.getValue();
|
||||
if (!(val instanceof String)) continue;
|
||||
String s = (String) val;
|
||||
for (String kw : KEYWORDS) {
|
||||
if (s.contains(kw)) {
|
||||
emit("\nSTRING @ " + d.getAddress() + " : " + s.replace("\n", "\\n"));
|
||||
ReferenceIterator ri = rm.getReferencesTo(d.getAddress());
|
||||
int n = 0;
|
||||
while (ri.hasNext() && n++ < 25) {
|
||||
Reference r = ri.next();
|
||||
Function f = fm.getFunctionContaining(r.getFromAddress());
|
||||
emit(" xref from " + r.getFromAddress()
|
||||
+ (f != null ? " in " + f.getName() + " @ " + f.getEntryPoint() : " (no func)"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit("\n===== CONSTANT XREFS =====");
|
||||
Set<Long> targets = new HashSet<>();
|
||||
for (long m : MAGICS) targets.add(m);
|
||||
Map<Long, Integer> counts = new TreeMap<>();
|
||||
InstructionIterator ii = listing.getInstructions(true);
|
||||
while (ii.hasNext() && !monitor.isCancelled()) {
|
||||
Instruction insn = ii.next();
|
||||
for (int op = 0; op < insn.getNumOperands(); op++) {
|
||||
for (Object o : insn.getOpObjects(op)) {
|
||||
if (o instanceof Scalar) {
|
||||
long v = ((Scalar) o).getUnsignedValue();
|
||||
if (targets.contains(v)) {
|
||||
Function f = fm.getFunctionContaining(insn.getAddress());
|
||||
emit(String.format("CONST 0x%X (%d) @ %s %s in %s", v, v,
|
||||
insn.getAddress(), insn.toString(),
|
||||
f != null ? f.getName() + " @ " + f.getEntryPoint() : "(no func)"));
|
||||
counts.merge(v, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emit("\n===== CONSTANT HIT COUNTS =====");
|
||||
for (Map.Entry<Long, Integer> e : counts.entrySet())
|
||||
emit(String.format("0x%X (%d): %d hits", e.getKey(), e.getKey(), e.getValue()));
|
||||
|
||||
out.flush();
|
||||
out.close();
|
||||
emit("\nwrote /home/ntr/ghidra_proj/findpdb.txt");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user