Files
Embedded-Hacking/WEEK05/WEEK05-04.md
2026-03-19 15:01:07 -04:00

8.4 KiB
Raw Blame History

Embedded Systems Reverse Engineering

Repository

Week 5

Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis

Non-Credit Practice Exercise 4: Patch the Double Binary — Changing 42.52525 to 99.99

Objective

Calculate the IEEE 754 double-precision encoding of 99.99, patch both register words in the double binary in Ghidra, export the patched binary, convert it to UF2 format, and flash it to the Pico 2 to verify the change.

Prerequisites

  • Completed Exercise 3 (double binary imported and analyzed in Ghidra)
  • Understanding of IEEE 754 double-precision encoding from Week 5 Parts 2.7 and 3.7
  • Knowledge of integer-to-binary conversion and the multiply-by-2 method for fractions
  • Python installed for UF2 conversion and verification
  • Raspberry Pi Pico 2 connected via USB

Task Description

You will derive the IEEE 754 encoding of 99.99 step by step (integer part, fractional part, normalization, field extraction), patch both the low word and high word data constants in Ghidra, and verify on hardware that the serial output now prints 99.990000. Unlike Exercise 2 where only one word changed, this exercise requires patching both registers.

Step-by-Step Instructions

Step 1: Convert the Integer Part (99) to Binary

Use repeated division by 2:

Division Quotient Remainder
99 ÷ 2 49 1
49 ÷ 2 24 1
24 ÷ 2 12 0
12 ÷ 2 6 0
6 ÷ 2 3 0
3 ÷ 2 1 1
1 ÷ 2 0 1

Read remainders bottom-to-top: 99_{10} = 1100011_2

Step 2: Convert the Fractional Part (.99) to Binary

Use the multiply-by-2 method:

Multiply Result Integer part Remaining fraction
0.99 × 2 1.98 1 0.98
0.98 × 2 1.96 1 0.96
0.96 × 2 1.92 1 0.92
0.92 × 2 1.84 1 0.84
0.84 × 2 1.68 1 0.68
0.68 × 2 1.36 1 0.36
0.36 × 2 0.72 0 0.72
0.72 × 2 1.44 1 0.44
0.44 × 2 0.88 0 0.88
0.88 × 2 1.76 1 0.76
0.76 × 2 1.52 1 0.52
0.52 × 2 1.04 1 0.04
... ... ... (continues — repeating fraction)

Reading the integer parts top-to-bottom: 0.99_{10} \approx 0.111111010111..._2

This is a repeating fraction — it never terminates in binary.

Step 3: Combine Integer and Fractional Parts
99.99_{10} = 1100011.111111010111..._2
Step 4: Normalize to IEEE 754 Form

Move the binary point so there is exactly one 1 before it:

1100011.111111010111..._2 = 1.100011111111010111..._2 \times 2^6

We shifted the binary point 6 places left, so the exponent is 6.

Step 5: Extract the IEEE 754 Fields
  1. Sign: 0 (positive)
  2. Exponent: 6 + 1023 = 1029 = 10000000101_2
  3. Mantissa: 1000111111110101110000101000111101011100001010001111... (52 bits after the 1.)
  4. Full double: 0x4058FF5C28F5C28F

Verify with Python:

>>> import struct
>>> struct.pack('>d', 99.99).hex()
'4058ff5c28f5c28f'
Step 6: Split into Register Words
Register Old Value New Value Changed?
r2 0x645A1CAC 0x28F5C28F Yes
r3 0x4045433B 0x4058FF5C Yes

Both registers change! This is the key difference from Exercise 2 where only r3 changed.

Step 7: Locate the Data Constants in Ghidra

Open your Ghidra project from Exercise 3. In the Listing view, find the two data constants:

Low word (loaded into r2):

        DAT_10000254
10000254 ac  1c  5a  64    undefined4  645A1CACh

High word (loaded into r3):

        DAT_10000258
