mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-06-08 07:13:56 +02:00
965 lines
33 KiB
Markdown
965 lines
33 KiB
Markdown
# 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:
|
||
|
||
> 📖 **Datasheet Reference:** SRAM starts at `0x20000000` and XIP flash at `0x10000000` (Section 2.2, p. 31). The `.data` and `.bss` sections live in SRAM; `.text` and `.rodata` live in flash and are accessed via Execute-In-Place (Section 4.4, p. 340+).
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ .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!
|
||
|
||
> 📖 **Datasheet Reference:** IO_BANK0 is at `0x40028000` and PADS_BANK0 is at `0x40038000` (Section 2.2, p. 33). FUNCSEL value 5 selects the SIO function for a GPIO pin (Section 9.6.1, p. 612). The SIO block base address is `0xd0000000` (Section 2.2, p. 31).
|
||
|
||
---
|
||
|
||
## 📚 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:**
|
||
|
||
```powershell
|
||
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. Select Cortex M Little Endian 32
|
||
5. Select Options and set up the .text and offset 10000000
|
||
6. **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:**
|
||
|
||
```powershell
|
||
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0005_intro-to-variables
|
||
```
|
||
|
||
**Run the conversion command:**
|
||
|
||
```powershell
|
||
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
|
||
|
||
```powershell
|
||
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\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!
|
||
|
||
> 📖 **Datasheet Reference:** The GPIO coprocessor (GPIOC) is documented in Section 3.7.5 (p. 101–104). It provides low-overhead Cortex-M33 coprocessor access to SIO GPIO registers via `mcrr`/`mrrc` instructions, allowing single-cycle GPIO operations.
|
||
|
||
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) |
|
||
|
||
> 📖 **Datasheet Reference:** Full address map in Section 2.2 (p. 31–33). IO_BANK0 at `0x40028000` (p. 605), PADS_BANK0 at `0x40038000` (p. 786), SIO at `0xd0000000`.
|
||
|
||
---
|
||
|
||
**Remember:** Every binary you encounter in the real world can be analyzed and understood using these same techniques. Practice makes perfect!
|
||
|
||
Happy hacking! 🔧
|