This commit is contained in:
Nathan Rashleigh committed 2026-06-08 23:22:16 +10:00
1 parent 6a6553077b
commit f71a2a3512
23 files changed
+7773 -68

No files matched your search

+54
View File
@@ -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
+85
View File
@@ -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");
}
}
+44
View File
@@ -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");
}
}
+78
View File
@@ -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");
}
}
+96
View File
@@ -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");
}
}