Added WEEK04

This commit is contained in:
Kevin Thomas
2026-01-31 14:07:15 -05:00
parent 021ca960c6
commit 29073bd383
6 changed files with 2044 additions and 0 deletions
+10
View File
@@ -102,6 +102,16 @@ This chapter covers a comprehensive embedded system analysis reviewing parts of
## Week 4 ## Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
### Week 4 Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04.md)
#### Exercise 1: Analyze Variable Storage in Ghidra [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04-01.md)
#### Exercise 2: Patch Binary to Change Variable Value [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04-02.md)
#### Exercise 3: Analyze and Understand GPIO Control [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04-03.md)
#### Exercise 4: Patch GPIO Binary to Change LED Pin [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04-04.md)
### Chapter 5: Intro To Variables ### Chapter 5: Intro To Variables
This chapter covers an introduction to variables as it relates to embedded development on the Pico 2. This chapter covers an introduction to variables as it relates to embedded development on the Pico 2.
+241
View File
@@ -0,0 +1,241 @@
# Embedded Systems Reverse Engineering
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
## Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
### Exercise 1: Analyze Variable Storage in Ghidra
#### Objective
Import and analyze the `0x0005_intro-to-variables.bin` binary in Ghidra to understand where variables are stored, identify memory sections, and trace how the compiler optimizes variable usage.
#### Prerequisites
- Ghidra installed and configured
- `0x0005_intro-to-variables.bin` binary available in your build directory
- Understanding of memory sections (`.data`, `.bss`, `.rodata`) from Week 4 Part 2
- Basic Ghidra navigation skills from Week 3
#### Task Description
You will import the binary into Ghidra, configure it for ARM Cortex-M33, analyze the code structure, resolve function names, and locate where the `age` variable is used in the compiled binary.
#### Step-by-Step Instructions
##### Step 1: Start Ghidra and Create New Project
```bash
ghidraRun
```
1. Click **File****New Project**
2. Select **Non-Shared Project**
3. Click **Next**
4. Enter Project Name: `week04-ex01-intro-to-variables`
5. Choose a project directory
6. Click **Finish**
##### Step 2: Import the Binary
1. Navigate to your file explorer
2. Find `Embedded-Hacking/0x0005_intro-to-variables/build/0x0005_intro-to-variables.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: Navigate to the Symbol Tree
Look at the left panel for the **Symbol Tree**. Expand **Functions** to see the auto-detected functions:
You should see function names like:
- `FUN_1000019a`
- `FUN_10000210`
- `FUN_10000234`
- Many more...
These are auto-generated names because we're analyzing a raw binary without debug symbols.
##### Step 6: Identify the Main Function
From Week 3, we know the typical boot sequence:
1. Reset handler copies data
2. `frame_dummy` runs
3. `main()` is called
Click on `FUN_10000234` - this should be our `main()` function.
**Look at the Decompile window:**
```c
void FUN_10000234(void)
{
FUN_100030cc();
do {
FUN_10003100("age: %d\r\n", 0x2b);
} while (true);
}
```
**Observations:**
- `FUN_100030cc()` is likely `stdio_init_all()`
- `FUN_10003100()` is likely `printf()`
- The magic value `0x2b` appears (what is this?)
##### Step 7: Convert 0x2b to Decimal
Let's figure out what `0x2b` means:
**Manual calculation:**
- `0x2b` in hexadecimal
- `2 × 16 + 11 = 32 + 11 = 43` in decimal
**In GDB (alternative method):**
```gdb
(gdb) p/d 0x2b
$1 = 43
```
So `0x2b = 43`! This matches our `age = 43` from the source code!
##### Step 8: Rename Functions for Clarity
Let's rename the functions to their actual names:
**Rename FUN_10000234 to main:**
1. Right-click on `FUN_10000234` in the Symbol Tree
2. Select **Rename Function**
3. Enter: `main`
4. Press **Enter**
**Update main's signature:**
1. In the Decompile window, right-click on `main`
2. Select **Edit Function Signature**
3. Change to: `int main(void)`
4. Click **OK**
**Rename FUN_100030cc to stdio_init_all:**
1. Click on `FUN_100030cc` in the decompile window
2. Right-click → **Edit Function Signature**
3. Change name to: `stdio_init_all`
4. Change signature to: `bool stdio_init_all(void)`
5. Click **OK**
**Rename FUN_10003100 to printf:**
1. Click on `FUN_10003100`
2. Right-click → **Edit Function Signature**
3. Change name to: `printf`
4. Check the **Varargs** checkbox (printf accepts variable arguments)
5. Click **OK**
##### Step 9: Examine the Optimized Code
After renaming, the decompiled main should now look like:
```c
int main(void)
{
stdio_init_all();
do {
printf("age: %d\r\n", 0x2b);
} while (true);
}
```
**Critical observation:** Where did our `age` variable go?
Original source code:
```c
uint8_t age = 42;
age = 43;
```
The compiler **optimized it completely away**!
**Why?**
1. `age = 42` is immediately overwritten
2. The value `42` is never used
3. The compiler replaces `age` with the constant `43` (`0x2b`)
4. No variable allocation in memory is needed!
##### Step 10: Examine the Assembly Listing
Click on the **Listing** window (shows assembly code):
Find the instruction that loads `0x2b`:
```assembly
10000xxx movs r1, #0x2b
10000xxx ...
10000xxx bl printf
```
**What this does:**
- `movs r1, #0x2b` - Moves the immediate value 0x2b (43) into register r1
- `bl printf` - Branches to printf, which expects format args in r1+
##### Step 11: Document Your Findings
Create a table of your observations:
| Item | Value/Location | Notes |
| --------------------- | -------------- | ------------------------------- |
| Main function address | `0x10000234` | Entry point of program |
| Age value (hex) | `0x2b` | Optimized constant |
| Age value (decimal) | `43` | Original variable value |
| Variable in memory? | No | Compiler optimized it away |
| Printf address | `0x10003100` | Standard library function |
| Format string | "age: %d\r\n" | Located in .rodata section |
#### Expected Output
After completing this exercise, you should be able to:
- Successfully import and configure ARM binaries in Ghidra
- Navigate the Symbol Tree and identify functions
- Understand how compiler optimization removes unnecessary variables
- Convert hexadecimal values to decimal
- Rename functions for better code readability
#### Questions for Reflection
###### Question 1: Why did the compiler optimize away the `age` variable?
###### Question 2: In what memory section would `age` have been stored if it wasn't optimized away?
###### Question 3: Where is the string "age: %d\r\n" stored, and why can't it be in RAM?
###### Question 4: What would happen if we had used `age` in a calculation before reassigning it to 43?
#### Tips and Hints
- Use **CTRL+F** in Ghidra to search for specific values or strings
- The **Data Type Manager** window shows all recognized data types
- If Ghidra's decompiler output looks wrong, try re-analyzing with different options
- Remember: optimized code often looks very different from source code
- The **Display****Function Graph** shows control flow visually
#### Next Steps
- Proceed to Exercise 2 to learn binary patching
- Try analyzing `0x0008_uninitialized-variables.bin` to see how uninitialized variables behave
- Explore the `.rodata` section to find string literals
#### Additional Challenge
Find the format string "age: %d\r\n" in Ghidra. What address is it stored at? How does the program reference this string in the assembly code? (Hint: Look for an `ldr` instruction that loads the string address into a register.)
+220
View File
@@ -0,0 +1,220 @@
# Embedded Systems Reverse Engineering
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
## Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
### Exercise 2: Patch Binary to Change Variable Value
#### Objective
Use Ghidra's patching capabilities to modify the compiled binary, changing the value printed by the program from 43 to a different value of your choice, then convert and flash the modified binary to the Pico 2.
#### Prerequisites
- Completed Exercise 1 (binary imported and analyzed in Ghidra)
- Python installed for UF2 conversion
- `uf2conv.py` script available in your project directory
- Raspberry Pi Pico 2 connected via USB
- Serial monitor software (PuTTY, minicom, or screen)
#### Task Description
You will locate the instruction that loads the value 43 into a register, patch it to use a different value (70 in this example), export the modified binary, convert it to UF2 format, and flash it to verify the change.
#### Step-by-Step Instructions
##### Step 1: Open Your Ghidra Project
If you closed Ghidra from Exercise 1:
1. Launch `ghidraRun`
2. Open the project: `week04-ex01-intro-to-variables`
3. Double-click the binary file to open it
##### Step 2: Navigate to the Value Load Instruction
In Exercise 1, we found that `main()` calls `printf("age: %d\r\n", 0x2b)`.
**Find the assembly instruction:**
1. Click on the `main` function in the Symbol Tree
2. Look at the **Listing** window (assembly view)
3. Find the line with `movs r1, #0x2b`
The instruction should look like:
```assembly
10000xxx 21 2b movs r1, #0x2b
```
**What this instruction means:**
- Opcode: `21 2b` (encoded instruction bytes)
- Mnemonic: `movs r1, #0x2b`
- Operation: Move the immediate value 0x2b into register r1
- Register r1 will be used as the argument to printf
##### Step 3: Choose Your New Value
Let's change the value from `43` (0x2b) to `70` (0x46).
**Convert 70 to hexadecimal:**
- 70 ÷ 16 = 4 remainder 6
- Therefore: 70 decimal = 0x46 hexadecimal
You can verify this in Python:
```python
>>> hex(70)
'0x46'
>>> 0x46
70
```
##### Step 4: Patch the Instruction
Now we'll modify the binary:
1. **Right-click** on the instruction `movs r1, #0x2b`
2. Select **Patch Instruction**
3. A dialog appears showing the current instruction
4. Change `#0x2b` to `#0x46`
5. Press **Enter** or click **OK**
The instruction now reads:
```assembly
10000xxx 21 46 movs r1, #0x46
```
**Visual confirmation:**
- The patched instruction should be highlighted (usually in red or orange)
- The bytes should change from `21 2b` to `21 46`
- The decompiled view should update to show `printf("age: %d\r\n", 0x46);`
##### Step 5: Verify the Patch in Decompile Window
Click on the `main` function again and check the **Decompile** window:
```c
int main(void)
{
stdio_init_all();
do {
printf("age: %d\r\n", 0x46); // Changed from 0x2b!
} while (true);
}
```
Perfect! The decompiler recognized our patch.
##### Step 6: Export the Patched Binary
Now we need to save the modified binary:
1. Click **File****Export Program**
2. Set **Format** to: **Binary**
3. Navigate to your build directory:
- `Embedded-Hacking/0x0005_intro-to-variables/build/`
4. Set **Filename** to: `0x0005_intro-to-variables-h.bin`
- The `-h` suffix means "hacked"
5. Click **OK**
**Important:** Make sure you're exporting to the correct location!
##### Step 7: Convert to UF2 Format
The Pico 2 requires UF2 format. Open a terminal and run:
**Navigate to the project directory:**
```powershell
cd C:\path\to\Embedded-Hacking\0x0005_intro-to-variables
```
**Run the conversion:**
```powershell
python ..\uf2conv.py build\0x0005_intro-to-variables-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
```
**Command breakdown:**
- `uf2conv.py` - The conversion script
- `--base 0x10000000` - XIP flash base address (where code runs from)
- `--family 0xe48bff59` - RP2350 family ID
- `--output build\hacked.uf2` - Output filename
**Expected output:**
```
Converting to uf2, output size: 57856, start address: 0x10000000
Wrote 57856 bytes to build\hacked.uf2
```
##### Step 8: Flash the Hacked Binary
**Enter bootloader mode:**
1. Disconnect your Pico 2 from USB
2. Hold down the **BOOTSEL** button
3. While holding BOOTSEL, plug in the USB cable
4. Release BOOTSEL
5. A drive called **RPI-RP2** appears
**Flash the binary:**
1. Open the RPI-RP2 drive
2. Drag and drop `build\hacked.uf2` onto the drive
3. The Pico will automatically reboot
##### Step 9: Verify the Changes
**Open your serial monitor:**
For PuTTY:
1. Select **Serial** connection type
2. Set the COM port (check Device Manager)
3. Set speed to **115200**
4. Click **Open**
For PowerShell:
```powershell
# Find the COM port
Get-PnpDevice -Class Ports | Where-Object {$_.FriendlyName -like "*USB Serial*"}
# Connect (replace COM3 with your port)
$port = new-Object System.IO.Ports.SerialPort COM3,115200,None,8,one
$port.Open()
while($true) { $port.ReadLine() }
```
**Expected output:**
```
age: 70
age: 70
age: 70
age: 70
...
```
🎉 **Success!** You've successfully patched a binary and changed its behavior!
#### Expected Output
After completing this exercise, you should:
- See `age: 70` printing instead of `age: 43`
- Have a patched binary file (`0x0005_intro-to-variables-h.bin`)
- Have a UF2 file (`hacked.uf2`)
- Understand the complete patching workflow
#### Questions for Reflection
###### Question 1: Why do we need to convert to UF2 format instead of flashing the raw .bin file?
###### Question 2: What is the significance of the base address 0x10000000 in the conversion command?
###### Question 3: What would happen if you patched the wrong instruction by mistake?
###### Question 4: How can you verify a patch was applied correctly before exporting?
#### Tips and Hints
- Always make a backup of the original binary before patching
- Use descriptive names like `-h` (hacked) or `-patch` for modified binaries
- Test your patches on hardware to ensure they work as expected
- If something goes wrong, you can always flash the original UF2 file
- Use `File → Export Program → Original File` to revert all patches
#### Next Steps
- Try patching to different values (100, 255, etc.)
- Proceed to Exercise 3 to learn about GPIO control
- Experiment with patching multiple values in the same binary
#### Additional Challenge
Instead of changing the value to 70, change it to 255 (0xFF) - the maximum value for a `uint8_t`. What do you observe? Now try changing it to 256 (0x100). What happens and why? (Hint: Think about the size limits of the instruction encoding.)
+270
View File
@@ -0,0 +1,270 @@
# Embedded Systems Reverse Engineering
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
## Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
### Exercise 3: Analyze and Understand GPIO Control
#### Objective
Import the `0x0008_uninitialized-variables.bin` binary, analyze the GPIO initialization and control sequences, understand how `gpio_init()`, `gpio_set_dir()`, and `gpio_put()` work at the assembly level.
#### Prerequisites
- Completed Exercises 1 and 2
- Understanding of GPIO basics from Week 4 Part 3
- Raspberry Pi Pico 2 with an LED connected to GPIO 16
- Basic knowledge of ARM Thumb-2 instruction set
#### Task Description
You will import a new binary that controls GPIO pins, identify the GPIO-related function calls, trace the initialization sequence, and understand how the Pico SDK controls hardware at the low level.
#### Step-by-Step Instructions
##### Step 1: Flash the Original Binary
Before analysis, let's see what the program does:
1. Hold **BOOTSEL** and plug in your Pico 2
2. Flash `0x0008_uninitialized-variables.uf2` to the RPI-RP2 drive
3. Open your serial monitor
**Expected output:**
```
age: 0
age: 0
age: 0
...
```
**Expected behavior:**
- The red LED on GPIO 16 blinks on/off every 500ms
- The value `0` is printed (uninitialized variable)
##### Step 2: Create a New Ghidra Project
1. Launch Ghidra: `ghidraRun`
2. Click **File****New Project**
3. Select **Non-Shared Project**
4. Project Name: `week04-ex03-gpio-analysis`
5. Click **Finish**
##### Step 3: Import the GPIO Binary
1. Drag and drop `0x0008_uninitialized-variables.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: Identify the Main Function
Look for the main function (likely `FUN_10000234` or similar):
**In the Symbol Tree:**
1. Expand **Functions**
2. Look for a function that appears to be an entry point
3. Click on potential `main` candidates
**Look for these patterns in the decompile:**
- Call to `stdio_init_all()`
- Call to `gpio_init()`
- Infinite while loop with `gpio_put()` and `sleep_ms()`
##### Step 5: Rename the Main Function
Once you identify `main`:
1. Right-click on the function name
2. Select **Edit Function Signature**
3. Change to: `int main(void)`
4. Click **OK**
**Expected decompiled code structure:**
```c
int main(void)
{
// Some initial value
stdio_init_all();
gpio_init(0x10); // GPIO 16
// ... more GPIO setup
while (true) {
printf(...);
gpio_put(0x10, 1);
sleep_ms(0x1f4);
gpio_put(0x10, 0);
sleep_ms(0x1f4);
}
}
```
##### Step 6: Identify GPIO Function Calls
Look in the decompiled main for function calls. You should see several undefined functions.
**Find and rename these GPIO functions:**
| Auto-Generated Name | Actual Function | How to Identify |
| ------------------- | ----------------- | ------------------------------------------ |
| `FUN_xxxxx` | `gpio_init` | Takes one parameter (pin number) |
| `FUN_xxxxx` | `gpio_set_dir` | Takes two parameters (pin, direction) |
| `FUN_xxxxx` | `gpio_put` | Takes two parameters (pin, value) |
| `FUN_xxxxx` | `sleep_ms` | Takes one parameter (milliseconds) |
| `FUN_xxxxx` | `stdio_init_all` | Takes no parameters, called first |
| `FUN_xxxxx` | `printf` | Takes variable args, has format string |
**Example renaming gpio_init:**
1. Click on the function call in the decompile window
2. Right-click → **Edit Function Signature**
3. Change name to: `gpio_init`
4. Set signature to: `void gpio_init(uint gpio)`
5. Click **OK**
##### Step 7: Analyze GPIO Initialization Sequence
After renaming, your decompiled main should look clearer:
```c
int main(void)
{
stdio_init_all();
gpio_init(0x10); // Initialize GPIO 16
gpio_set_dir(0x10, 1); // Set as output (1 = GPIO_OUT)
while (true) {
printf("age: %d\r\n", 0);
gpio_put(0x10, 1); // LED ON
sleep_ms(0x1f4); // Wait 500ms (0x1f4 = 500)
gpio_put(0x10, 0); // LED OFF
sleep_ms(0x1f4); // Wait 500ms
}
}
```
**Key observations:**
- `0x10` is hexadecimal for 16 (GPIO 16 - red LED)
- `0x1f4` is hexadecimal for 500 (milliseconds)
- `1` means GPIO_OUT (output direction)
- The LED is controlled by toggling between 1 (on) and 0 (off)
##### Step 8: Examine gpio_init Assembly
Double-click on `gpio_init` to jump to its implementation.
**Look for these key operations in the assembly:**
```assembly
; Load GPIO pin number into register
movs r4, r0 ; Save pin number
; Calculate pad register address
; Base address: 0x40038000 (PADS_BANK0)
; Offset: pin * 4
ldr r3, =0x40038000
lsls r5, r4, #2 ; pin * 4
add r3, r5 ; Calculate address
; Configure pad (clear OD bit, set IE bit)
ldr r2, [r3] ; Read current config
bic r2, #0x80 ; Clear output disable
orr r2, #0x40 ; Set input enable
str r2, [r3] ; Write back
; Set GPIO function to SIO (0x05)
ldr r3, =0x40028000 ; IO_BANK0 base
add r3, r5 ; Add offset
movs r2, #5 ; FUNCSEL = SIO
str r2, [r3] ; Set function
```
**What this does:**
1. Configures the GPIO pad registers (physical pin properties)
2. Sets the GPIO function to SIO (Software I/O)
3. Prepares the pin for software control
##### Step 9: Examine gpio_put Assembly
Find the `gpio_put` function and examine its implementation.
**Look for the GPIO coprocessor instruction:**
```assembly
gpio_put:
movs r4, r0 ; GPIO pin number
movs r5, r1 ; Value (0 or 1)
; Use ARM coprocessor to control GPIO
mcrr p0, #4, r4, r5, c0
bx lr ; Return
```
**Critical instruction: `mcrr p0, #4, r4, r5, c0`**
- `mcrr` = Move to Coprocessor from two ARM Registers
- `p0` = Coprocessor 0 (GPIO coprocessor in RP2350)
- `#4` = Operation code
- `r4, r5` = Source registers (pin number, value)
- `c0` = Coprocessor register (GPIO output control)
This is a **single-cycle GPIO operation** - extremely fast!
##### Step 10: Document the GPIO Memory Map
Create a reference table of the addresses you found:
| Address | Register | Purpose |
| ------------ | ------------- | ------------------------------- |
| `0x40028000` | IO_BANK0 | GPIO function selection |
| `0x40038000` | PADS_BANK0 | GPIO pad configuration |
| `0xd0000000` | SIO | Single-cycle I/O (coprocessor) |
**GPIO 16 specific addresses:**
- Pad control: `0x40038000 + (16 * 4) = 0x40038040`
- Function select: `0x40028000 + (16 * 4) = 0x40028040`
##### Step 11: Trace the Blink Timing
Calculate the actual timing:
**sleep_ms(0x1f4):**
- Convert: 0x1f4 = (1 × 256) + (15 × 16) + 4 = 256 + 240 + 4 = 500 decimal
- So the LED is on for 500ms, off for 500ms
- Total cycle time: 1000ms = 1 second
- Blink rate: 1 Hz
#### Expected Output
After completing this exercise, you should understand:
- How GPIO initialization configures hardware registers
- The role of the GPIO coprocessor in the RP2350
- How `gpio_put()` uses a single ARM instruction for fast I/O
- The memory-mapped addresses for GPIO control
- How timing delays are implemented with `sleep_ms()`
#### Questions for Reflection
###### Question 1: Why does gpio_init() need to configure both PADS_BANK0 and IO_BANK0 registers?
###### Question 2: What is the advantage of using the GPIO coprocessor instruction (`mcrr`) instead of writing to memory-mapped registers?
###### Question 3: If you wanted to blink the LED at 10 Hz instead of 1 Hz, what value should `sleep_ms()` use?
###### Question 4: What would happen if you called `gpio_put()` on a pin that hasn't been initialized with `gpio_init()` first?
#### Tips and Hints
- Use Ghidra's **References** feature (right-click → Find References) to see where functions are called
- The **Display****Memory Map** shows all memory regions
- Look for bit manipulation instructions (`bic`, `orr`) to understand register configuration
- The ARM Architecture Reference Manual has complete documentation on coprocessor instructions
- Use hex-to-decimal converters online if you're unsure about conversions
#### Next Steps
- Proceed to Exercise 4 to patch the GPIO binary
- Try to identify other SDK functions like `gpio_get()` if they appear
- Explore the full GPIO initialization in the SDK source code
#### Additional Challenge
Find the `gpio_set_dir()` function in Ghidra. Does it also use a GPIO coprocessor instruction? What coprocessor register does it use (c0, c4, or something else)? Compare its implementation to `gpio_put()` and document the differences.
+345
View File
@@ -0,0 +1,345 @@
# Embedded Systems Reverse Engineering
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
## Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
### Exercise 4: Patch GPIO Binary to Change LED Pin
#### Objective
Patch the `0x0008_uninitialized-variables.bin` binary to change which LED blinks, modify the printed value, and change the blink timing, then verify all changes work correctly on hardware.
#### Prerequisites
- Completed Exercise 3 (GPIO binary analyzed in Ghidra)
- Understanding of how GPIO pin numbers are encoded
- Knowledge of hexadecimal-to-decimal conversion
- Pico 2 with ability to test multiple GPIO pins
#### Task Description
You will locate all instances where GPIO 16 is used, patch them to GPIO 17 (changing from red LED to green LED), modify the printed value from 0 to 66, and adjust the blink timing from 500ms to 100ms for faster blinking.
#### Step-by-Step Instructions
##### Step 1: Plan Your Patches
Before we start patching, let's identify what needs to change:
| Current Value | New Value | Description | Hex Conversion |
| ------------- | --------- | --------------------- | ------------------- |
| GPIO 16 | GPIO 17 | Change LED pin | 0x10 → 0x11 |
| age = 0 | age = 66 | Change printed value | 0x00 → 0x42 |
| 500ms | 100ms | Change blink timing | 0x1f4 → 0x64 |
**Verify the hex conversions:**
- 17 decimal = 0x11 hex ✓
- 66 decimal = 0x42 hex ✓
- 100 decimal = 0x64 hex ✓
##### Step 2: Open the GPIO Project in Ghidra
1. Launch Ghidra and open `week04-ex03-gpio-analysis`
2. Double-click the binary to open the CodeBrowser
3. Navigate to the `main` function
**Review the decompiled code:**
```c
int main(void)
{
stdio_init_all();
gpio_init(0x10);
gpio_set_dir(0x10, 1);
while (true) {
printf("age: %d\r\n", 0);
gpio_put(0x10, 1);
sleep_ms(0x1f4);
gpio_put(0x10, 0);
sleep_ms(0x1f4);
}
}
```
##### Step 3: Find and Patch gpio_init Parameter
Look at the **Listing** window (assembly view) for the main function.
**Find the gpio_init call:**
```assembly
1000023a 10 20 movs r0, #0x10
1000023c xx xx bl gpio_init
```
**Patch instruction:**
1. Right-click on `movs r0, #0x10`
2. Select **Patch Instruction**
3. Change `#0x10` to `#0x11`
4. Press **Enter**
**Result:**
```assembly
1000023a 11 20 movs r0, #0x11
```
The instruction bytes change from `10 20` to `11 20`.
##### Step 4: Find and Patch gpio_set_dir Parameter
Continue down the assembly listing:
```assembly
10000240 10 23 movs r3, #0x10
10000242 01 22 movs r2, #1
10000244 xx xx bl gpio_set_dir
```
**Patch the r3 load:**
1. Right-click on `movs r3, #0x10`
2. Select **Patch Instruction**
3. Change to `#0x11`
4. Press **Enter**
**Why r3 instead of r0?** The SDK might pass GPIO pin as the first parameter differently, or this could be due to register allocation. Trust the analysis!
##### Step 5: Find All gpio_put Calls
Inside the while loop, there are two `gpio_put` calls:
**First gpio_put (LED ON):**
```assembly
10000252 10 24 movs r4, #0x10
10000254 01 25 movs r5, #1
10000256 xx xx bl gpio_put
```
**Patch:**
1. Right-click on `movs r4, #0x10`
2. Change to `#0x11`
**Second gpio_put (LED OFF):**
```assembly
1000025e 10 24 movs r4, #0x10
10000260 00 25 movs r5, #0
10000262 xx xx bl gpio_put
```
**Patch:**
1. Right-click on `movs r4, #0x10`
2. Change to `#0x11`
**Note:** The exact addresses will vary, but the pattern is consistent.
##### Step 6: Patch the Printed Value
Find the printf call in the loop:
```assembly
1000024a 00 21 movs r1, #0x0
1000024c xx xx ldr r0, [pc, #offset]
1000024e xx xx bl printf
```
**Patch the value:**
1. Right-click on `movs r1, #0x0`
2. Select **Patch Instruction**
3. Change to `#0x42` (66 in decimal)
4. Press **Enter**
**Result:**
```assembly
1000024a 42 21 movs r1, #0x42
```
##### Step 7: Patch the Sleep Timing (First)
Find the first `sleep_ms(0x1f4)` call:
```assembly
10000258 f4 21 movs r1, #0xf4
1000025a 01 20 movs r0, #1
1000025c 00 04 lsls r0, r0, #16
1000025e 08 44 add r0, r1
10000260 xx xx bl sleep_ms
```
**Wait, this looks complex!** The value 0x1f4 (500) is being constructed:
- Load 1 into r0
- Shift left 16 bits: 1 << 16 = 0x10000
- Load 0xf4 into r1
- Add them: 0x10000 + 0xf4 = 0x1f4
**Alternative pattern (simpler):**
```assembly
10000xxx f4 20 movs r0, #0xf4
10000xxx 01 20 movs r1, #0x01
10000xxx ...
```
**For 100ms (0x64):**
Simply find where 0x1f4 is loaded and change it to 0x64.
**If the instruction is:**
```assembly
movs r0, #0x1f4
```
**Change to:**
```assembly
movs r0, #0x64
```
**Note:** The exact encoding depends on the instruction. For immediate values > 255, Thumb-2 uses different encodings.
##### Step 8: Handle Large Immediate Values
If `sleep_ms(500)` uses a multi-instruction sequence to load 0x1f4, you have two options:
**Option A: Patch both instructions**
If it's loading 0x1f4 as (0x100 + 0xf4):
1. Find where 0xf4 is loaded
2. Change it to 0x64
3. Find where 0x1 is loaded for the high byte
4. Change it to 0x0
**Option B: Simplify to single instruction**
Since 0x64 (100) fits in an 8-bit immediate, you can replace the multi-instruction sequence with:
```assembly
movs r0, #0x64
nop
nop
```
##### Step 9: Verify All Patches
Check the **Decompile** window to confirm changes:
```c
int main(void)
{
stdio_init_all();
gpio_init(0x11); // Changed from 0x10!
gpio_set_dir(0x11, 1);
while (true) {
printf("age: %d\r\n", 0x42); // Changed from 0!
gpio_put(0x11, 1); // Changed from 0x10!
sleep_ms(0x64); // Changed from 0x1f4!
gpio_put(0x11, 0); // Changed from 0x10!
sleep_ms(0x64); // Changed from 0x1f4!
}
}
```
Perfect! All changes are reflected.
##### Step 10: Export the Patched Binary
1. Click **File****Export Program**
2. Set Format: **Binary**
3. Navigate to: `Embedded-Hacking/0x0008_uninitialized-variables/build/`
4. Filename: `0x0008_uninitialized-variables-h.bin`
5. Click **OK**
##### Step 11: Convert to UF2
Open PowerShell and navigate to the project:
```powershell
cd C:\path\to\Embedded-Hacking\0x0008_uninitialized-variables
```
**Run conversion:**
```powershell
python ..\uf2conv.py build\0x0008_uninitialized-variables-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
```
**Expected output:**
```
Converting to uf2, output size: 61952, start address: 0x10000000
Wrote 61952 bytes to build\hacked.uf2
```
##### Step 12: Flash and Test
**Enter bootloader:**
1. Hold **BOOTSEL** button
2. Plug in USB
3. Release BOOTSEL
**Flash:**
1. Drag `build\hacked.uf2` to RPI-RP2 drive
2. Pico reboots automatically
**Test with serial monitor:**
```
age: 66
age: 66
age: 66
...
```
**Hardware verification:**
- ✅ GREEN LED (GPIO 17) should be blinking
- ✅ RED LED (GPIO 16) should be off
- ✅ Blink rate should be much faster (10 Hz instead of 1 Hz)
- ✅ Serial output shows 66 instead of 0
🎉 **Triple success!** You've patched three different aspects of the program!
#### Expected Output
After completing this exercise, you should:
- See `age: 66` printing continuously
- Observe the green LED (GPIO 17) blinking rapidly
- Understand how to find and patch all instances of a value
- Know how to handle different immediate value encoding schemes
#### Questions for Reflection
###### Question 1: Why did we need to patch GPIO 16 in multiple places (gpio_init, gpio_set_dir, gpio_put)?
###### Question 2: What would happen if you forgot to patch one of the gpio_put calls?
###### Question 3: How does the instruction encoding differ for immediate values less than 256 vs. greater than 255?
###### Question 4: If you wanted to make the LED blink at exactly 5 Hz, what sleep_ms value would you use?
#### Tips and Hints
- Use Ghidra's **Search****For Scalars** to find all instances of a hex value
- Right-click in Listing → **Highlight****Highlight Instruction** helps track your patches
- Make notes of addresses you've patched to avoid confusion
- Test incrementally - patch one thing at a time if you're unsure
- Keep the original UF2 to revert if needed
#### Next Steps
- Try patching to use GPIO 18 (blue LED) instead
- Change the printf format string to display in hexadecimal
- Experiment with different timing patterns (e.g., 200ms on, 800ms off)
#### Additional Challenge
**Advanced Multi-LED Pattern:**
Patch the binary to create an alternating pattern:
- GPIO 16 (red) blinks for 100ms
- GPIO 17 (green) blinks for 100ms
- GPIO 18 (blue) blinks for 100ms
- Repeat
This requires:
1. Adding new gpio_init and gpio_set_dir calls for GPIO 18
2. Restructuring the while loop to have three gpio_put sequences
3. Finding space in the binary or replacing existing code
**Hint:** You might need to NOP out some instructions and carefully insert new ones. This is advanced patching!
#### Verification Checklist
Before moving on, confirm:
- [ ] GPIO 17 LED blinks (not GPIO 16)
- [ ] Blink rate is approximately 10 Hz (100ms on/off)
- [ ] Serial output shows "age: 66"
- [ ] You can explain each patch you made
- [ ] You understand why each patch was necessary
- [ ] You successfully converted and flashed the UF2
**Congratulations!** You've completed all Week 4 exercises and mastered variable analysis, binary patching, and GPIO manipulation!
+958
View File
@@ -0,0 +1,958 @@
# Embedded Systems Reverse Engineering
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
## Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
### 🎯 What You'll Learn This Week
By the end of this tutorial, you will be able to:
- Understand what variables are and how they're stored in memory
- Know the difference between initialized, uninitialized, and constant variables
- Use Ghidra to analyze binaries without debug symbols
- Patch binary files to change program behavior permanently
- Control GPIO pins to blink LEDs on the Pico 2
- Convert patched binaries to UF2 format for flashing
- Understand the `.data`, `.bss`, and `.rodata` memory sections
---
## 📚 Part 1: Understanding Variables
### What is a Variable?
A **variable** is like a labeled box where you can store information. Imagine you have a row of boxes numbered 0 to 9. Each box can hold one item. In programming:
- The **boxes** are memory locations (addresses in SRAM)
- The **items** are the values you store
- The **labels** are the variable names you choose
```
┌─────────────────────────────────────────────────────────────────┐
│ Memory (SRAM) - Like a row of numbered boxes │
│ │
│ Box 0 Box 1 Box 2 Box 3 Box 4 ... │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 42 │ │ 17 │ │ 0 │ │255 │ │ 99 │ │
│ └────┘ └────┘ └────┘ └────┘ └────┘ │
│ age score count max temp │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Declaration vs Definition
When working with variables, there are two important concepts:
| Concept | What It Does | Example |
| ------------------ | ------------------------------------ | -------------------------- |
| **Declaration** | Tells the compiler the name and type | `uint8_t age;` |
| **Definition** | Allocates memory for the variable | (happens with declaration) |
| **Initialization** | Assigns an initial value | `uint8_t age = 42;` |
**Important Rule:** You must declare a variable BEFORE you use it!
### Understanding Data Types
The **data type** tells the compiler how much memory to allocate:
| Type | Size | Range | Description |
| ---------- | ------- | ------------------------------- | ----------------------- |
| `uint8_t` | 1 byte | 0 to 255 | Unsigned 8-bit integer |
| `int8_t` | 1 byte | -128 to 127 | Signed 8-bit integer |
| `uint16_t` | 2 bytes | 0 to 65,535 | Unsigned 16-bit integer |
| `int16_t` | 2 bytes | -32,768 to 32,767 | Signed 16-bit integer |
| `uint32_t` | 4 bytes | 0 to 4,294,967,295 | Unsigned 32-bit integer |
| `int32_t` | 4 bytes | -2,147,483,648 to 2,147,483,647 | Signed 32-bit integer |
### Anatomy of a Variable Declaration
Let's break down this line of code:
```c
uint8_t age = 42;
```
| Part | Meaning |
| --------- | ----------------------------------------------------- |
| `uint8_t` | Data type - unsigned 8-bit integer (1 byte) |
| `age` | Variable name - how we refer to this storage location |
| `=` | Assignment operator - puts a value into the variable |
| `42` | The initial value |
| `;` | Semicolon - tells compiler the statement is complete |
---
## 📚 Part 2: Memory Sections - Where Variables Live
### The Three Main Sections
When your program is compiled, variables go to different places depending on how they're declared:
```
┌─────────────────────────────────────────────────────────────────┐
│ .data Section (Flash → copied to RAM at startup) │
│ Contains: Initialized global/static variables │
│ Example: int counter = 42; │
├─────────────────────────────────────────────────────────────────┤
│ .bss Section (RAM - zeroed at startup) │
│ Contains: Uninitialized global/static variables │
│ Example: int counter; (will be 0) │
├─────────────────────────────────────────────────────────────────┤
│ .rodata Section (Flash - read only) │
│ Contains: Constants, string literals │
│ Example: const int MAX = 100; │
│ Example: "hello, world" │
└─────────────────────────────────────────────────────────────────┘
```
### What Happens to Uninitialized Variables?
In older C compilers, uninitialized variables could contain "garbage" - random leftover data. But modern compilers (including the Pico SDK) are smarter:
1. Uninitialized global variables go into the `.bss` section
2. The `.bss` section is **NOT stored in the binary** (saves space!)
3. At boot, the startup code uses `memset` to **zero out** all of `.bss`
4. So uninitialized variables are always `0`!
This is why in our code:
```c
uint8_t age; // This will be 0, not garbage!
```
---
## 📚 Part 3: Understanding GPIO (General Purpose Input/Output)
### What is GPIO?
**GPIO** stands for **General Purpose Input/Output**. These are pins on the microcontroller that you can control with software. Think of them as tiny switches you can turn on and off.
```
┌─────────────────────────────────────────────────────────────────┐
│ Raspberry Pi Pico 2 │
│ │
│ GPIO 16 ───────► Red LED │
│ GPIO 17 ───────► Green LED │
│ GPIO 18 ───────► Blue LED │
│ ... │
│ GPIO 25 ───────► Onboard LED │
└─────────────────────────────────────────────────────────────────┘
```
### GPIO Functions in the Pico SDK
The Pico SDK provides simple functions to control GPIO pins:
| Function | Purpose |
| ------------------------------ | ------------------------------- |
| `gpio_init(pin)` | Initialize a GPIO pin for use |
| `gpio_set_dir(pin, direction)` | Set pin as INPUT or OUTPUT |
| `gpio_put(pin, value)` | Set pin HIGH (1) or LOW (0) |
| `sleep_ms(ms)` | Wait for specified milliseconds |
### Basic LED Blink Code
```c
#include <stdio.h>
#include "pico/stdlib.h"
#define LED_PIN 16
int main(void) {
gpio_init(LED_PIN); // Initialize GPIO 16
gpio_set_dir(LED_PIN, GPIO_OUT); // Set as output
while (true) {
gpio_put(LED_PIN, 1); // LED ON
sleep_ms(500); // Wait 500ms
gpio_put(LED_PIN, 0); // LED OFF
sleep_ms(500); // Wait 500ms
}
}
```
### What Happens Behind the Scenes?
Each high-level function calls lower-level code. Let's trace `gpio_init()`:
```
gpio_init(LED_PIN)
gpio_set_dir(LED_PIN, GPIO_IN) // Initially set as input
gpio_put(LED_PIN, 0) // Set output value to 0
gpio_set_function(LED_PIN, GPIO_FUNC_SIO) // Connect to SIO block
```
The SIO (Single-cycle I/O) block is a special hardware unit in the RP2350 that provides fast GPIO control!
---
## 📚 Part 4: Setting Up Your Environment
### Prerequisites
Before we start, make sure you have:
1. A Raspberry Pi Pico 2 board
2. Ghidra installed (for static analysis)
3. Python installed (for UF2 conversion)
4. The sample projects:
- `0x0005_intro-to-variables`
- `0x0008_uninitialized-variables`
5. A serial monitor (PuTTY, minicom, or screen)
### Project Structure
```
Embedded-Hacking/
├── 0x0005_intro-to-variables/
│ ├── build/
│ │ ├── 0x0005_intro-to-variables.uf2
│ │ └── 0x0005_intro-to-variables.bin
│ └── 0x0005_intro-to-variables.c
├── 0x0008_uninitialized-variables/
│ ├── build/
│ │ ├── 0x0008_uninitialized-variables.uf2
│ │ └── 0x0008_uninitialized-variables.bin
│ └── 0x0008_uninitialized-variables.c
└── uf2conv.py
```
---
## 🔬 Part 5: Hands-On Tutorial - Analyzing Variables in Ghidra
### Step 1: Review the Source Code
First, let's look at the code we'll be analyzing:
**File: `0x0005_intro-to-variables.c`**
```c
#include <stdio.h>
#include "pico/stdlib.h"
int main(void) {
uint8_t age = 42;
age = 43;
stdio_init_all();
while (true)
printf("age: %d\r\n", age);
}
```
**What this code does:**
1. Declares a variable `age` and initializes it to `42`
2. Changes `age` to `43`
3. Initializes the serial output
4. Prints `age` forever in a loop
### Step 2: Flash the Binary to Your Pico 2
1. Hold the BOOTSEL button on your Pico 2
2. Plug in the USB cable (while holding BOOTSEL)
3. Release BOOTSEL - a drive called "RPI-RP2" appears
4. Drag and drop `0x0005_intro-to-variables.uf2` onto the drive
5. The Pico will reboot and start running!
### Step 3: Verify It's Working
Open your serial monitor (PuTTY, minicom, or screen) and you should see:
```
age: 43
age: 43
age: 43
...
```
The program is printing `43` because that's what we assigned after the initial `42`.
---
## 🔬 Part 6: Setting Up Ghidra for Binary Analysis
### Step 4: Start Ghidra
**Open a terminal and type:**
```bash
ghidraRun
```
Ghidra will open. Now we need to create a new project.
### Step 5: Create a New Project
1. Click **File****New Project**
2. Select **Non-Shared Project**
3. Click **Next**
4. Enter Project Name: `0x0005_intro-to-variables`
5. Click **Finish**
### Step 6: Import the Binary
1. Open your file explorer
2. Navigate to the `Embedded-Hacking` folder
3. Find `0x0005_intro-to-variables.bin`
4. **Drag and drop** the `.bin` file into Ghidra's project window
### Step 7: Configure the Binary Format
A dialog appears. The file is identified as a "BIN" (raw binary without debug symbols).
**Click the three dots (…) next to "Language" and:**
1. Search for "Cortex"
2. Select **ARM Cortex 32 little endian default**
3. Click **OK**
**Click the "Options…" button and:**
1. Change **Block Name** to `.text`
2. Change **Base Address** to `10000000` (the XIP address!)
3. Click **OK**
### Step 8: Open and Analyze
1. Double-click on the file in the project window
2. A dialog asks "Analyze now?" - Click **Yes**
3. Use default analysis options and click **Analyze**
Wait for analysis to complete (watch the progress bar in the bottom right).
---
## 🔬 Part 7: Navigating and Resolving Functions
### Step 9: Find the Functions
Look at the **Symbol Tree** panel on the left. Expand **Functions**.
You'll see function names like:
- `FUN_1000019a`
- `FUN_10000210`
- `FUN_10000234`
These are auto-generated names because we imported a raw binary without symbols!
### Step 10: Resolve Known Functions
From our previous chapters, we know what some of these functions are:
| Ghidra Name | Actual Name | How We Know |
| -------------- | ------------- | -------------------------- |
| `FUN_1000019a` | `data_cpy` | From Week 3 boot analysis |
| `FUN_10000210` | `frame_dummy` | From Week 3 boot analysis |
| `FUN_10000234` | `main` | This is where our code is! |
**To rename `FUN_1000019a` to `data_cpy`:**
1. Click on `FUN_1000019a` in the Symbol Tree
2. In the Decompile window, right-click on the function name
3. Select **Edit Function Signature**
4. Change the name to `data_cpy`
5. Click **OK**
**Repeat for the other functions:**
- Rename `FUN_10000210` to `frame_dummy`
- Rename `FUN_10000234` to `main`
### Step 11: Update Main's Signature
For `main`, let's also fix the return type:
1. Right-click on `main` in the Decompile window
2. Select **Edit Function Signature**
3. Change to: `int main(void)`
4. Click **OK**
---
## 🔬 Part 8: Analyzing the Main Function
### Step 12: Examine Main in Ghidra
Click on `main` (or `FUN_10000234`). Look at the **Decompile** window:
You'll see something like:
```c
void FUN_10000234(void)
{
FUN_10002f54();
do {
FUN_100030e4(DAT_10000244,0x2b);
} while( true );
}
```
### Step 13: Resolve stdio_init_all
1. Click on `FUN_10002f54`
2. Right-click → **Edit Function Signature**
3. Change to: `bool stdio_init_all(void)`
4. Click **OK**
### Step 14: Resolve printf
1. Click on `FUN_100030e4`
2. Right-click → **Edit Function Signature**
3. Change the name to `printf`
4. Check the **Varargs** checkbox (printf takes variable arguments!)
5. Click **OK**
### Step 15: Understand the Optimization
Look at the decompiled code. This will look different if you resolved your functions however do you notice something interesting?
```c
void FUN_10000234(void)
{
FUN_10002f54();
do {
FUN_100030e4(DAT_10000244,0x2b);
} while( true );
}
```
**Where's `uint8_t age = 42`?** It's gone!
The compiler **optimized it out**! Here's what happened:
1. Original code: `age = 42`, then `age = 43`
2. Compiler sees: "The `42` is never used, only `43` matters"
3. Compiler removes the unused `42` and just uses `43` directly
**What is `0x2b`?** Let's check:
- `0x2b` in hexadecimal = `43` in decimal ✓
The compiler replaced our variable with the constant value!
---
## 🔬 Part 9: Patching the Binary - Changing the Value
### Step 16: Find the Value to Patch
Look at the **Listing** window (assembly view). Find the instruction that loads `0x2b`:
```assembly
1000023a 2b 21 movs r1,#0x2b
```
This instruction loads the value `0x2b` (43) into register `r1` before calling `printf`.
### Step 17: Patch the Instruction
We're going to change `0x2b` (43) to `0x46` (70)!
1. Click on the instruction `movs r1,#0x2b`
2. Right-click and select **Patch Instruction**
3. Change `0x2b` to `0x46`
4. Press Enter
The instruction now reads:
```assembly
1000023a 46 21 movs r1,#0x46
```
### Step 18: Export the Patched Binary
1. Click **File****Export Program**
2. Set **Format** to **Raw Bytes**
3. Navigate to your build directory
4. Name the file `0x0005_intro-to-variables-h.bin`
5. Click **OK**
---
## 🔬 Part 10: Converting and Flashing the Hacked Binary
### Step 19: Convert to UF2 Format
The Pico 2 expects UF2 files, not raw BIN files. We need to convert it!
**Open a terminal and navigate to your project directory:**
```bash
cd Embedded-Hacking/0x0005_intro-to-variables
```
**Run the conversion command:**
```bash
python ../uf2conv.py build/0x0005_intro-to-variables-h.bin --base 0x10000000 --family 0xe48bff59 --output build/hacked.uf2
```
**What this command means:**
- `uf2conv.py` = the conversion script
- `--base 0x10000000` = the XIP base address
- `--family 0xe48bff59` = the RP2350 family ID
- `--output build/hacked.uf2` = the output filename
### Step 20: Flash the Hacked Binary
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
**You should see:**
```
age: 70
age: 70
age: 70
...
```
🎉 **BOOM! We hacked it!** The value changed from 43 to 70!
---
## 🔬 Part 11: Uninitialized Variables and GPIO
Now let's work with a more complex example that includes GPIO control.
### Step 21: Review the Uninitialized Variables Code
**File: `0x0008_uninitialized-variables.c`**
```c
#include <stdio.h>
#include "pico/stdlib.h"
#define LED_PIN 16
int main(void) {
uint8_t age; // Uninitialized!
stdio_init_all();
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
while (true) {
printf("age: %d\r\n", age);
gpio_put(LED_PIN, 1);
sleep_ms(500);
gpio_put(LED_PIN, 0);
sleep_ms(500);
}
}
```
**What this code does:**
1. Declares `age` without initializing it (will be 0 due to BSS zeroing)
2. Initializes GPIO 16 as an output
3. In a loop: prints age, blinks the LED
### Step 22: Flash and Verify
1. Flash `0x0008_uninitialized-variables.uf2` to your Pico 2
2. Open your serial monitor
**You should see:**
```
age: 0
age: 0
age: 0
...
```
And the **red LED on GPIO 16 should be blinking**!
The value is `0` because uninitialized variables in the `.bss` section are zeroed at startup.
---
## 🔬 Part 12: Analyzing GPIO Code in Ghidra
### Step 23: Set Up Ghidra for the New Binary
1. Create a new project: `0x0008_uninitialized-variables`
2. Import `0x0008_uninitialized-variables.bin`
3. Set Language to **ARM Cortex 32 little endian**
4. Set Base Address to `10000000`
5. Auto-analyze
### Step 24: Resolve the Functions
Find and rename these functions:
| Ghidra Name | Actual Name |
| -------------- | ---------------- |
| `FUN_10000234` | `main` |
| `FUN_100030cc` | `stdio_init_all` |
| `FUN_100002b4` | `gpio_init` |
| `FUN_1000325c` | `printf` |
For `gpio_init`, set the signature to:
```c
void gpio_init(uint gpio)
```
### Step 25: Examine the Main Function
The decompiled main should look something like:
```c
void FUN_10000234(void)
{
undefined4 extraout_r1;
undefined4 extraout_r2;
undefined4 in_cr0;
undefined4 in_cr4;
FUN_100030cc();
FUN_100002b4(0x10);
coprocessor_moveto2(0,4,0x10,1,in_cr4);
do {
FUN_1000325c(DAT_10000274,0);
coprocessor_moveto2(0,4,0x10,1,in_cr0);
FUN_10000d10(500);
coprocessor_moveto2(0,4,0x10,0,in_cr0);
FUN_10000d10(500,extraout_r1,extraout_r2,0);
} while( true );
}
```
---
## 🔬 Part 13: Hacking GPIO - Changing the LED Pin
### Step 26: Find the GPIO Pin Value
Look in the assembly for instructions that use `0x10` (which is 16 in decimal - our LED pin):
```assembly
1000023a 10 20 movs r0,#0x10
```
This is where `gpio_init(LED_PIN)` is called with GPIO 16.
### Step 27: Patch GPIO 16 to GPIO 17
We'll change the red LED (GPIO 16) to the green LED (GPIO 17)!
1. Find the instruction `movs r0,#0x10`
2. Right-click → **Patch Instruction**
3. Change `0x10` to `0x11` (17 in hex)
4. Click **OK**
### Step 28: Find All GPIO 16 References
There are more places that use GPIO 16. Look for:
```assembly
10000244 10 23 movs r3,#0x10
```
This is used in `gpio_set_dir`. Patch this to `0x11` as well.
```assembly
10000252 10 24 movs r4,#0x10
```
This is inside the loop for `gpio_put`. Patch this to `0x11` as well.
### Step 29: Bonus - Change the Printed Value
Let's also change the printed value from `0` to `0x42` (66 in decimal):
```assembly
1000024a 00 21 movs r1,#0x0
```
1. Right-click → **Patch Instruction**
2. Change `0x0` to `0x42`
3. Click **OK**
---
## 🔬 Part 14: Export and Test the Hacked GPIO
### Step 30: Export the Patched Binary
1. Click **File****Export Program**
2. Format: **Raw Bytes**
3. Filename: `0x0008_uninitialized-variables-h.bin`
4. Click **OK**
### Step 31: Convert to UF2
```bash
cd Embedded-Hacking/0x0008_uninitialized-variables
python ../uf2conv.py build/0x0008_uninitialized-variables-h.bin --base 0x10000000 --family 0xe48bff59 --output build/hacked.uf2
```
### Step 32: Flash and Verify
1. Flash `hacked.uf2` to your Pico 2
2. Check your serial monitor
**You should see:**
```
age: 66
age: 66
age: 66
...
```
And now the **GREEN LED on GPIO 17** should be blinking instead of the red one!
🎉 **We successfully:**
1. Changed the printed value from 0 to 66
2. Changed which LED blinks from red (GPIO 16) to green (GPIO 17)
---
## 📚 Part 15: Deep Dive - GPIO at the Assembly Level
### Understanding the GPIO Coprocessor
The RP2350 has a special **GPIO coprocessor** that provides fast, single-cycle GPIO control. This is different from the RP2040!
The coprocessor is accessed using special ARM instructions:
```assembly
mcrr p0, #4, r4, r5, c0 ; GPIO output control
mcrr p0, #4, r4, r5, c4 ; GPIO direction control
```
**What this means:**
- `mcrr` = Move to Coprocessor from two ARM Registers
- `p0` = Coprocessor 0 (the GPIO coprocessor)
- `r4` = Contains the GPIO pin number
- `r5` = Contains the value (0 or 1)
- `c0` = Output value register
- `c4` = Output enable register
### The Full GPIO Initialization Sequence
When you call `gpio_init(16)`, here's what actually happens:
```
Step 1: Configure pad (address 0x40038044)
┌─────────────────────────────────────────────────────────────────┐
│ - Clear OD bit (output disable) │
│ - Set IE bit (input enable) │
│ - Clear ISO bit (isolation) │
└─────────────────────────────────────────────────────────────────┘
Step 2: Set function (address 0x40028084)
┌─────────────────────────────────────────────────────────────────┐
│ - Set FUNCSEL to 5 (SIO - Software I/O) │
└─────────────────────────────────────────────────────────────────┘
Step 3: Enable output (via coprocessor)
┌─────────────────────────────────────────────────────────────────┐
│ - mcrr p0, #4, r4, r5, c4 (where r4=16, r5=1) │
└─────────────────────────────────────────────────────────────────┘
```
### Raw Assembly LED Blink
Here's what a completely hand-written assembly LED blink looks like:
```assembly
; Initialize GPIO 16 as output
movs r4, #0x10 ; GPIO 16
movs r5, #0x01 ; Enable
mcrr p0, #4, r4, r5, c4 ; Set as output
; Configure pad registers
ldr r3, =0x40038044 ; Pad control for GPIO 16
ldr r2, [r3] ; Load current config
bic r2, r2, #0x80 ; Clear OD (output disable)
orr r2, r2, #0x40 ; Set IE (input enable)
str r2, [r3] ; Store config
; Set GPIO function to SIO
ldr r3, =0x40028084 ; IO bank control for GPIO 16
movs r2, #5 ; FUNCSEL = SIO
str r2, [r3] ; Set function
; Main loop
loop:
; LED ON
movs r4, #0x10 ; GPIO 16
movs r5, #0x01 ; High
mcrr p0, #4, r4, r5, c0
; Delay
ldr r2, =0x17D7840 ; ~25 million iterations
delay1:
subs r2, r2, #1
bne delay1
; LED OFF
movs r4, #0x10 ; GPIO 16
movs r5, #0x00 ; Low
mcrr p0, #4, r4, r5, c0
; Delay
ldr r2, =0x17D7840
delay2:
subs r2, r2, #1
bne delay2
b loop ; Repeat forever
```
---
## 📊 Part 16: Summary and Review
### What We Accomplished
1. **Learned about variables** - How they're declared, initialized, and stored
2. **Understood memory sections** - `.data`, `.bss`, and `.rodata`
3. **Analyzed binaries in Ghidra** - Without debug symbols!
4. **Patched binaries** - Changed values directly in the binary
5. **Controlled GPIO** - Made LEDs blink
6. **Changed program behavior** - Different LED, different value
### The Binary Patching Workflow
```
┌─────────────────────────────────────────────────────────────────┐
│ 1. Import .bin file into Ghidra │
│ - Set language to ARM Cortex │
│ - Set base address to 0x10000000 │
├─────────────────────────────────────────────────────────────────┤
│ 2. Analyze and resolve functions │
│ - Rename functions to meaningful names │
│ - Fix function signatures │
├─────────────────────────────────────────────────────────────────┤
│ 3. Find the values/instructions to patch │
│ - Look in the assembly listing │
│ - Right-click → Patch Instruction │
├─────────────────────────────────────────────────────────────────┤
│ 4. Export the patched binary │
│ - File → Export Program │
│ - Format: Raw Bytes │
├─────────────────────────────────────────────────────────────────┤
│ 5. Convert to UF2 │
│ - python uf2conv.py file.bin --base 0x10000000 │
│ --family 0xe48bff59 --output hacked.uf2 │
├─────────────────────────────────────────────────────────────────┤
│ 6. Flash and verify │
│ - Hold BOOTSEL, plug in, drag UF2 │
│ - Check serial output and LED behavior │
└─────────────────────────────────────────────────────────────────┘
```
### Key Memory Sections
| Section | Location | Contains | Writable? |
| --------- | -------- | ------------------------------ | --------- |
| `.text` | Flash | Code | No |
| `.rodata` | Flash | Constants, strings | No |
| `.data` | RAM | Initialized globals | Yes |
| `.bss` | RAM | Uninitialized globals (zeroed) | Yes |
### Important Ghidra Commands
| Action | How To Do It |
| ----------------- | ------------------------------------- |
| Rename function | Right-click → Edit Function Signature |
| Patch instruction | Right-click → Patch Instruction |
| Export binary | File → Export Program → Raw Bytes |
| Go to address | Press 'G' and enter address |
---
## ✅ Practice Exercises
### Exercise 1: Change the Delay
The LED blinks every 500ms. Find the `sleep_ms(500)` calls in the binary and change them to `sleep_ms(100)` for faster blinking.
**Hint:** Look for the value `0x1F4` (500 in hex) being loaded into a register.
### Exercise 2: Reverse the LED
Instead of GPIO 16 → ON → OFF, make it GPIO 16 → OFF → ON (start with LED on).
**Hint:** Find and swap the two `gpio_put` calls (the ones with values 0 and 1).
### Exercise 3: Add a Second LED
Patch the binary so that BOTH GPIO 16 and GPIO 17 blink together.
**Hint:** You'll need to find space for additional instructions or modify existing ones cleverly.
### Exercise 4: Change the Format String
The program prints "age: %d\r\n". Can you find this string in Ghidra and figure out where it's stored?
**Hint:** Look in the `.rodata` section around address `0x10001xxx`.
---
## 🎓 Key Takeaways
1. **Variables are just memory locations** - The compiler assigns them addresses in SRAM.
2. **Compilers optimize aggressively** - Unused code and values may be removed entirely.
3. **Uninitialized doesn't mean random** - Modern compilers zero out the `.bss` section.
4. **Ghidra works without symbols** - You can analyze any binary, even stripped ones.
5. **Binary patching is powerful** - You can change behavior without source code.
6. **UF2 conversion is required** - The Pico 2 needs UF2 format, not raw binaries.
7. **GPIO is just memory-mapped I/O** - Writing to specific addresses controls hardware.
---
## 📖 Glossary
| Term | Definition |
| ------------------ | --------------------------------------------------------------------- |
| **BSS** | Block Started by Symbol - section for uninitialized global variables |
| **Declaration** | Telling the compiler a variable's name and type |
| **Definition** | Allocating memory for a variable |
| **GPIO** | General Purpose Input/Output - controllable pins on a microcontroller |
| **Initialization** | Assigning an initial value to a variable |
| **Linker** | Tool that combines compiled code and assigns memory addresses |
| **Optimization** | Compiler removing or simplifying code for efficiency |
| **Patching** | Modifying bytes directly in a binary file |
| **rodata** | Read-only data section for constants and string literals |
| **SIO** | Single-cycle I/O - fast GPIO control block in RP2350 |
| **UF2** | USB Flashing Format - file format for Pico 2 firmware |
| **Variable** | A named storage location in memory |
---
## 🔗 Additional Resources
### GPIO Coprocessor Reference
The RP2350 GPIO coprocessor instructions:
| Instruction | Description |
| -------------------------- | ---------------------------- |
| `mcrr p0, #4, Rt, Rt2, c0` | Set/clear GPIO output |
| `mcrr p0, #4, Rt, Rt2, c4` | Set/clear GPIO output enable |
### RP2350 Memory Map Quick Reference
| Address | Description |
| ------------ | ------------------------ |
| `0x10000000` | XIP Flash (code) |
| `0x20000000` | SRAM (data) |
| `0x40028000` | IO_BANK0 (GPIO control) |
| `0x40038000` | PADS_BANK0 (pad control) |
| `0xd0000000` | SIO (single-cycle I/O) |
---
**Remember:** Every binary you encounter in the real world can be analyzed and understood using these same techniques. Practice makes perfect!
Happy hacking! 🔧