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

9.2 KiB
Raw Permalink 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 3: Analyze the Double Binary in Ghidra

Objective

Import and analyze the 0x0011_double-floating-point-data-type.bin binary in Ghidra to understand how doubles differ from floats at the binary level, observe that both register words carry non-zero data when the fractional part is complex, and decode the IEEE 754 double-precision encoding of 42.52525 from two 32-bit registers.

Prerequisites

  • Completed Exercises 1 and 2 (float analysis and patching)
  • Understanding of IEEE 754 double-precision format from Week 5 Part 3
  • Understanding of register pairs (r2:r3) from Exercise 1
  • Basic Ghidra navigation skills

Task Description

You will import the double binary into Ghidra, resolve function names, identify that 42.52525 requires non-zero data in both r2 and r3 (unlike 42.5 which had r2 = 0), and decode the full 64-bit double 0x4045433B645A1CAC field by field to confirm it represents 42.52525.

Step-by-Step Instructions

Step 1: Flash the Original Binary

Before analysis, verify the program works:

  1. Hold BOOTSEL and plug in your Pico 2
  2. Flash 0x0011_double-floating-point-data-type.uf2 to the RPI-RP2 drive
  3. Open your serial monitor

Expected output:

fav_num: 42.525250
fav_num: 42.525250
fav_num: 42.525250
...
Step 2: Create a New Ghidra Project
  1. Launch Ghidra: ghidraRun
  2. Click FileNew Project
  3. Select Non-Shared Project
  4. Project Name: week05-ex03-double-analysis
  5. Click Finish
Step 3: Import and Configure the Binary
  1. Drag and drop 0x0011_double-floating-point-data-type.bin into Ghidra
  2. Set Language: ARM Cortex 32 little endian default
  3. Click Options…
    • Block Name: .text
    • Base Address: 10000000
  4. Click OK on all dialogs
  5. Double-click the file and click Yes to analyze
Step 4: Locate and Rename Functions

Identify the main function and standard library calls:

Rename main:

  1. Click on FUN_10000238
  2. Right-click → Edit Function Signature
  3. Change to: int main(void)
  4. Click OK

Rename stdio_init_all:

  1. Click on FUN_10002f64
  2. Right-click → Edit Function Signature
  3. Change to: bool stdio_init_all(void)
  4. Click OK

Rename printf:

  1. Click on FUN_100030f4
  2. Right-click → Edit Function Signature
  3. Change to: int printf(char *format,...)
  4. Check the Varargs checkbox
  5. Click OK
Step 5: Observe the Decompiled Code

After renaming, the decompiled main should look like:

int main(void)
{
  undefined4 uVar1;
  undefined4 uVar2;
  undefined4 extraout_r1;
  undefined4 uVar3;
  undefined4 extraout_r1_00;
  
  uVar2 = DAT_10000258;
  uVar1 = DAT_10000254;
  stdio_init_all();
  uVar3 = extraout_r1;
  do {
    printf(DAT_10000250,uVar3,uVar1,uVar2);
    uVar3 = extraout_r1_00;
  } while( true );
}

Critical observation: There are now two non-zero data constants — DAT_10000254 and DAT_10000258. This is the key difference from the float exercise!

Step 6: Examine the Assembly Listing

Click on the Listing window and find the main function assembly:

