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 ipthrough a descriptor slot; - a gate dispatches through two vtable slots;
- a frame count comes from
TABLE + 4rather thanheader + 4, becauser1is 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.