Skip to content

The Ghidra project

The Ghidra project is the findings store. A discovery that lives only in a report is lost to the next session. This is a rule, not a preference, and the cost of ignoring it is measured in hours.

Where things are

Project firmware/iPodClassic.gpr, program osos-dec.dfu
Scripts firmware/ghidra_scripts_project/
C dump firmware/claudedump/c_current/
ASM dump firmware/claudedump/asm_current/

The two dumps use the same file naming, so c_current/220cfc74.c and asm_current/220cfc74.s are the same function. Ghidra ships prebuilt decompiler natives for Apple silicon, so nothing needs building.

Refresh the dump first

Not at the end of the session. First.

cd firmware && <ghidra>/support/analyzeHeadless . iPodClassic \
    -process osos-dec.dfu -noanalysis -scriptPath ghidra_scripts_project \
    -postScript DumpAllFunctionsC /tmp/c_fresh

DumpAllFunctionsAsm does the same for the disassembly.

The reason is unglamorous: the dump went stale at 192 named functions while the project had 457, and an hour went into re-deriving a function that was already named. Script output arrives on stderr, prefixed INFO <Script>.java>.

Read the ARM whenever a call's target matters

The decompiler renders an indirect call as a plain one, and that has cost three separate digs:

  • a display function's panel write is ldr ip,[r0,#0xc] ; blx ip through a descriptor slot;
  • a gate dispatches through two vtable slots;
  • a frame count comes from TABLE + 4 rather than header + 4, because r1 is reloaded midway.

In all three the C read as a settled answer and was hiding the question.

Findings go back in through a script

Apply what was learned with an Apply*Findings.java script, then re-dump, so the next read sees the new names. Do this at the point a finding is established -- not at the end of the thread. A whole session once went by with the first findings applied and none of the later ones, because the work got interesting.

Before disassembling anything that looks like infrastructure

Grep the project's symbols and the reports first. One address was independently "discovered" from scratch a second time, when it had already been named and already written up.

Two things worth knowing about this particular image

A name can be attached to the wrong function. getFunctionContaining will happily return an unrelated function when the address you want has no function defined at it. Check getFunctionAt first, or you leave a wrong name in the shared project -- which is worse than no name.

Semihosting is instrumented and then thrown away. RetailOS instruments roughly 131 call sites with SVC 0x123456 (ARM semihosting) and the retail handler is a stub -- mov r0,#0 ; bx lr, which discards all of it. Replacement firmware that implements that handler inherits the convention, and OpenOCD semihosting, on day one.