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

278 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Embedded Systems Reverse Engineering
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
## 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 **File****New 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:
```c
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:
```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:
```assembly
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 = `0x4045433B`**both 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 | 2³ | 8 |
| 0 | 2² | 0 |
| 1 | 2¹ | 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?