- 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
15 KiB
Ghidra Binary Patching Tutorial: ARM Cortex-M Thumb-2 Bytes Window Workflow
+-----------------------------------------------------------------+
| GHIDRA BYTES WINDOW BINARY PATCHING GUIDE |
| |
| Target Architecture: ARM Cortex-M33 (ARMv8-M Main / Thumb-2) |
| Tool: Ghidra Software Reverse Engineering Suite |
| Core Technique: Listing Clear (C) -> Bytes Window Edit |
| -> Listing Disassemble (D) |
| Target Firmware: Raspberry Pi Pico 2 (RP2350) Raw Binaries |
| Classification: Defensive Firmware Security & Patching |
+-----------------------------------------------------------------+
1. Executive Summary & Overview
Binary patching is a fundamental capability in embedded systems reverse engineering and defensive firmware security. When analyzing compiled firmware images—such as raw binary files (.bin) extracted from bare-metal microcontrollers—security analysts and engineers frequently need to modify program logic directly in machine code without access to the original source code or a compilation toolchain.
Common operational scenarios for embedded binary patching include:
- Defensive Telemetry Correction: Rectifying corrupted or spoofed sensor thresholds in mission-critical industrial or SCADA control nodes.
- Vulnerability Remediation & Hot-Patching: Neutralizing memory corruption flaws, logic bugs, or insecure dispatch routines in deployed firmware.
- Hardware Reverse Engineering Challenges: Solving embedded Capture The Flag (CTF) challenges where firmware logic gates must be manipulated to uncover security flags.
While high-level desktop reverse engineering tutorials often recommend right-clicking an instruction and selecting Patch Instruction, this GUI action consistently fails when targeting ARM Cortex-M Thumb-2 binaries. This comprehensive guide documents the underlying architectural reason for this failure and provides the official, step-by-step Bytes Window Workflow using high-resolution Ghidra screenshots from live analysis of CTF-01.bin.
2. The Architectural Problem: Why GUI Patching Fails in ARM Thumb-2
2.1 The ARM Cortex-M Thumb-2 Instruction Set & IT Blocks
The Raspberry Pi Pico 2 is powered by the dual-core ARM Cortex-M33 microcontroller, which executes the ARMv8-M Mainline (Thumb-2) instruction set. In Thumb-2 mode:
- Instructions are variable-length: either 16 bits (2 bytes) or 32 bits (4 bytes).
- Conditional execution is handled via If-Then (
IT) blocks (e.g.,it,ite,itt). - An instruction immediately preceding an
ITblock sets the Condition Code Flags (e.g.,cmp r3, #0x5e), and the subsequentite hiinstruction evaluates those flags to execute conditionally.
2.2 The Context Collision in Ghidra's Disassembler
When you right-click a compare instruction (such as cmp r3, #0x5e at offset 0x100001FC) and select Patch Instruction (or press Ctrl+Shift+G):
+-----------------------------------------------------------------+
| GUI PATCH INSTRUCTION FAILURE CHAIN |
| |
| 1. User invokes GUI Patch Instruction at compare site |
| 2. Ghidra PatchInstructionAction invokes ReDisassembleCommand |
| 3. ReDisassembleCommand hits subsequent 'ite' block |
| 4. Ghidra attempts to write internal ITBlock context register |
| 5. CodeManager throws: ContextChangeException |
| 6. Ghidra aborts Thumb decoding -> falls back to 32-bit ARM |
| 7. Downstream instructions collapsed into movwcs / ldmdavs |
| 8. Compare Site B (0x1000020A) completely swallowed & lost |
+-----------------------------------------------------------------+
Ghidra maintains internal context registers (such as TMode for Thumb execution and ITBlock for conditional state tracking). When ReDisassembleCommand encounters an existing instruction downstream of the patch site, the context register write collides with existing database state, throwing a ContextChangeException (Context register change conflicts with one or more instructions).
Because Ghidra's assembler aborts decoding Thumb instructions upon this conflict, it defaults to 32-bit ARM disassembly mode. It misinterprets the 16-bit Thumb opcode bytes as 32-bit ARM instructions (movwcs, ldmdavs, blcs), which swallows downstream code and completely obliterates subsequent compare sites (such as Compare Site B at 0x1000020A).
2.3 Why 'Locking TMode = 1' (Ctrl+R) Does Not Solve the Issue
Attempting to highlight the .text section and press Ctrl+R (Set Register Values) to set TMode = 1 also fails once Ghidra has already performed its initial auto-analysis. Ghidra's database rules forbid setting context register values across memory ranges that already contain disassembled code.
The clean, permanent, and battle-tested technique is the Bytes Window Workflow.
3. The 5-Step Bytes Window Workflow
The Bytes Window Workflow completely bypasses Ghidra's assembler context conflicts. By clearing the single instruction to undefined raw bytes, editing the underlying byte in the hex view, and then disassembling just that location, Ghidra's disassembler operates cleanly without attempting to rewrite ITBlock context registers over existing downstream code.
Step 1: Open the Bytes Window
By default, Ghidra displays the Listing view and the Decompiler view. To inspect and edit raw hex bytes directly:
- Look at the top Ghidra menu bar.
- Click Window in the menu.
- Select Bytes: .bin (for example, Bytes: CTF-01.bin).
Figure 1: Navigating to Window -> Bytes: CTF-01.bin in the Ghidra menu bar.
- Dock the Bytes window side-by-side with your Listing window so both panels are simultaneously visible on your screen.
Step 2: Locate the Target Instruction in the Listing Window
Before making any modifications, navigate to the target function and identify the exact instruction bytes:
- Click inside the Listing window and press key
G(Go To Address). - Enter the target address (for example,
0x100001FC). - Observe the disassembled instruction and its opcode encoding:
100001fc 5e 2b cmp r3,#0x5e
Figure 2: Listing view at offset 100001fc showing the initial instruction: cmp r3,#0x5e (bytes 5e 2b).
Technical Breakdown of Opcode 5e 2b:
In ARMv8-M Thumb-16 architecture, the compare-immediate instruction cmp <Rn>, #<imm8> is encoded as:
- Bit pattern:
0010 1nnn iiiiiiii - Opcode field (
00101): SpecifiesCMPimmediate. - Register
r3(nnn = 011binary): Specifies operand registerr3. - Upper byte:
0010 1011binary =0x2B. - Lower byte:
0x5E(hex) =94decimal (the comparison threshold). - Because ARM Cortex-M microcontrollers are little-endian, the low byte (
5E) appears first in memory at address0x100001FC, followed by the high byte (2B) at address0x100001FD. - To change the threshold from
94(0\text{x}5E) to59(0\text{x}3B), we only need to alter byte0x100001FCfrom5Eto3B!
Step 3: Clear the Code Bytes in the Listing Window (C)
This is the critical step that prevents assembler context conflicts. Before editing a byte, the instruction must be cleared from Ghidra's active instruction database:
- In the Listing window, click directly on address
100001fc(the row containingcmp r3,#0x5e). - Press the key
Con your keyboard (or right-click and select Clear Code Bytes). - Notice that the instruction disassembly immediately clears into undefined raw bytes:
100001fc 5e ?? 5Eh ^
100001fd 2b ?? 2Bh +
Figure 3: Listing view after pressing 'C': the instruction at 100001fc is cleared to raw undefined bytes.
By clearing the instruction first, Ghidra's database disarms the active context parser for this address. The subsequent ite hi instruction downstream is no longer linked to an active parsing state at this address.
Step 4: Enable Edit Mode and Modify the Byte in the Bytes Window
Now that the address is cleared, modify the raw hex byte using the Bytes window:
- In the Bytes window toolbar, locate and click the Pencil Icon (Toggle Edit Mode). The icon highlights to indicate that byte-editing mode is now active.
- Locate offset row
100001f0in the Bytes window. - Move your cursor to column
C(representing address100001fc). - Click on the byte
5Eand type the replacement value3B. - Notice that Ghidra immediately updates the byte to
3Band highlights it in red to indicate an uncommitted edit.
Figure 4: Side-by-side view showing the active pencil icon, the modified byte '3b' highlighted in red in the Bytes window, and the synchronized Listing window.
- Look at the Listing window. Address
100001fcnow reflects the new byte3bwhile remaining undefined:
100001fc 3b ?? 3Bh ;
100001fd 2b ?? 2Bh +
Figure 5: Listing view showing the modified byte '3b' at 100001fc ready for disassembly.
Step 5: Disassemble the Modified Instruction (D)
With the desired byte safely written to memory, restore the disassembly:
- Click back into the Listing window.
- Click directly on address
100001fc. - Press the key
Don your keyboard (or right-click and select Disassemble). - Ghidra immediately decodes the bytes
3b 2bas Thumb-2 machine code:
100001fc 3b 2b cmp r3,#0x3b
- Verify Downstream Disassembly: Inspect the instructions immediately following
0x100001FC:100001fe 8c bf ite hiremains completely intact!10000200 00 23 movhi r3,#0x0remains intact!10000202 01 23 movls r3,#0x1remains intact!- Compare Site B at
0x1000020Aremains completely intact and visible!
No context conflict was triggered, no instructions were swallowed, and no 32-bit ARM decoding errors occurred.
Step 6: Repeat for Secondary Sites & Export Patched Firmware
Repeating on Compare Site B:
In many defensive patches (such as CTF-01), redundant checks or dual comparison gates are enforced by the compiler:
- In the Listing window, click address
1000020a(cmp r3,#0x5e). - Press
Cto clear code bytes. - In the Bytes window at offset
1000020a, change byte5Eto3B. - In the Listing window, click back on
1000020aand pressDto disassemble. - Both Compare Site A and Compare Site B are now patched to
0x3B(59 decimal)!
Exporting the Patched Binary Image:
- Click File in the Ghidra menu bar.
- Select Export Program (or press key
O). - Set Format to Raw Bytes.
- Click the
...button next to Output File and navigate to your project directory. - Set the filename (for example,
CTF-01_fixed.bin). - Click OK to save the patched binary image to disk.
Converting to UF2 and Flashing:
Convert the exported raw binary to the Raspberry Pi Pico 2 UF2 format using uf2conv.py:
python3 uf2conv.py CTF-01_fixed.bin \
--base 0x10000000 \
--family 0xe48bff59 \
--output CTF-01_fixed.uf2
Hold the BOOTSEL button on your Raspberry Pi Pico 2, plug it into your workstation's USB port, and drag-and-drop CTF-01_fixed.uf2 onto the RP2350 drive to verify your patched firmware on physical hardware!
4. Method Comparison & Reference Guide
+-----------------------------------------------------------------+
| GHIDRA ARM THUMB-2 PATCHING METHODS MATRIX |
| |
| Method Downstream Safety IT-Block Safe Status |
| ----------------- ----------------- ------------- ------ |
| GUI Patch Inst. Fails (Swallows) No (Conflict) AVOID |
| TMode = 1 (Ctrl+R) Fails (Database) No (Exception) AVOID |
| Bytes Window (C/D) 100% Safe & Clean Yes (Disarmed) OPTIMAL |
+-----------------------------------------------------------------+
Keystroke Quick Reference Table
| Step | Action | Window | Shortcut / Control | Result |
|---|---|---|---|---|
| 1 | Open Bytes Panel | Menu Bar | Window \rightarrow Bytes: <bin> |
Displays raw hex grid |
| 2 | Locate Target | Listing | Press G \rightarrow enter address |
Cursor at target opcode |
| 3 | Clear Code Bytes | Listing | Press C |
Instruction cleared to ?? |
| 4 | Enable Edit Mode | Bytes | Click Pencil Icon | Enables write mode |
| 5 | Modify Hex Byte | Bytes | Type new hex byte (3B) |
Byte highlighted in red |
| 6 | Re-Disassemble | Listing | Click address \rightarrow press D |
Reassembles cleanly as Thumb-2 |
| 7 | Export Program | Menu Bar | File \rightarrow Export Program |
Saves patched .bin file |
5. Key Takeaways & Best Practices
- Thumb-2 Conditional State is Fragile in Ghidra: The presence of
IT/ITEblocks creates dynamic context register dependencies. High-level assembler dialogs fail because they attempt to re-evaluate downstream context over existing disassembly. - Clear First (
C), Edit Second, Disassemble Last (D): Always clear the code bytes before modifying machine code in Ghidra. Clearing breaks the active context dependency graph and allows clean byte substitution. - Always Verify Downstream Code: After pressing
D, scan the next 10 instructions to ensure downstream labels, branch targets, and conditional blocks were not disrupted. - Export as Raw Bytes: When flashing embedded microcontrollers like the RP2350, always export as Raw Bytes (
.bin), never as ELF or PE, before converting to UF2.




