mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-04-01 17:10:20 +02:00
278 lines
9.2 KiB
Markdown
278 lines
9.2 KiB
Markdown
# 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 30–20 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 19–0):
|
||
```
|
||
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?
|