10000258 3b  43  45  40    undefined4  4045433Bh
Step 8: Patch the Low Word
  1. Click on WindowBytes to open the Bytes Editor
  2. Click the Pencil Icon to enable editing
  3. Navigate to address 10000254
  4. The bytes read: AC 1C 5A 64 (little-endian for 0x645A1CAC)
  5. Change to: 8F C2 F5 28 (little-endian for 0x28F5C28F)
  6. Press Enter
Step 9: Patch the High Word
  1. Navigate to address 10000258
  2. The bytes read: 3B 43 45 40 (little-endian for 0x4045433B)
  3. Change to: 5C FF 58 40 (little-endian for 0x4058FF5C)
  4. Press Enter

Verify in the Listing view:

  • DAT_10000254 should show 28F5C28Fh
  • DAT_10000258 should show 4058FF5Ch

Together: 0x4058FF5C28F5C28F = 99.99 as a double ✓

Step 10: Export the Patched Binary
  1. Click FileExport Program
  2. Set Format to Raw Bytes
  3. Navigate to your build directory
  4. Name the file: 0x0011_double-floating-point-data-type-h.bin
  5. Click OK
Step 11: Convert to UF2 Format

Open a terminal:

cd Embedded-Hacking-main\0x0011_double-floating-point-data-type
python ..\uf2conv.py build\0x0011_double-floating-point-data-type-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
Step 12: Flash and Verify
  1. Hold BOOTSEL and plug in your Pico 2
  2. Drag and drop hacked.uf2 onto the RPI-RP2 drive
  3. Open your serial monitor

Expected output:

fav_num: 99.990000
fav_num: 99.990000
fav_num: 99.990000
...

🎉 Success! The value changed from 42.52525 to 99.99!

Expected Output

After completing this exercise, you should:

  • See fav_num: 99.990000 printing instead of fav_num: 42.525250
  • Have a patched binary file (0x0011_double-floating-point-data-type-h.bin)
  • Have a UF2 file (hacked.uf2)
  • Understand that patching doubles with repeating fractions requires modifying both register words

Questions for Reflection

Question 1: Why did both r2 and r3 change when patching 42.52525 → 99.99, but only r3 changed when patching 42.5 → 99.0?
Question 2: The multiply-by-2 method for 0.99 produces a repeating pattern. What does this mean for the precision of the stored value?
Question 3: If you wanted to patch the double to 100.0 instead of 99.99, how many data constants would need to change?
Question 4: Compare the Ghidra Listing for the float binary (Exercise 1) and the double binary (Exercise 3). How does the compiler load the double differently?

Tips and Hints

  • Always verify your encoding with Python before patching
  • Little-endian byte order: 0x28F5C28F is stored as 8F C2 F5 28 in memory
  • Use Ghidra's Bytes window (Window → Bytes) for precise hex editing
  • If r2 was zero before and needs to be non-zero after, you need to patch the data constant — not the movs r4, #0x0 instruction
  • The ldrd instruction loads r4 and r5 from two consecutive memory addresses — both must be correct

Next Steps

  • Review the complete patching workflow diagram in Week 5 Part 3.95
  • Try patching to 100.0 — since it has a zero low word, you'll need to change r2 from non-zero to zero
  • Attempt the practice exercises at the end of Week 5

Additional Challenge

Patch the double to 3.14159265358979 (pi). This requires extreme precision in all 52 mantissa bits. Use Python to get the exact encoding, then patch both words. Verify the output prints at least 6 correct decimal places. What happens to the precision if you only patch the high word and leave the low word as 0x645A1CAC?

Verification Checklist

Before moving on, confirm:

  • Serial output shows fav_num: 99.990000
  • Both data constants were patched (low word and high word)
  • You can derive the IEEE 754 encoding of 99.99 from scratch
  • You understand why messy fractions require patching both register words
  • You can explain the difference between the float and double patching workflows
  • You successfully converted and flashed the UF2

Congratulations! You've completed all Week 5 exercises and mastered floating-point analysis, IEEE 754 decoding, and double-precision binary patching!