mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-04-04 02:12:34 +02:00
Added WEEK04
This commit is contained in:
10
README.md
10
README.md
@@ -102,6 +102,16 @@ This chapter covers a comprehensive embedded system analysis reviewing parts of
|
||||
## Week 4
|
||||
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
|
||||
This chapter covers an introduction to variables as it relates to embedded development on the Pico 2.
|
||||
|
||||
|
||||
241
WEEK04/WEEK04-01.md
Normal file
241
WEEK04/WEEK04-01.md
Normal 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
WEEK04/WEEK04-02.md
Normal file
220
WEEK04/WEEK04-02.md
Normal 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
WEEK04/WEEK04-03.md
Normal file
270
WEEK04/WEEK04-03.md
Normal 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
WEEK04/WEEK04-04.md
Normal file
345
WEEK04/WEEK04-04.md
Normal 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
WEEK04/WEEK04.md
Normal file
958
WEEK04/WEEK04.md
Normal 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! 🔧
|
||||
Reference in New Issue
Block a user