mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-04-01 17:10:20 +02:00
272 lines
8.9 KiB
Markdown
272 lines
8.9 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 1: Analyze the Float Binary in Ghidra
|
||
|
||
#### Objective
|
||
Import and analyze the `0x000e_floating-point-data-type.bin` binary in Ghidra to understand how the compiler handles floating-point variables, discover float-to-double promotion, and decode the IEEE 754 double-precision encoding of `42.5` from two 32-bit registers.
|
||
|
||
#### Prerequisites
|
||
- Ghidra installed and configured
|
||
- `0x000e_floating-point-data-type.bin` binary available in your build directory
|
||
- Understanding of IEEE 754 encoding from Week 5 Part 2
|
||
- Basic Ghidra navigation skills from Weeks 3 and 4
|
||
|
||
#### Task Description
|
||
You will import the float binary into Ghidra, configure it for ARM Cortex-M33, resolve function names, discover that the compiler promotes `float` to `double` when passing to `printf`, and manually decode the 64-bit double `0x4045400000000000` field by field to confirm it represents `42.5`.
|
||
|
||
#### Step-by-Step Instructions
|
||
|
||
##### Step 1: Start Ghidra and Create New Project
|
||
|
||
```powershell
|
||
ghidraRun
|
||
```
|
||
|
||
1. Click **File** ? **New Project**
|
||
2. Select **Non-Shared Project**
|
||
3. Click **Next**
|
||
4. Enter Project Name: `week05-ex01-floating-point`
|
||
5. Choose a project directory
|
||
6. Click **Finish**
|
||
|
||
##### Step 2: Import the Binary
|
||
|
||
1. Navigate to your file explorer
|
||
2. Find `Embedded-Hacking\0x000e_floating-point-data-type\build\0x000e_floating-point-data-type.bin`
|
||
3. **Drag and drop** the `.bin` file into Ghidra's project window
|
||
|
||
##### Step 3: Configure Import Settings
|
||
|
||
When the import dialog appears:
|
||
|
||
1. Click the three dots (**…**) next to **Language**
|
||
2. Search for: `Cortex`
|
||
3. Select: **ARM Cortex 32 little endian default**
|
||
4. Click **OK**
|
||
|
||
Now click **Options…** button:
|
||
1. Change **Block Name** to: `.text`
|
||
2. Change **Base Address** to: `10000000` (XIP flash base)
|
||
3. Click **OK**
|
||
|
||
Then click **OK** on the main import dialog.
|
||
|
||
##### Step 4: Analyze the Binary
|
||
|
||
1. Double-click the imported file in the project window
|
||
2. When prompted "Analyze now?" click **Yes**
|
||
3. Leave all default analysis options selected
|
||
4. Click **Analyze**
|
||
5. Wait for analysis to complete (watch bottom-right progress bar)
|
||
|
||
##### Step 5: Locate and Rename the Main Function
|
||
|
||
Look at the **Symbol Tree** panel on the left. Expand **Functions**.
|
||
|
||
From previous weeks, we know the boot sequence leads to `main()`:
|
||
|
||
1. Click on `FUN_10000234`
|
||
2. Right-click ? **Edit Function Signature**
|
||
3. Change to: `int main(void)`
|
||
4. Click **OK**
|
||
|
||
##### Step 6: Resolve stdio_init_all and printf
|
||
|
||
**Rename stdio_init_all:**
|
||
1. Click on `FUN_10002f5c` in the decompile window
|
||
2. Right-click ? **Edit Function Signature**
|
||
3. Change to: `bool stdio_init_all(void)`
|
||
4. Click **OK**
|
||
|
||
**Rename printf:**
|
||
1. Click on `FUN_100030ec`
|
||
2. Right-click ? **Edit Function Signature**
|
||
3. Change to: `int __wrap_printf(char *format,...)`
|
||
4. Check the **Varargs** checkbox
|
||
5. Click **OK**
|
||
|
||
##### Step 7: Observe the Decompiled Code
|
||
|
||
After resolving, the decompiled `main` should look like:
|
||
|
||
```c
|
||
int main(void)
|
||
{
|
||
undefined4 uVar1;
|
||
undefined4 extraout_r1;
|
||
undefined4 uVar2;
|
||
undefined4 extraout_r1_00;
|
||
|
||
stdio_init_all();
|
||
uVar1 = DAT_1000024c;
|
||
uVar2 = extraout_r1;
|
||
do {
|
||
__wrap_printf(DAT_10000250,uVar2,0,uVar1);
|
||
uVar2 = extraout_r1_00;
|
||
} while( true );
|
||
}
|
||
```
|
||
|
||
**Critical observation:** Where is `float fav_num = 42.5`? The compiler optimized it into constants!
|
||
|
||
##### Step 8: Identify the Register Pair
|
||
|
||
Look at the **Listing** window (assembly view) for `main`:
|
||
|
||
```assembly
|
||
1000023a 00 24 movs r4, #0x0
|
||
1000023c 03 4d ldr r5, [DAT_1000024c] = 40454000h
|
||
```
|
||
|
||
Two values are being passed to `printf`:
|
||
- `r2 = 0x00000000` (low 32 bits)
|
||
- `r3 = 0x40454000` (high 32 bits)
|
||
|
||
Together they form a 64-bit double: `0x4045400000000000`
|
||
|
||
**Why a double?** The C standard requires that `float` arguments to variadic functions like `printf` are **promoted to `double`**. So even though our variable is declared as `float fav_num = 42.5`, `printf` always receives a 64-bit double.
|
||
|
||
##### Step 9: Write Out the Binary Layout
|
||
|
||
Convert both registers to binary:
|
||
|
||
```
|
||
r3 (high 32 bits): 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
|
||
r2 (low 32 bits): 0x00000000 = 0000 0000 0000 0000 0000 0000 0000 0000
|
||
```
|
||
|
||
Map the 64-bit IEEE 754 fields:
|
||
|
||
```
|
||
Bit 63 (sign): 0
|
||
Bits 62–52 (exponent): 10000000100
|
||
Bits 51–0 (mantissa): 0101010000000000...0000
|
||
```
|
||
|
||
##### Step 10: Decode the Sign Bit
|
||
|
||
Bit 63 of the double = bit 31 of r3:
|
||
|
||
```
|
||
r3 = 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
|
||
^
|
||
bit 31 = 0 ? Positive number
|
||
```
|
||
|
||
IEEE 754 sign rule: `0` = Positive, `1` = Negative.
|
||
|
||
##### Step 11: Decode the Exponent
|
||
|
||
Extract bits 30–20 from r3:
|
||
|
||
```
|
||
0x40454000: 0 10000000100 01010100000000000000
|
||
^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
|
||
sign exponent mantissa (top 20)
|
||
```
|
||
|
||
Exponent bits: `10000000100` = 2¹° + 2² = 1024 + 4 = **1028**
|
||
|
||
Subtract the double-precision bias (1023):
|
||
|
||
$$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
|
||
|
||
##### Step 12: Decode the Mantissa
|
||
|
||
High 20 bits of mantissa (from r3 bits 19–0):
|
||
```
|
||
0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||
```
|
||
|
||
Low 32 bits of mantissa (from r2):
|
||
```
|
||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||
```
|
||
|
||
With the implied leading `1`:
|
||
```
|
||
1.010101 00000...
|
||
```
|
||
|
||
##### Step 13: Reconstruct the Final Value
|
||
|
||
$$1.010101_2 \times 2^5 = 101010.1_2$$
|
||
|
||
Convert to decimal:
|
||
|
||
| Bit | Power | Value |
|
||
|-----|-------|-------|
|
||
| 1 | 25 | 32 |
|
||
| 0 | 24 | 0 |
|
||
| 1 | 2³ | 8 |
|
||
| 0 | 2² | 0 |
|
||
| 1 | 2¹ | 2 |
|
||
| 0 | 2° | 0 |
|
||
| 1 | 2?¹ | 0.5 |
|
||
|
||
$$32 + 8 + 2 + 0.5 = \mathbf{42.5} ?$$
|
||
|
||
##### Step 14: Find the Format String
|
||
|
||
In the Listing view, click on the data reference to locate:
|
||
|
||
```
|
||
s_fav_num:_%f_100034a8 ds "fav_num: %f\r\n"
|
||
```
|
||
|
||
Note that the format specifier is `%f`, confirming this is a floating-point print call.
|
||
|
||
##### Step 15: Document Your Findings
|
||
|
||
Create a table of your observations:
|
||
|
||
| Item | Value | Notes |
|
||
| ------------------------- | ------------------ | ---------------------------------- |
|
||
| Main function address | `0x10000234` | Entry point of program |
|
||
| Float value (original) | `42.5` | Declared as `float` |
|
||
| Double hex encoding | `0x4045400000000000` | Promoted to double for printf |
|
||
| r3 (high word) | `0x40454000` | Contains sign + exponent + mantissa top bits |
|
||
| r2 (low word) | `0x00000000` | All zeros — clean fractional part |
|
||
| Exponent (stored) | 1028 | Biased value |
|
||
| Exponent (real) | 5 | After subtracting bias 1023 |
|
||
| Format string | `"fav_num: %f\r\n"` | Located at `0x100034a8` |
|
||
| Float-to-double promotion | Yes | C standard for variadic functions |
|
||
|
||
#### Expected Output
|
||
|
||
After completing this exercise, you should be able to:
|
||
- Import and configure ARM binaries in Ghidra for float analysis
|
||
- Explain why `printf` receives a `double` even when the variable is a `float`
|
||
- Identify the register pair `r2:r3` that holds a 64-bit double
|
||
- Manually decode an IEEE 754 double from hex to decimal
|
||
- Locate format strings in the binary
|
||
|
||
#### Questions for Reflection
|
||
|
||
###### Question 1: Why does the compiler promote a `float` to a `double` when passing it to `printf`?
|
||
|
||
###### Question 2: The low word (`r2`) is `0x00000000`. What does this tell you about the fractional part of `42.5`?
|
||
|
||
###### Question 3: What is the purpose of the exponent bias (1023) in IEEE 754 double-precision?
|
||
|
||
###### Question 4: If the sign bit (bit 63) were `1` instead of `0`, what value would the double represent?
|
||
|
||
#### Tips and Hints
|
||
- The **Listing** window shows raw assembly; the **Decompile** window shows reconstructed C
|
||
- Double-click on a `DAT_` reference to jump to the data constant
|
||
- Use Python to verify: `import struct; struct.pack('>d', 42.5).hex()` gives `4045400000000000`
|
||
- Remember: r3 = high 32 bits (sign + exponent + top mantissa), r2 = low 32 bits (bottom mantissa)
|
||
- The bias for doubles is always 1023; for floats it's 127
|
||
|
||
#### Next Steps
|
||
- Proceed to Exercise 2 to patch this float value in Ghidra
|
||
- Try computing the IEEE 754 encoding of other values like `3.14` or `100.0` by hand
|
||
- Compare the 32-bit float encoding `0x422A0000` with the 64-bit double encoding `0x4045400000000000` — both represent `42.5`
|
||
|
||
#### Additional Challenge
|
||
Find the data constant `DAT_1000024c` in the Listing view. What raw bytes are stored there? Remember that ARM is little-endian — the bytes in memory are in reverse order. Write out the byte order as it appears in memory vs. as a 32-bit value.
|