mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-01 21:50:16 +02:00
- 0x0011a_cb (Operation Dark Vector): nation-state CTF redesign with an AES-128-ECB sealed target and a plaintext launch origin; RP2350 firmware with bearing-driven servo, tri-color LEDs, GSV stats, and a realistic no-fix path - docs: story-driven classified brief, GDB and Ghidra tutorials with deep step-throughs, regenerated artifacts and PDFs - scripts: docstring standard, AES per-student randomizer, telemetry monitor - week 3 to week 5 lessons: Ghidra patching tutorial, CMSIS-SVD hardware RE, double floating-point and GPIO architecture chapters, README structure
526 lines
18 KiB
Markdown
526 lines
18 KiB
Markdown
# Ghidra Reversing Tutorial: Static Analysis of the Stripped RP2350 Image
|
|
|
|
## 0. Cold Open
|
|
|
|
Open the image and it will lie to you with total confidence.
|
|
|
|
That is not a bug. That is the tool doing exactly what it was built to do:
|
|
propose. Ghidra proposes functions. It proposes boundaries. It proposes names
|
|
for things it cannot possibly know. And when it is wrong it will not tell you,
|
|
because it cannot tell the difference between a guess and a fact any better
|
|
than the machine that answered nine seconds too fast.
|
|
|
|
A stripped binary is a wall of Thumb-2 with the labels torn off. The only way
|
|
through is patience, cross-references, and the refusal to accept a story just
|
|
because it is plausible. Ghidra is the map. It is not the territory.
|
|
|
|
So you draw the map, then you walk the ground. You separate the two coordinate
|
|
pairs by what *references* them, not by what they look like. You find the eight
|
|
bytes that decide where a machine goes, and you prove which eight they are.
|
|
|
|
The tool proposes. The bench disposes.
|
|
|
|
**Think, then verify.**
|
|
|
|
---
|
|
|
|
## 1. Executive Summary
|
|
|
|
This tutorial reverses the **stripped** `0x0011a_cb.bin` with Ghidra: no
|
|
symbols, no sections, no metadata. We import the raw image, recover the code
|
|
graph, correct the boundaries Ghidra gets wrong, find the hardcoded target
|
|
waypoint, and patch it. Every output below is from the raw image at base
|
|
`0x10000000`, language `ARM:LE:32:Cortex`.
|
|
|
|
---
|
|
|
|
## 2. Import the Raw Image
|
|
|
|
The `.bin` is a raw XIP image. You must tell Ghidra where it lives and how to
|
|
decode it.
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| GHIDRA IMPORT SETTINGS |
|
|
+-----------------------------------------------------------------+
|
|
| Language : ARM:LE:32:Cortex (ARMv8-M / Cortex-M33, Thumb-2) |
|
|
| Format : Raw Binary |
|
|
| Base : 0x10000000 (RP2350 external flash / XIP) |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### 2.1 Headless (reproducible), per host
|
|
|
|
macOS:
|
|
|
|
```bash
|
|
GHIDRA=/Applications/ghidra_12.0.4_PUBLIC
|
|
"$GHIDRA/support/analyzeHeadless" /tmp/ghproj DarkVector \
|
|
-import 0x0011a_cb.bin \
|
|
-processor "ARM:LE:32:Cortex" \
|
|
-loader BinaryLoader -loader-baseAddr 0x10000000
|
|
```
|
|
|
|
Linux:
|
|
|
|
```bash
|
|
GHIDRA=$HOME/ghidra_12.0.4_PUBLIC
|
|
"$GHIDRA/support/analyzeHeadless" /tmp/ghproj DarkVector \
|
|
-import 0x0011a_cb.bin \
|
|
-processor "ARM:LE:32:Cortex" \
|
|
-loader BinaryLoader -loader-baseAddr 0x10000000
|
|
```
|
|
|
|
Windows PowerShell:
|
|
|
|
```powershell
|
|
$GHIDRA = "C:\ghidra_12.0.4_PUBLIC"
|
|
& "$GHIDRA\support\analyzeHeadless.bat" "$env:TEMP\ghproj" DarkVector `
|
|
-import 0x0011a_cb.bin `
|
|
-processor "ARM:LE:32:Cortex" `
|
|
-loader BinaryLoader -loader-baseAddr 0x10000000
|
|
```
|
|
|
|
### 2.2 GUI, per host
|
|
|
|
Launch Ghidra, then **File -> Import File**, set language and base address, and
|
|
analyze:
|
|
|
|
- macOS: `/Applications/ghidra_12.0.4_PUBLIC/ghidraRun`
|
|
- Linux: `$HOME/ghidra_12.0.4_PUBLIC/ghidraRun`
|
|
- Windows: `C:\ghidra_12.0.4_PUBLIC\ghidraRun.bat`
|
|
|
|
---
|
|
|
|
## 3. Entry and the Stripped-Binary Problem
|
|
|
|
The first two words are the ARMv8-M vector table:
|
|
|
|
```
|
|
0x10000000: 0x20082000 ; initial SP
|
|
0x10000004: 0x1000015D ; reset handler (Thumb)
|
|
```
|
|
|
|
Ghidra recovers **166 functions** from the call graph. But with no symbols it
|
|
gets some boundaries wrong, and it folds tail-call-only helpers into their
|
|
callers: `release_payload` is reached only by a `b.w` tail call from
|
|
`navigate_to_target` at `0x10000D18`, so Ghidra does not give it its own
|
|
function. It names the entry `FUN_10000234`. The disassembly is right; the
|
|
boundaries are not. Correcting them is the analyst's job, and only the bench
|
|
confirms them.
|
|
|
|
Applying correct boundaries yields:
|
|
|
|
| Address | Function |
|
|
|---|---|
|
|
| `0x10000234` | `main` |
|
|
| `0x10000300` | `init_gps_pio` |
|
|
| `0x1000040C` | `poll_gps` |
|
|
| `0x10000858` | `gps_get_stats` |
|
|
| `0x10000870` | `init_lora` |
|
|
| `0x10000A08` | `lora_send` |
|
|
| `0x10000A54` | `lora_tick` |
|
|
| `0x10000AC4` | `init_propeller` |
|
|
| `0x10000AF8` | `propeller_set_bearing` |
|
|
| `0x10000B34` | `propeller_stop` |
|
|
| `0x10000B64` | `init_payload` |
|
|
| `0x10000BB0` | `set_gnss_leds` |
|
|
| `0x10000BFC` | `release_payload` |
|
|
| `0x10000C2C` | `init_navigation` |
|
|
| `0x10000C7C` | `send_telemetry` |
|
|
| `0x10000CB8` | `navigate_to_target` |
|
|
| `0x10000E5C` | `aes128_ecb_decrypt_block` |
|
|
| `0x100012D4` | `init_lcd` |
|
|
| `0x1000175C` | `lcd_show_coords` |
|
|
| `0x10001B4C` | `lcd_show_gnss` |
|
|
|
|
---
|
|
|
|
## 4. `main` as Ghidra Sees It (Raw Image)
|
|
|
|
```
|
|
void main(void)
|
|
{
|
|
local_18 = *DAT_100002f4; // ORIGIN_LAT @ 0x1000A098
|
|
uStack_14 = DAT_100002f4[1];
|
|
local_10 = *DAT_100002f8; // ORIGIN_LON @ 0x1000A090
|
|
uStack_c = DAT_100002f8[1];
|
|
FUN_10005d98(); // stdio_init_all
|
|
FUN_10000c2c(); // init_navigation
|
|
FUN_10000b64(); // init_payload
|
|
FUN_10000870(); // init_lora
|
|
FUN_10000300(); // init_gps_pio
|
|
FUN_10000ac4(); // init_propeller
|
|
FUN_100012d4(); // init_lcd
|
|
piVar1 = DAT_100002fc; // &hold @ 0x200010F0
|
|
do {
|
|
while( true ) {
|
|
iVar3 = 200;
|
|
bVar4 = 0;
|
|
local_20 = 0;
|
|
uStack_1c = 0;
|
|
do {
|
|
bVar2 = FUN_1000040c(&local_18,&local_10); // poll_gps
|
|
bVar4 = bVar2 | bVar4;
|
|
FUN_10000a54(); // lora_tick
|
|
FUN_10002ab8(5); // sleep_ms(5)
|
|
iVar3 = iVar3 + -1;
|
|
} while (iVar3 != 0);
|
|
FUN_10000858(&local_20,&uStack_1c); // gps_get_stats
|
|
if (bVar4 == 0) break;
|
|
*piVar1 = 3; // hold = 3
|
|
FUN_10000bb0(1,local_20); // set_gnss_leds
|
|
LAB_100002a4:
|
|
FUN_1000175c(local_18,uStack_14,local_10,uStack_c); // lcd_show_coords
|
|
FUN_10000cb8(local_18,uStack_14,local_10,uStack_c); // navigate_to_target
|
|
}
|
|
iVar3 = *piVar1;
|
|
if (iVar3 < 1) { iVar3 = 1; }
|
|
*piVar1 = iVar3 + -1; // hold--
|
|
if (iVar3 + -1 != 0) {
|
|
FUN_10000bb0(1,local_20); // set_gnss_leds
|
|
goto LAB_100002a4;
|
|
}
|
|
FUN_10000bb0(0,local_20); // set_gnss_leds(0,...)
|
|
FUN_10001b4c(local_20,uStack_1c); // lcd_show_gnss
|
|
FUN_10000b34(); // propeller_stop
|
|
FUN_10000c7c(local_18,uStack_14,local_10,uStack_c); // send_telemetry
|
|
} while( true );
|
|
}
|
|
```
|
|
|
|
`FUN_` prefixes everywhere: this is what a stripped target really looks like.
|
|
|
|
---
|
|
|
|
## 5. The Decoy And The Encrypted Target
|
|
|
|
`navigate_to_target` no longer compares against a literal; it reads the pointers
|
|
at `DAT_10000e50` / `DAT_10000e54`, which resolve to RAM `0x20000D00` /
|
|
`0x20000D08`, **runtime doubles**. Where do they come from? `init_navigation`,
|
|
and that is the whole puzzle.
|
|
|
|
```
|
|
void init_navigation(void)
|
|
{
|
|
local_28 = *DAT_10000c6c; // key @ 0x10009D90
|
|
uStack_24 = DAT_10000c6c[1];
|
|
uStack_20 = DAT_10000c6c[2];
|
|
uStack_1c = DAT_10000c6c[3];
|
|
local_18 = *DAT_10000c70; // ct @ 0x10009DA4
|
|
uStack_14 = DAT_10000c70[1];
|
|
uStack_10 = DAT_10000c70[2];
|
|
uStack_c = DAT_10000c70[3];
|
|
FUN_10000e5c(&local_18,&local_28,&local_38); // aes128_ecb_decrypt_block
|
|
*DAT_10000c74 = local_38; // TARGET_LAT -> 0x20000D00
|
|
puVar1[1] = uStack_34;
|
|
*puVar2 = local_30; // TARGET_LON -> 0x20000D08
|
|
puVar2[1] = uStack_2c;
|
|
}
|
|
|
|
```
|
|
|
|
Three data addresses do all the work:
|
|
|
|
| Symbol | Address | Meaning |
|
|
|---|---|---|
|
|
| `CTF_AES_KEY` | `0x10009D90` | the AES key `564543544f5231314145534b45592121` |
|
|
| `CTF_TARGET_CT` | `0x10009DA4` | the encrypted target pair |
|
|
| `TARGET_LAT/LON` | `0x20000D00` / `0x20000D08` | RAM, reconstructed at boot |
|
|
|
|
**Reading the key bytes (Ghidra will call them code):**
|
|
|
|
`0x10009D90` is **data**, not code. `init_navigation` copies the block into a
|
|
stack buffer and hands it to `aes128_ecb_decrypt_block`; it is never executed.
|
|
Ghidra still marks it as code because it sees the read cross-reference from
|
|
`init_navigation` (`FUN_10000c2c:10000c36(R)`) and guesses. The Listing then
|
|
shows fabricated mnemonics:
|
|
|
|
```
|
|
LAB_10009d90 XREF[1]: FUN_10000c2c:10000c36(R)
|
|
10009d90 56 45 cmp r6,r10
|
|
10009d92 43 54 strb r3,[r0,r1]
|
|
10009d94 4f 52 strh r7,[r1,r1]
|
|
10009d96 31 31 adds r1,#0x31
|
|
10009d98 41 45 cmp r1,r8
|
|
10009d9a 53 4b ldr r3,[s_n_"%s"_failed:_file_"%s",_line = "n \"%s\" failed: file
|
|
10009d9c 45 59 ldr r5,[r0,r5]
|
|
10009d9e 21 21 movs r1,#0x21
|
|
```
|
|
|
|
Those are the key bytes, not instructions. Two traps:
|
|
|
|
1. The mnemonics are meaningless: `56 45` is `V`,`E`; `43 54` is `C`,`T`;
|
|
`4f 52` is `O`,`R`.
|
|
2. At `0x10009D9A` the halfword `53 4B` decodes as `ldr r3, [pc, #332]`,
|
|
whose literal-pool target is `0x10009EE8`. That address really does hold a
|
|
string, newlib's assert message `Assertion "%s" failed: file "%s", line
|
|
%d%s%s` at `0x10009EE0`, so Ghidra prints its label. The string is real, but
|
|
the reference is spurious: `53 4B` is `S`,`K`, bytes 10-11 of the key, and
|
|
only decodes as that `ldr` by coincidence. The same thing happens at
|
|
`0x10009DA4`, where the ciphertext byte pair `20 4F` decodes to an `ldr`
|
|
that lands on the real two-space string at `0x10009E28`.
|
|
|
|
Read the bytes, never the mnemonics. Any of these is exact:
|
|
|
|
```bash
|
|
xxd -s 0x9D90 -l 16 0x0011a_cb.bin # raw image, no tools
|
|
```
|
|
|
|
```gdb
|
|
(gdb) x/16bx 0x10009D90 # live target
|
|
```
|
|
|
|
In the Ghidra GUI the simplest path is **Window -> Bytes**, press **G**, enter
|
|
`0x10009D90`, and read the 16 raw bytes. To retype them in the Listing instead,
|
|
select the 16 bytes, press **`C`** (Clear Code/Data), then **`T`** (Define Data)
|
|
and choose `byte`; press **`[`** to make it an array of 16. Note that **`B`**
|
|
is **not** "define byte" in Ghidra, it is *Cycle Integer Types*, and data
|
|
cannot be defined over bytes that are still typed as code, which is why the
|
|
clear (**`C`**) must come first.
|
|
The value is the ASCII key `VECTOR11AESKEY!!`:
|
|
`56 45 43 54 4F 52 31 31 41 45 53 4B 45 59 21 21`.
|
|
|
|
**Decrypt the target (fully offline, reproducible):**
|
|
|
|
Step 1. The key, 16 bytes at `0x10009D90` (read them as above):
|
|
|
|
```text
|
|
56 45 43 54 4F 52 31 31 41 45 53 4B 45 59 21 21 = "VECTOR11AESKEY!!"
|
|
hex for tools: 564543544f5231314145534b45592121
|
|
```
|
|
|
|
Step 2. The ciphertext, 16 bytes at `0x10009DA4`. Read them the exact same way
|
|
(**Window -> Bytes**, press **G**, enter `0x10009DA4`):
|
|
|
|
```text
|
|
20 4F AC C2 C0 23 EA 7A 3A 93 0F 71 11 23 EF CA
|
|
hex for tools: 204facc2c023ea7a3a930f711123efca
|
|
```
|
|
|
|
Step 3. AES-128-ECB decrypt the block with the key (no padding). The plaintext
|
|
is two little-endian IEEE-754 doubles, latitude then longitude.
|
|
|
|
Python, all platforms (`cryptography` is already used by the course):
|
|
|
|
```python
|
|
import struct
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
key = bytes.fromhex("564543544f5231314145534b45592121")
|
|
ct = bytes.fromhex("204facc2c023ea7a3a930f711123efca")
|
|
|
|
pt = Cipher(algorithms.AES(key), modes.ECB()).decryptor().update(ct)
|
|
print("plaintext:", pt.hex())
|
|
print("lat:", struct.unpack("<d", pt[:8])[0])
|
|
print("lon:", struct.unpack("<d", pt[8:])[0])
|
|
```
|
|
|
|
Output:
|
|
|
|
```text
|
|
plaintext: 6284f068e3704340bf823463d15c53c0
|
|
lat: 38.88194
|
|
lon: -77.45028
|
|
```
|
|
|
|
`<d` is a little-endian 64-bit double, exactly how the firmware reads the pair.
|
|
|
|
Same result with `openssl` (macOS, Linux, WSL):
|
|
|
|
```bash
|
|
printf '\x20\x4f\xac\xc2\xc0\x23\xea\x7a\x3a\x93\x0f\x71\x11\x23\xef\xca' \
|
|
| openssl enc -aes-128-ecb -K 564543544f5231314145534b45592121 -nopad -d \
|
|
| xxd
|
|
# 00000000: 6284 f068 e370 4340 bf82 3463 d15c 53c0
|
|
```
|
|
|
|
Same result in CyberChef (browser, nothing to install):
|
|
|
|
1. **From Hex** on `204facc2c023ea7a3a930f711123efca`.
|
|
2. **AES Decrypt**: Key = `Hex` `564543544f5231314145534b45592121`,
|
|
Mode = `ECB`, Padding = `None`, Input/Output = `Raw`.
|
|
3. **To Hex** to read `6284f068e3704340bf823463d15c53c0`.
|
|
4. That is two 8-byte little-endian doubles:
|
|
`62 84 F0 68 E3 70 43 40` and `BF 82 34 63 D1 5C 53 C0`.
|
|
|
|
Step 4. Decode the two doubles with the bit layout
|
|
|
|
$$V = (-1)^s \times 2^{e-1023} \times (1 + m)$$
|
|
|
|
The decrypted bytes are little-endian, so reverse each 8-byte group to the
|
|
big-endian hex word the Week 5 utility expects:
|
|
|
|
```bash
|
|
python3 scripts/float_hex_converter.py 0x404370E368F08462 # +38.881940
|
|
python3 scripts/float_hex_converter.py 0xC0535CD1633482BF # -77.450280
|
|
```
|
|
|
|
`struct.unpack("<d", ...)` already performs that byte swap for you.
|
|
|
|
And the plaintext pair at `0x1000A090` / `0x1000A098` (`+38.840280` /
|
|
`-77.428890`)? That is the **decoy**, the beacon's broadcast, and it is a lie.
|
|
The real target only exists after the AES.
|
|
|
|
## 6. `release_payload` (Raw Decompilation)
|
|
|
|
```
|
|
void release_payload(void)
|
|
{
|
|
coprocessor_moveto2(0,4,0x10,1,in_cr0); // GPIO16 = 1
|
|
coprocessor_moveto2(0,4,0x11,1,in_cr0); // GPIO17 = 1
|
|
coprocessor_moveto2(0,4,0x12,1,in_cr0); // GPIO18 = 1
|
|
__wrap_puts(uRam10009d3c); // "PAYLOAD RELEASED AT TARGET COORDINATES"
|
|
lora_send(uRam10009d64); // tail call
|
|
}
|
|
```
|
|
|
|
`coprocessor_moveto2` is Ghidra's rendering of the RP2350 GPIO `mcrr p0, #4`
|
|
path. A model will "helpfully" tell you this is a normal SIO store; the
|
|
encoding says otherwise, and GDB confirms it live.
|
|
|
|
---
|
|
|
|
### Two coordinate pairs (the decoy)
|
|
|
|
The image contains **two** hardcoded double pairs, not one:
|
|
|
|
| Pair | Address | Meaning |
|
|
|---|---|---|
|
|
| Launch origin (Centreville) | `0x1000A098` / `0x1000A090` | broadcast by the crash beacon |
|
|
| Target (NRO HQ) | `0x20000D00` / `0x20000D08` | RAM, rebuilt by `init_navigation` |
|
|
|
|
A pattern-matcher latches onto the **broadcast** origin and calls it the target.
|
|
The only way to tell them apart is the cross-reference: the target pair is
|
|
loaded with `ldrd` and fed to `__aeabi_dcmpeq` inside `navigate_to_target`; the
|
|
origin pair is handed to `send_telemetry`. Strings and intuition are not enough.
|
|
|
|
---
|
|
|
|
## 7. The Patch: Re-vector to a Safe Waypoint
|
|
|
|
The target is the 16 bytes at `0x10009DA4`. To re-vector the drone you replace
|
|
that block with the AES-128-ECB encryption of a safe waypoint (`37.0` / `-74.0`,
|
|
open Atlantic) under the same key. Compute the safe doubles and their ciphertext:
|
|
|
|
```bash
|
|
python3 scripts/float_hex_converter.py 37.0 # 0x4042800000000000
|
|
python3 scripts/float_hex_converter.py -74.0 # 0xC052800000000000
|
|
|
|
python3 -c "import struct,sys; sys.stdout.buffer.write(struct.pack('<d',37.0)+struct.pack('<d',-74.0))" \
|
|
| openssl enc -aes-128-ecb -K 564543544f5231314145534b45592121 -nopad | xxd -p
|
|
# c2bb647c8778bf59279c01ad066bb16b
|
|
```
|
|
|
|
In the Ghidra Listing, press **G** to `0x10009DA4`, select the 16 bytes, then
|
|
**Ctrl+Shift+G** (Patch Data):
|
|
|
|
| Address | Original | Patched |
|
|
|---|---|---|
|
|
| `0x10009DA4` | `20 4F AC C2 C0 23 EA 7A 3A 93 0F 71 11 23 EF CA` | `C2 BB 64 7C 87 78 BF 59 27 9C 01 AD 06 6B B1 6B` |
|
|
|
|
Export: **File -> Export Program...** -> Format **Binary** ->
|
|
`0x0011a_cb_patched.bin`.
|
|
|
|
---
|
|
|
|
## 8. Export, Convert, Flash, Verify
|
|
|
|
```bash
|
|
python3 uf2conv.py 0x0011a_cb_patched.bin \
|
|
-f 0xe48bff59 -b 0x10000000 -c -o 0x0011a_cb_patched.uf2
|
|
```
|
|
|
|
Hold **BOOTSEL**, copy the UF2 across, and verify against the live ground HUD.
|
|
A patch that is not flashed and confirmed is a guess.
|
|
|
|
---
|
|
|
|
## 9. Randomized Builds
|
|
|
|
Each student image embeds a unique key and waypoint, so no answer key travels:
|
|
|
|
```bash
|
|
python3 scripts/randomize_build.py --student-id alice --seed 12345 --uf2
|
|
[+] student_id : alice
|
|
[+] TARGET_LAT : 38.880272
|
|
[+] TARGET_LON : -77.460077
|
|
[+] AES key : <16 random bytes>
|
|
[+] ciphertext : <AES-128-ECB(key, target)>
|
|
[+] image : build-ctf/0x0011a_cb_alice.uf2
|
|
[+] answer key : <keydir>/answer_alice.json (INSTRUCTOR ONLY, do not ship)
|
|
```
|
|
|
|
Addresses are identical; only the key and ciphertext bytes differ.
|
|
|
|
---
|
|
|
|
## 10. Reproducibility Checklist
|
|
|
|
1. Import raw `.bin`, `ARM:LE:32:Cortex`, base `0x10000000`.
|
|
2. Reset vector `[0]=0x20082000`, `[1]=0x1000015D`.
|
|
3. `main @ 0x10000234`; fix the merged boundary at `a single FUN_ function`.
|
|
4. `navigate_to_target @ 0x10000CB8` reads the pointers at `0x10000E50`/`0x10000E54`, targeting RAM `0x20000D00`/`0x20000D08`.
|
|
5. Decode to `+38.881940` / `-77.450280`.
|
|
6. Patch, export, `uf2conv`, flash, verify.
|
|
|
|
---
|
|
|
|
## 11. Why Buddy Fails Here
|
|
|
|
Ghidra itself proves the point: it *proposes* functions and boundaries, and the
|
|
analyst corrects them with cross-references and the bench. A language model
|
|
does the same, faster and wrong, with no way to confirm. It cannot validate a
|
|
boundary, cannot read the live `ldrd`, and cannot flash a patch to see the
|
|
drone re-vector. Structure is a hypothesis; silicon is the verdict.
|
|
**Think, then verify.**
|
|
|
|
---
|
|
|
|
## Appendix A. Deep Ghidra Step-Through
|
|
|
|
### A.1 Import
|
|
|
|
Language `ARM:LE:32:Cortex`, Base Address `0x10000000`, Raw Binary. Analyze.
|
|
|
|
### A.2 Address map
|
|
|
|
| Address | What it is |
|
|
|---|---|
|
|
| `0x10000234` | `main` |
|
|
| `0x10000C2C` | `init_navigation`, rebuilds the target |
|
|
| `0x10000E5C` | `aes128_ecb_decrypt_block` |
|
|
| `0x10000CB8` | `navigate_to_target`, the arrival compare |
|
|
| `0x10009D90` | AES key |
|
|
| `0x10009DA4` | ciphertext |
|
|
| `0x1000A090` | decoy waypoint |
|
|
|
|
### A.3 The S-box
|
|
|
|
Go to `0x1000A1AC` and find the 256-byte S-box that starts `63 7C 77 7B` (the
|
|
inverse S-box, starting `52 09 6A D5`, sits at `0x1000A0AC`). Right click,
|
|
create an array of 256 bytes.
|
|
|
|
### A.4 Recover
|
|
|
|
Take 16 bytes at `0x10009DA4` and the key at `0x10009D90`, then run the Python
|
|
decrypt block from section 5.
|
|
|
|
### A.5 Patch
|
|
|
|
Overwrite the 16 bytes at `0x10009DA4` with the re-encrypted Atlantic block,
|
|
then File, Export Program, Format Binary.
|
|
|
|
### A.6 Platform note
|
|
|
|
Ghidra is identical on Windows, Linux, and macOS. Only the paths differ.
|
|
|
|
| Action | macOS | Linux | Windows |
|
|
|---|---|---|---|
|
|
| GUI launch | `/Applications/ghidra_12.0.4_PUBLIC/ghidraRun` | `$HOME/ghidra_12.0.4_PUBLIC/ghidraRun` | `C:\ghidra_12.0.4_PUBLIC\ghidraRun.bat` |
|
|
| Headless | `/Applications/ghidra_12.0.4_PUBLIC/support/analyzeHeadless` | `$HOME/ghidra_12.0.4_PUBLIC/support/analyzeHeadless` | `C:\ghidra_12.0.4_PUBLIC\support\analyzeHeadless.bat` |
|
|
| Shell | `zsh` / `bash` | `bash` | PowerShell |
|
|
|
|
See sections 2.1 and 2.2 for the full import commands.
|
|
|