10000238    push    {r3,r4,r5,lr}
1000023a    adr     r5, [0x10000254]
1000023c    ldrd    r4, r5, [r5, #0x0]    = 645A1CACh / 4045433Bh
10000240    bl      stdio_init_all

Key instruction: ldrd r4, r5, [r5, #0x0]

This is a load register double instruction — it loads two consecutive 32-bit words in a single instruction:

  • r4 gets the low word: 0x645A1CAC
  • r5 gets the high word: 0x4045433B

Later in the loop:

10000244    mov     r2, r4    ; r2 = 0x645A1CAC (low)
10000246    mov     r3, r5    ; r3 = 0x4045433B (high)
Step 7: Identify the Register Pair
Register Value Role
r2 0x645A1CAC Low 32 bits
r3 0x4045433B High 32 bits

Together: 0x4045433B645A1CAC

Compare to the float exercise:

  • Float 42.5: r2 = 0x00000000, r3 = 0x40454000 — low word is all zeros
  • Double 42.52525: r2 = 0x645A1CAC, r3 = 0x4045433Bboth words are non-zero!

This happens because 42.52525 has a repeating binary fraction that needs all 52 mantissa bits.

Step 8: Decode the Sign Bit

Convert r3 to binary and check bit 31:

r3 = 0x4045433B = 0100 0000 0100 0101 0100 0011 0011 1011
                   ^
                   bit 31 = 0  →  Positive number ✓
Step 9: Decode the Exponent

Extract bits 3020 from r3:

0x4045433B:  0  10000000100  01010100001100111011
             ^  ^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^
           sign  exponent      mantissa (top 20)

Exponent bits: 10000000100 = 2¹⁰ + 2² = 1024 + 4 = 1028

\text{real exponent} = 1028 - 1023 = \mathbf{5}
Step 10: Decode the Mantissa

High 20 bits (r3 bits 190):

0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1

Low 32 bits (all of r2 = 0x645A1CAC):

0 1 1 0 0 1 0 0 0 1 0 1 1 0 1 0 0 0 0 1 1 1 0 0 1 0 1 0 1 1 0 0

With the implied leading 1:

1.0101010000110011101101100100010110100001110010101100
Step 11: Reconstruct the Integer Part
1.0101010000110011..._2 \times 2^5

Shift the binary point 5 places right → 101010.1000011001110110...

Integer part 101010:

Bit Power Value
1 2⁵ 32
0 2⁴ 0
1 8
0 0
1 2
0 2⁰ 0
32 + 8 + 2 = \mathbf{42}
Step 12: Approximate the Fractional Part

The first few fractional bits .10000110011...:

Bit Power Decimal
1 2⁻¹ 0.5
0 2⁻² 0
0 2⁻³ 0
0 2⁻⁴ 0
0 2⁻⁵ 0
1 2⁻⁶ 0.015625
1 2⁻⁷ 0.0078125
0 2⁻⁸ 0
0 2⁻⁹ 0
1 2⁻¹⁰ 0.0009765625
1 2⁻¹¹ 0.00048828125

First 11 bits sum ≈ 0.5249. The remaining 36 fractional bits refine this to ≈ 0.52525.

42 + 0.52525 = \mathbf{42.52525} ✓
Step 13: Find the Format String

In the Listing view, locate:

s_fav_num:_%lf_100034b0    ds    "fav_num: %lf\r\n"

Note the %lf format specifier — this program explicitly uses double, unlike the float program which used %f.

Step 14: Document Your Findings
Item Float (42.5) Double (42.52525)
r2 (low word) 0x00000000 0x645A1CAC
r3 (high word) 0x40454000 0x4045433B
Low word is zero? Yes No
Words to patch 1 (r3 only) 2 (both r2 and r3)
Format specifier %f %lf
Assembly load instruction movs + ldr ldrd (load double)

Expected Output

After completing this exercise, you should understand:

  • How the compiler loads a 64-bit double using ldrd (load register double)
  • Why 42.52525 requires non-zero data in both registers while 42.5 does not
  • How to decode a complex IEEE 754 double with a repeating binary fraction
  • The differences between float and double handling at the assembly level

Questions for Reflection

Question 1: Why does 42.5 have a zero low word but 42.52525 does not?
Question 2: The assembly uses ldrd r4, r5, [r5, #0x0] instead of two separate ldr instructions. What is the advantage?
Question 3: Both the float and double programs have the same exponent (stored as 1028, real exponent 5). Why?
Question 4: If you were patching this double, how many data constants would you need to modify compared to the float exercise?

Tips and Hints

  • Use Python to verify: import struct; struct.pack('>d', 42.52525).hex() gives 4045433b645a1cac
  • The ldrd instruction always loads the lower-addressed word into the first register
  • A repeating binary fraction (like 0.52525) can never be represented exactly — double precision uses the closest 52-bit approximation
  • Compare data addresses: the float binary has one DAT_ constant; the double binary has two consecutive ones

Next Steps

  • Proceed to Exercise 4 to patch both register words
  • Compare the mantissa of 42.5 (clean: 010101 000...) vs. 42.52525 (complex: 0101010000110011...)
  • Think about what values would have a zero low word (hint: powers of 2, halves, quarters)

Additional Challenge

Using the 52-bit mantissa 0101010000110011101101100100010110100001110010101100, manually sum the first 20 fractional bits to see how close you get to 0.52525. How many bits of precision does it take to get within 0.001 of the true value?