mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-07-23 12:20:54 +02:00
1088 lines
40 KiB
Markdown
1088 lines
40 KiB
Markdown
# Week 7: Constants in Embedded Systems: Debugging and Hacking Constants w/ 1602 LCD I2C Basics
|
|
|
|
---
|
|
**LEGAL DISCLAIMER:**
|
|
The information, tools, and code provided in this repository and course are strictly for educational, research, and defensive purposes only.
|
|
|
|
You are explicitly prohibited from using any materials contained herein to access, test, modify, or exploit any device, network, or system that you do not own 100% or for which you do not have explicit, documented, and legally binding authorization to interact with.
|
|
|
|
By using this repository and course, you acknowledge and agree that:
|
|
1. Any illegal, unauthorized, or malicious use of this information is solely your responsibility.
|
|
2. The author(s) and contributor(s) of this repository and course shall not be held liable for any damages, legal repercussions, criminal charges, or unauthorized actions resulting from the use, misuse, or abuse of the contents herein.
|
|
3. You will comply with all applicable local, state, national, and international laws regarding cybersecurity and computer fraud.
|
|
|
|
**IF YOU DO NOT AGREE WITH THESE TERMS, DO NOT USE THIS REPOSITORY AND COURSE.**
|
|
---
|
|
|
|
## What You'll Learn This Week
|
|
|
|
By the end of this tutorial, you will be able to:
|
|
- Understand the difference between `#define` macros and `const` variables
|
|
- Know how constants are stored differently in memory (compile-time vs runtime)
|
|
- Understand the I2C (Inter-Integrated Circuit) communication protocol
|
|
- Configure I2C peripherals and communicate with LCD displays
|
|
- Understand C structs and how the Pico SDK uses them for hardware abstraction
|
|
- Use GDB to examine constants, structs, and string literals in memory
|
|
- Hack constant values and string literals using a hex editor
|
|
- Patch LCD display text without access to source code
|
|
|
|
---
|
|
|
|
## Part 1: Understanding Constants in C
|
|
|
|
### Two Types of Constants
|
|
|
|
In C, there are two ways to create values that shouldn't change:
|
|
|
|
| Type | Syntax | Where It Lives | When Resolved |
|
|
| ----------- | ----------------------- | ------------------ | ------------- |
|
|
| **#define** | `#define FAV_NUM 42` | Nowhere (replaced) | Compile time |
|
|
| **const** | `const int NUM = 1337;` | Flash (.rodata) | Runtime |
|
|
|
|
### Preprocessor Macros (#define)
|
|
|
|
A **preprocessor macro** is a text replacement that happens BEFORE your code is compiled:
|
|
|
|
```c
|
|
#define FAV_NUM 42
|
|
|
|
printf("Value: %d", FAV_NUM);
|
|
// Becomes: printf("Value: %d", 42);
|
|
```
|
|
|
|
Think of it like a "find and replace" in a text editor. The compiler never sees `FAV_NUM` - it only sees `42`!
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| Preprocessor Macro Flow |
|
|
| |
|
|
| Source Code Preprocessor Compiler |
|
|
| +----------+ +----------+ +----------+ |
|
|
| | #define | | Replace | | Compile | |
|
|
| | FAV_NUM | -----? | FAV_NUM | -----? | binary | |
|
|
| | 42 | | with 42 | | code | |
|
|
| +----------+ +----------+ +----------+ |
|
|
| |
|
|
| FAV_NUM doesn't exist in the final binary! |
|
|
| The value 42 is embedded directly in instructions. |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### Const Variables
|
|
|
|
A **const variable** is an actual variable stored in memory, but marked as read-only:
|
|
|
|
```c
|
|
const int OTHER_FAV_NUM = 1337;
|
|
```
|
|
|
|
Unlike `#define`, this creates a real memory location in the `.rodata` (read-only data) section of flash:
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| Const Variable in Memory |
|
|
| |
|
|
| Flash Memory (.rodata section) |
|
|
| +------------------------------------------------------------+ |
|
|
| | Address: 0x10001234 | |
|
|
| | Value: 0x00000539 (1337 in hex) | |
|
|
| | Name: OTHER_FAV_NUM (in debug symbols only) | |
|
|
| +------------------------------------------------------------+ |
|
|
| |
|
|
| The variable EXISTS in memory and can be read at runtime. |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### Comparison: #define vs const
|
|
|
|
| Feature | #define | const |
|
|
| -------------------- | ------------------------ | ---------------------------- |
|
|
| **Type checking** | None (just text) | Yes (compiler enforced) |
|
|
| **Memory usage** | None (inlined) | Uses flash space |
|
|
| **Debugger visible** | No | Yes (with symbols) |
|
|
| **Can take address** | No (`&FAV_NUM` fails) | Yes (`&OTHER_FAV_NUM` works) |
|
|
| **Scope** | Global (from definition) | Normal C scoping rules |
|
|
|
|
---
|
|
|
|
## Part 2: Understanding I2C Communication
|
|
|
|
### What is I2C?
|
|
|
|
**I2C** (pronounced "I-squared-C" or "I-two-C") stands for **Inter-Integrated Circuit**. It's a way for chips to talk to each other using just TWO wires!
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| I2C Bus - Two Wires, Many Devices |
|
|
| |
|
|
| 3.3V |
|
|
| | |
|
|
| + Pull-up + Pull-up |
|
|
| | | |
|
|
| SDA -+------------+--------------------------------------- |
|
|
| | | |
|
|
| SCL -+------------+--------------------------------------- |
|
|
| | | | | |
|
|
| +---+----+ +--+---+ +----+--+ +-----+---+ |
|
|
| | Pico | | LCD | |Sensor | | EEPROM | |
|
|
| |(Master)| | 0x27 | | 0x48 | | 0x50 | |
|
|
| +--------+ +------+ +-------+ +---------+ |
|
|
| |
|
|
| Each device has a unique address (0x27, 0x48, 0x50...) |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### The Two I2C Wires
|
|
|
|
| Wire | Name | Purpose |
|
|
| ------- | ------------ | ------------------------------------ |
|
|
| **SDA** | Serial Data | Carries the actual data bits |
|
|
| **SCL** | Serial Clock | Timing signal that synchronizes data |
|
|
|
|
### Why Pull-Up Resistors?
|
|
|
|
I2C uses **open-drain** signals, meaning devices can only pull the line LOW. They can't drive it HIGH! Pull-up resistors are needed to bring the lines back to HIGH when no device is pulling them down.
|
|
|
|
The Pico 2 has internal pull-ups that we can enable with `gpio_pull_up()`.
|
|
|
|
### I2C Addresses
|
|
|
|
Every I2C device has a unique **7-bit address**. Common addresses:
|
|
|
|
| Device Type | Typical Address |
|
|
| --------------------- | ---------------- |
|
|
| 1602 LCD with PCF8574 | `0x27` or `0x3F` |
|
|
| Temperature sensor | `0x48` |
|
|
| EEPROM | `0x50` |
|
|
| Real-time clock | `0x68` |
|
|
|
|
### I2C Communication Flow
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| I2C Transaction |
|
|
| |
|
|
| 1. Master sends START condition |
|
|
| 2. Master sends device address (7 bits) + R/W bit |
|
|
| 3. Addressed device sends ACK (acknowledge) |
|
|
| 4. Data is transferred (8 bits at a time) |
|
|
| 5. Receiver sends ACK after each byte |
|
|
| 6. Master sends STOP condition |
|
|
| |
|
|
| START --? Address --? ACK --? Data --? ACK --? STOP |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
---
|
|
|
|
## Part 3: Understanding C Structs
|
|
|
|
### What is a Struct?
|
|
|
|
A **struct** (short for "structure") is a way to group related variables together under one name. Think of it like a form with multiple fields:
|
|
|
|
```c
|
|
// A struct definition - like a template
|
|
struct student {
|
|
char name[50];
|
|
int age;
|
|
float gpa;
|
|
};
|
|
|
|
// Creating a variable of this struct type
|
|
struct student alice = {"Alice", 16, 3.8};
|
|
```
|
|
|
|
### Why Use Structs?
|
|
|
|
Instead of passing many separate variables:
|
|
```c
|
|
void print_student(char *name, int age, float gpa); // Messy!
|
|
```
|
|
|
|
You pass one struct:
|
|
```c
|
|
void print_student(struct student s); // Clean!
|
|
```
|
|
|
|
### The typedef Keyword
|
|
|
|
Writing `struct student` everywhere is tedious. The `typedef` keyword creates an alias:
|
|
|
|
```c
|
|
typedef struct student student_t;
|
|
|
|
// Now you can write:
|
|
student_t alice; // Instead of: struct student alice;
|
|
```
|
|
|
|
### Forward Declaration
|
|
|
|
Sometimes you need to tell the compiler "this struct exists" before defining it:
|
|
|
|
```c
|
|
typedef struct i2c_inst i2c_inst_t; // Forward declaration + alias
|
|
|
|
// Later, the full definition:
|
|
struct i2c_inst {
|
|
i2c_hw_t *hw;
|
|
bool restart_on_next;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Part 4: Understanding the Pico SDK's I2C Structs
|
|
|
|
### The i2c_inst_t Struct
|
|
|
|
The Pico SDK uses a struct to represent each I2C controller:
|
|
|
|
```c
|
|
struct i2c_inst {
|
|
i2c_hw_t *hw; // Pointer to hardware registers
|
|
bool restart_on_next; // SDK internal flag
|
|
};
|
|
```
|
|
|
|
**What each member means:**
|
|
|
|
| Member | Type | Purpose |
|
|
| ----------------- | ------------ | ---------------------------------------- |
|
|
| `hw` | `i2c_hw_t *` | Pointer to the actual hardware registers |
|
|
| `restart_on_next` | `bool` | Tracks if next transfer needs a restart |
|
|
|
|
### The Macro Chain
|
|
|
|
When you write `I2C_PORT` in your code, here's what happens:
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| Macro Expansion Chain |
|
|
| |
|
|
| In your code: #define I2C_PORT i2c1 |
|
|
| | |
|
|
| ? |
|
|
| In i2c.h: #define i2c1 (&i2c1_inst) |
|
|
| | |
|
|
| ? |
|
|
| In i2c.c: i2c_inst_t i2c1_inst = {i2c1_hw, false}; |
|
|
| | |
|
|
| ? |
|
|
| In i2c.h: #define i2c1_hw ((i2c_hw_t *)I2C1_BASE) |
|
|
| | |
|
|
| ? |
|
|
| In addressmap.h: #define I2C1_BASE 0x40098000 |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
So `I2C_PORT` eventually becomes a pointer to a struct that contains a pointer to hardware registers at address `0x40098000`!
|
|
|
|
### The Hardware Register Pointer
|
|
|
|
The `i2c_hw_t *hw` member points to the actual silicon:
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| Memory Map |
|
|
| |
|
|
| Address 0x40098000: I2C1 Hardware Registers |
|
|
| +------------------------------------------------------------+ |
|
|
| | Offset 0x00: IC_CON (Control register) | |
|
|
| | Offset 0x04: IC_TAR (Target address register) | |
|
|
| | Offset 0x10: IC_DATA_CMD (Data command register) | |
|
|
| | ... | |
|
|
| +------------------------------------------------------------+ |
|
|
| |
|
|
| The i2c_hw_t struct maps directly to these registers! |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
---
|
|
|
|
## Part 5: The ARM Calling Convention (AAPCS)
|
|
|
|
### How Arguments Are Passed
|
|
|
|
On ARM Cortex-M, the **ARM Architecture Procedure Call Standard (AAPCS)** defines how functions receive arguments:
|
|
|
|
| Register | Purpose |
|
|
| -------- | ---------------- |
|
|
| `r0` | First argument |
|
|
| `r1` | Second argument |
|
|
| `r2` | Third argument |
|
|
| `r3` | Fourth argument |
|
|
| Stack | Fifth+ arguments |
|
|
| `r0` | Return value |
|
|
|
|
### Example: i2c_init(i2c1, 100000)
|
|
|
|
```c
|
|
i2c_init(I2C_PORT, 100000);
|
|
```
|
|
|
|
In assembly:
|
|
```assembly
|
|
ldr r0, [address of i2c1_inst] ; r0 = pointer to struct (first arg)
|
|
ldr r1, =0x186A0 ; r1 = 100000 (second arg)
|
|
bl i2c_init ; Call the function
|
|
```
|
|
|
|
---
|
|
|
|
## Part 6: Setting Up Your Environment
|
|
|
|
### Prerequisites
|
|
|
|
Before we start, make sure you have:
|
|
1. A Raspberry Pi Pico 2 board
|
|
2. A Raspberry Pi Pico Debug Probe
|
|
3. OpenOCD installed and configured
|
|
4. GDB (`arm-none-eabi-gdb`) installed
|
|
5. Python installed (for UF2 conversion)
|
|
6. A serial monitor (PuTTY, minicom, or screen)
|
|
7. A 1602 LCD display with I2C backpack (PCF8574)
|
|
8. A hex editor (HxD, ImHex, or similar)
|
|
9. The sample project: `0x0017_constants`
|
|
|
|
### Hardware Setup
|
|
|
|
Connect your LCD like this:
|
|
|
|
| LCD Pin | Pico 2 Pin |
|
|
| ------- | ---------- |
|
|
| VCC | 3.3V or 5V |
|
|
| GND | GND |
|
|
| SDA | GPIO 2 |
|
|
| SCL | GPIO 3 |
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| I2C LCD Wiring |
|
|
| |
|
|
| Pico 2 1602 LCD + I2C Backpack |
|
|
| +----------+ +----------------------+ |
|
|
| | | | | |
|
|
| | GPIO 2 |------- SDA -----?| SDA | |
|
|
| | (SDA) | | | |
|
|
| | | | +------------+ | |
|
|
| | GPIO 3 |------- SCL -----?| SCL| Reverse | | |
|
|
| | (SCL) | | |Engineering | | |
|
|
| | | | +------------+ | |
|
|
| | 3.3V |------- VCC -----?| VCC | |
|
|
| | | | | |
|
|
| | GND |------- GND -----?| GND | |
|
|
| | | | | |
|
|
| +----------+ +----------------------+ |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
Embedded-Hacking/
|
|
+-- 0x0017_constants/
|
|
| +-- build/
|
|
| | +-- 0x0017_constants.uf2
|
|
| | +-- 0x0017_constants.bin
|
|
| +-- 0x0017_constants.c
|
|
| +-- lcd_1602.h
|
|
+-- uf2conv.py
|
|
```
|
|
|
|
---
|
|
|
|
## Part 7: Hands-On Tutorial - Constants and I2C LCD
|
|
|
|
### Step 1: Review the Source Code
|
|
|
|
Let's examine the constants code:
|
|
|
|
**File: `0x0017_constants.c`**
|
|
|
|
```c
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "pico/stdlib.h"
|
|
#include "hardware/i2c.h"
|
|
#include "lcd_1602.h"
|
|
|
|
#define FAV_NUM 42
|
|
#define I2C_PORT i2c1
|
|
#define I2C_SDA_PIN 2
|
|
#define I2C_SCL_PIN 3
|
|
|
|
const int OTHER_FAV_NUM = 1337;
|
|
|
|
int main(void) {
|
|
stdio_init_all();
|
|
|
|
i2c_init(I2C_PORT, 100000);
|
|
gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
|
|
gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
|
|
gpio_pull_up(I2C_SDA_PIN);
|
|
gpio_pull_up(I2C_SCL_PIN);
|
|
|
|
lcd_i2c_init(I2C_PORT, 0x27, 4, 0x08);
|
|
lcd_set_cursor(0, 0);
|
|
lcd_puts("Reverse");
|
|
lcd_set_cursor(1, 0);
|
|
lcd_puts("Engineering");
|
|
|
|
while (true) {
|
|
printf("FAV_NUM: %d\r\n", FAV_NUM);
|
|
printf("OTHER_FAV_NUM: %d\r\n", OTHER_FAV_NUM);
|
|
}
|
|
}
|
|
```
|
|
|
|
**What this code does:**
|
|
|
|
1. **Lines 7-10:** Define preprocessor macros for constants and I2C configuration
|
|
2. **Line 12:** Define a `const` variable stored in flash
|
|
3. **Line 15:** Initialize UART for serial output
|
|
4. **Lines 17-21:** Initialize I2C1 at 100kHz, configure GPIO pins, enable pull-ups
|
|
5. **Lines 23-27:** Initialize LCD and display "Reverse" on line 0, "Engineering" on line 1
|
|
6. **Lines 29-32:** Infinite loop printing both constant values to serial terminal
|
|
|
|
### 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 `0x0017_constants.uf2` onto the drive
|
|
5. The Pico will reboot and start running!
|
|
|
|
### Step 3: Verify It's Working
|
|
|
|
**Check the LCD:**
|
|
- Line 1 should show: `Reverse`
|
|
- Line 2 should show: `Engineering`
|
|
|
|
**Check the serial monitor (PuTTY/screen):**
|
|
```
|
|
FAV_NUM: 42
|
|
OTHER_FAV_NUM: 1337
|
|
FAV_NUM: 42
|
|
OTHER_FAV_NUM: 1337
|
|
...
|
|
```
|
|
|
|
---
|
|
|
|
## Part 8: Debugging with GDB (Dynamic Analysis)
|
|
|
|
> ? **REVIEW:** This setup is identical to previous weeks. If you need a refresher on OpenOCD and GDB connection, refer back to Week 3 Part 6.
|
|
|
|
### Starting the Debug Session
|
|
|
|
**Terminal 1 - Start OpenOCD:**
|
|
|
|
```powershell
|
|
openocd -s "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
|
|
```
|
|
|
|
**Terminal 2 - Start GDB:**
|
|
|
|
```cmd
|
|
arm-none-eabi-gdb build\0x0017_constants.elf
|
|
```
|
|
|
|
**Connect to target:**
|
|
|
|
```gdb
|
|
(gdb) target extended-remote :3333
|
|
(gdb) monitor reset halt
|
|
```
|
|
|
|
### Step 4: Examine Main Function
|
|
|
|
Let's examine the main function. Disassemble from the entry point:
|
|
|
|
```
|
|
x/54i 0x10000234
|
|
```
|
|
|
|
You should see output like:
|
|
|
|
```
|
|
(gdb) x/54i 0x10000234
|
|
0x10000234 <main>: push {r3, lr}
|
|
0x10000236 <main+2>: bl 0x100037fc <stdio_init_all>
|
|
0x1000023a <main+6>: ldr r1, [pc, #104] @ (0x100002a4 <main+112>)
|
|
0x1000023c <main+8>: ldr r0, [pc, #104] @ (0x100002a8 <main+116>)
|
|
0x1000023e <main+10>: bl 0x10003cdc <i2c_init>
|
|
0x10000242 <main+14>: movs r1, #3
|
|
0x10000244 <main+16>: movs r0, #2
|
|
0x10000246 <main+18>: bl 0x100008f0 <gpio_set_function>
|
|
0x1000024a <main+22>: movs r1, #3
|
|
0x1000024c <main+24>: mov r0, r1
|
|
0x1000024e <main+26>: bl 0x100008f0 <gpio_set_function>
|
|
0x10000252 <main+30>: movs r2, #0
|
|
0x10000254 <main+32>: movs r1, #1
|
|
0x10000256 <main+34>: movs r0, #2
|
|
0x10000258 <main+36>: bl 0x1000092c <gpio_set_pulls>
|
|
0x1000025c <main+40>: movs r2, #0
|
|
0x1000025e <main+42>: movs r1, #1
|
|
0x10000260 <main+44>: movs r0, #3
|
|
0x10000262 <main+46>: bl 0x1000092c <gpio_set_pulls>
|
|
0x10000266 <main+50>: movs r3, #8
|
|
0x10000268 <main+52>: movs r2, #4
|
|
0x1000026a <main+54>: movs r1, #39 @ 0x27
|
|
0x1000026c <main+56>: ldr r0, [pc, #56] @ (0x100002a8 <main+116>)
|
|
0x1000026e <main+58>: bl 0x100002bc <lcd_i2c_init>
|
|
0x10000272 <main+62>: movs r1, #0
|
|
0x10000274 <main+64>: mov r0, r1
|
|
0x10000276 <main+66>: bl 0x100006f4 <lcd_set_cursor>
|
|
0x1000027a <main+70>: ldr r0, [pc, #48] @ (0x100002ac <main+120>)
|
|
0x1000027c <main+72>: bl 0x100007f0 <lcd_puts>
|
|
0x10000280 <main+76>: movs r0, #1
|
|
0x10000282 <main+78>: movs r1, #0
|
|
0x10000284 <main+80>: bl 0x100006f4 <lcd_set_cursor>
|
|
0x10000288 <main+84>: ldr r0, [pc, #36] @ (0x100002b0 <main+124>)
|
|
0x1000028a <main+86>: bl 0x100007f0 <lcd_puts>
|
|
0x1000028e <main+90>: movs r1, #42 @ 0x2a
|
|
0x10000290 <main+92>: ldr r0, [pc, #32] @ (0x100002b4 <main+128>)
|
|
0x10000292 <main+94>: bl 0x1000398c <__wrap_printf>
|
|
0x10000296 <main+98>: movw r1, #1337 @ 0x539
|
|
0x1000029a <main+102>: ldr r0, [pc, #28] @ (0x100002b8 <main+132>)
|
|
0x1000029c <main+104>: bl 0x1000398c <__wrap_printf>
|
|
0x100002a0 <main+108>: b.n 0x1000028e <main+90>
|
|
0x100002a2 <main+110>: nop
|
|
0x100002a4 <main+112>: strh r0, [r4, #52] @ 0x34
|
|
0x100002a6 <main+114>: movs r1, r0
|
|
0x100002a8 <main+116>: lsls r4, r5, #24
|
|
0x100002aa <main+118>: movs r0, #0
|
|
0x100002ac <main+120>: subs r6, #232 @ 0xe8
|
|
0x100002ae <main+122>: asrs r0, r0, #32
|
|
0x100002b0 <main+124>: subs r6, #240 @ 0xf0
|
|
0x100002b2 <main+126>: asrs r0, r0, #32
|
|
0x100002b4 <main+128>: subs r6, #252 @ 0xfc
|
|
0x100002b6 <main+130>: asrs r0, r0, #32
|
|
0x100002b8 <main+132>: subs r7, #12
|
|
0x100002ba <main+134>: asrs r0, r0, #32
|
|
```
|
|
|
|
### Step 5: Set a Breakpoint at Main
|
|
|
|
```
|
|
b *0x10000234
|
|
c
|
|
```
|
|
|
|
### Step 6: Find the #define Constant (FAV_NUM)
|
|
|
|
Step through to the printf call and examine the registers:
|
|
|
|
```
|
|
x/20i 0x1000028e
|
|
```
|
|
|
|
Look for:
|
|
```
|
|
...
|
|
0x1000028e <main+90>: movs r1, #42 @ 0x2a
|
|
...
|
|
```
|
|
|
|
The `#define` constant is embedded directly as an immediate value in the instruction!
|
|
|
|
### Step 7: Find the const Variable (OTHER_FAV_NUM)
|
|
|
|
Continue examining the loop body:
|
|
|
|
```gdb
|
|
(gdb) x/5i 0x10000296
|
|
```
|
|
|
|
Look for this instruction:
|
|
|
|
```
|
|
...
|
|
0x10000296 <main+98>: movw r1, #1337 @ 0x539
|
|
...
|
|
```
|
|
|
|
**Surprise!** The `const` variable is ALSO embedded as an immediate value - not loaded from memory! The compiler saw that `OTHER_FAV_NUM` is never address-taken (`&OTHER_FAV_NUM` is never used), so it optimized the `const` the same way as `#define` - as a constant embedded directly in the instruction.
|
|
|
|
The difference is the instruction encoding:
|
|
- `FAV_NUM` (42): `movs r1, #0x2a` - 16-bit Thumb instruction (values 0-255)
|
|
- `OTHER_FAV_NUM` (1337): `movw r1, #0x539` - 32-bit Thumb-2 instruction (values 0-65535)
|
|
|
|
> Tip: **Why `movw` instead of `movs`?** The value 1337 doesn't fit in 8 bits (max 255), so the compiler uses `movw` (Move Wide) which can encode any 16-bit immediate (0-65535) in a 32-bit instruction.
|
|
|
|
### Step 8: Examine the Literal Pool
|
|
|
|
The literal pool after the loop contains addresses and constants that are too large for regular instruction immediates. Let's examine it:
|
|
|
|
```gdb
|
|
(gdb) x/6wx 0x100002a4
|
|
0x100002a4 <main+112>: 0x000186a0 0x2000062c 0x10003ee8 0x10003ef0
|
|
0x100002b4 <main+128>: 0x10003efc 0x10003f0c
|
|
```
|
|
|
|
These are the values that `ldr rN, [pc, #offset]` instructions load:
|
|
|
|
| Literal Pool Addr | Value | Used By |
|
|
| ----------------- | -------------- | ------------------------------ |
|
|
| `0x100002a4` | `0x000186A0` | I2C baudrate (100000) |
|
|
| `0x100002a8` | `0x2000062C` | &i2c1_inst (I2C struct in RAM) |
|
|
| `0x100002ac` | `0x10003EE8` | "Reverse" string address |
|
|
| `0x100002b0` | `0x10003EF0` | "Engineering" string address |
|
|
| `0x100002b4` | `0x10003EFC` | "FAV_NUM: %d\r\n" format str |
|
|
| `0x100002b8` | `0x10003F0C` | "OTHER_FAV_NUM: %d\r\n" fmt |
|
|
|
|
> Tip: **Why does the disassembly at `0x100002a4` show `strh r0, [r4, #52]` instead of data?** Same reason as Week 6 - GDB's `x/i` tries to decode raw data as instructions. Use `x/wx` to see the actual word values or we can also use `x/x`.
|
|
|
|
```gdb
|
|
(gdb) x/x 0x100002a4
|
|
0x100002a4 <main+112>: 0x000186a0
|
|
(gdb) x/x 0x100002a8
|
|
0x100002a8 <main+116>: 0x2000062c
|
|
(gdb) x/x 0x100002ac
|
|
0x100002ac <main+120>: 0x10003ee8
|
|
(gdb) x/x 0x100002b0
|
|
0x100002b0 <main+124>: 0x10003ef0
|
|
(gdb) x/x 0x100002b4
|
|
0x100002b4 <main+128>: 0x10003efc
|
|
(gdb) x/x 0x100002b8
|
|
0x100002b8 <main+132>: 0x10003f0c
|
|
```
|
|
|
|
### Step 9: Examine the I2C Struct
|
|
|
|
Find the i2c1_inst struct address loaded into r0 before i2c_init:
|
|
|
|
```
|
|
x/2wx 0x2000062c
|
|
```
|
|
|
|
You should see:
|
|
```
|
|
0x2000062c <i2c1_inst>: 0x40098000 0x00000000
|
|
```
|
|
|
|
### Step 10: Examine the LCD String Literals
|
|
|
|
Find the strings passed to lcd_puts:
|
|
|
|
```
|
|
x/s 0x10003ee8
|
|
```
|
|
|
|
Output:
|
|
```
|
|
0x10003ee8: "Reverse"
|
|
```
|
|
|
|
```
|
|
x/s 0x10003ef0
|
|
```
|
|
|
|
Output:
|
|
```
|
|
0x10003ef0: "Engineering"
|
|
```
|
|
|
|
### Step 11: Step Through I2C Initialization
|
|
|
|
Step through instructions and watch the I2C setup:
|
|
|
|
```gdb
|
|
(gdb) b *0x1000023e
|
|
(gdb) c
|
|
(gdb) i r r0 r1
|
|
r0 0x2000062c 536872492
|
|
r1 0x186a0 100000
|
|
```
|
|
|
|
---
|
|
|
|
## Part 9: Understanding the Assembly
|
|
|
|
Now that we've explored the binary in GDB, let's make sense of the key patterns we found.
|
|
|
|
### Step 12: Analyze #define vs const in Assembly
|
|
|
|
From GDB, we discovered something interesting - **both constants ended up as instruction immediates!**
|
|
|
|
**For FAV_NUM (42) - a `#define` macro:**
|
|
```
|
|
0x1000028e <+90>: movs r1, #42 @ 0x2a
|
|
```
|
|
|
|
The value 42 is embedded directly in a 16-bit Thumb instruction. This is expected - `#define` is text replacement, so the compiler never sees `FAV_NUM`, only `42`.
|
|
|
|
**For OTHER_FAV_NUM (1337) - a `const` variable:**
|
|
```
|
|
0x10000296 <+98>: movw r1, #1337 @ 0x539
|
|
```
|
|
|
|
The value 1337 is ALSO embedded directly in an instruction - but this time a 32-bit Thumb-2 `movw` because the value doesn't fit in 8 bits.
|
|
|
|
**Why wasn't `const` stored in memory?** In theory, `const int OTHER_FAV_NUM = 1337` creates a variable in the `.rodata` section. But the compiler optimized it away because:
|
|
1. We never take the address of `OTHER_FAV_NUM` (no `&OTHER_FAV_NUM`)
|
|
2. The value fits in a 16-bit `movw` immediate
|
|
3. Loading from an immediate is faster than loading from memory
|
|
|
|
> Tip: **Key takeaway for reverse engineering:** Don't assume `const` variables will appear as memory loads. Modern compilers aggressively inline constant values. The C keyword `const` is a **source-level** concept - the compiler may or may not honor it in the final binary.
|
|
|
|
### Step 13: Analyze the I2C Struct Layout
|
|
|
|
In GDB, we examined the `i2c1_inst` struct at `0x2000062c`:
|
|
|
|
```gdb
|
|
(gdb) x/2wx 0x2000062c
|
|
0x2000062c <i2c1_inst>: 0x40098000 0x00000000
|
|
```
|
|
|
|
This maps to the `i2c_inst_t` struct:
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| i2c_inst_t at 0x2000062c |
|
|
| |
|
|
| +------------------------------------------------------------+ |
|
|
| | Offset Type Name Value | |
|
|
| | 0x00 i2c_hw_t * hw 0x40098000 | |
|
|
| | 0x04 bool restart_on_next 0x00 (false) | |
|
|
| +------------------------------------------------------------+ |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
The first member (`hw`) points to `0x40098000` - the I2C1 hardware register base. This is the end of the macro chain: `I2C_PORT` -> `i2c1` -> `&i2c1_inst` -> `hw` -> `0x40098000`.
|
|
|
|
### Step 14: Locate the String Literals
|
|
|
|
We found the LCD strings in flash memory:
|
|
|
|
```gdb
|
|
(gdb) x/s 0x10003ee8
|
|
0x10003ee8: "Reverse"
|
|
|
|
(gdb) x/s 0x10003ef0
|
|
0x10003ef0: "Engineering"
|
|
```
|
|
|
|
These are stored consecutively in the `.rodata` section. Note the addresses - we'll need them for patching.
|
|
|
|
---
|
|
|
|
## Part 10: Hacking the Binary with a Hex Editor
|
|
|
|
Now for the fun part - we'll patch the `.bin` file directly using a hex editor!
|
|
|
|
> Tip: **Why a hex editor?** GDB **cannot write to flash memory** - the `0x10000000+` address range where program instructions and read-only data live. Trying `set *(char *)0x1000028e = 0x2b` in GDB gives `Writing to flash memory forbidden in this context`. To make **permanent** patches that survive a power cycle, we edit the `.bin` file directly with a hex editor and re-flash it.
|
|
|
|
### Step 15: Open the Binary in a Hex Editor
|
|
|
|
1. Open **HxD** (or your preferred hex editor: ImHex, 010 Editor, etc.)
|
|
2. Click **File** -> **Open**
|
|
3. Navigate to `C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0017_constants\build\`
|
|
4. Open `0x0017_constants.bin`
|
|
|
|
### Step 16: Calculate the File Offset
|
|
|
|
The binary is loaded at base address `0x10000000`. To find the file offset of any address:
|
|
|
|
```
|
|
file_offset = address - 0x10000000
|
|
```
|
|
|
|
For example:
|
|
- Address `0x1000028e` -> file offset `0x28E` (654 in decimal)
|
|
- Address `0x10003ee8` -> file offset `0x3EE8` (16104 in decimal)
|
|
|
|
### Step 17: Understand FAV_NUM Encoding (movs - 16-bit Thumb)
|
|
|
|
From our GDB analysis, we know the instruction at `0x1000028e` is:
|
|
|
|
```
|
|
movs r1, #0x2a -> bytes: 2a 21
|
|
```
|
|
|
|
In HxD, use **Ctrl+G** to navigate to file offset `28E` and verify you see the byte `2A` followed by `21`.
|
|
|
|
> ?? **How Thumb encoding works:** In `movs r1, #imm8`, the immediate value is the first byte, and the opcode `21` is the second byte. So the bytes `2a 21` encode `movs r1, #0x2a` (42). If you wanted to change this to 43, you'd change `2A` to `2B`.
|
|
|
|
### Step 18: Understand OTHER_FAV_NUM Encoding (movw - 32-bit Thumb-2)
|
|
|
|
From GDB, we found the `movw r1, #1337` instruction at `0x10000296`. Examine the exact bytes:
|
|
|
|
```gdb
|
|
(gdb) x/4bx 0x10000296
|
|
0x10000296 <main+98>: 0x40 0xf2 0x39 0x51
|
|
```
|
|
|
|
This is the 32-bit Thumb-2 encoding of `movw r1, #0x539` (1337). The bytes break down as:
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| movw r1, #0x539 -> bytes: 40 F2 39 51 |
|
|
| |
|
|
| Byte 0: 0x40 -?? |
|
|
| Byte 1: 0xF2 -+ First halfword (opcode + upper imm bits) |
|
|
| Byte 2: 0x39 ---- Lower 8 bits of immediate (imm8) ?? CHANGE |
|
|
| Byte 3: 0x51 ---- Destination register (r1) + upper imm bits |
|
|
| |
|
|
| imm16 = 0x0539 = 1337 decimal |
|
|
| imm8 field = 0x39 (lower 8 bits of the value) |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
The file offset is `0x10000296 - 0x10000000 = 0x296`. The imm8 byte is the 3rd byte of the instruction: `0x296 + 2 = 0x298`.
|
|
|
|
To change `movw r1, #1337` to `movw r1, #1344`:
|
|
|
|
1. In HxD, press **Ctrl+G** (Go to offset)
|
|
2. Enter offset: `298` (the third byte of the 4-byte instruction)
|
|
3. You should see the byte `39` at this position
|
|
4. Change `39` to `40`
|
|
|
|
> ?? **Why offset `0x298` and not `0x296`?** The lower 8 bits of the immediate (`imm8`) are in the **third byte** of the 4-byte `movw` instruction. The instruction starts at file offset `0x296`, so imm8 is at `0x296 + 2 = 0x298`. Changing `0x39` to `0x40` changes the value from `0x539` (1337) to `0x540` (1344).
|
|
|
|
### Step 19: Hack - Change LCD Text from "Reverse" to "Exploit"
|
|
|
|
**IMPORTANT:** The new string must be the **same length** as the original! "Reverse" and "Exploit" are both 7 characters - perfect!
|
|
|
|
From our GDB analysis in Step 10, we found the string at `0x10003ee8`. File offset = `0x10003ee8 - 0x10000000 = 0x3EE8`.
|
|
|
|
1. In HxD, press **Ctrl+G** and enter offset: `3EE8`
|
|
2. You should see the bytes for "Reverse": `52 65 76 65 72 73 65 00`
|
|
3. Change the bytes to spell "Exploit": `45 78 70 6c 6f 69 74 00`
|
|
|
|
**ASCII Reference:**
|
|
|
|
| Character | Hex |
|
|
| --------- | ------ |
|
|
| E | `0x45` |
|
|
| x | `0x78` |
|
|
| p | `0x70` |
|
|
| l | `0x6c` |
|
|
| o | `0x6f` |
|
|
| i | `0x69` |
|
|
| t | `0x74` |
|
|
|
|
### Step 20: Save the Patched Binary
|
|
|
|
1. Click **File** -> **Save As**
|
|
2. Save as `0x0017_constants-h.bin` in the build directory
|
|
3. Close the hex editor
|
|
|
|
---
|
|
|
|
## Part 11: Converting and Flashing the Hacked Binary
|
|
|
|
### Step 21: Convert to UF2 Format
|
|
|
|
Open a terminal and navigate to your project directory:
|
|
|
|
```cmd
|
|
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0017_constants
|
|
```
|
|
|
|
Run the conversion command:
|
|
|
|
```cmd
|
|
python ..\uf2conv.py build\0x0017_constants-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
|
|
```
|
|
|
|
### Step 22: 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. Check your LCD and serial monitor
|
|
|
|
### Step 23: Verify the Hack
|
|
|
|
**Check the LCD:**
|
|
- Line 1 should now show: `Exploit` (instead of "Reverse")
|
|
- Line 2 should still show: `Engineering`
|
|
|
|
**Check the serial monitor:**
|
|
```
|
|
FAV_NUM: 42
|
|
OTHER_FAV_NUM: 1337
|
|
FAV_NUM: 42
|
|
OTHER_FAV_NUM: 1337
|
|
...
|
|
```
|
|
|
|
The numbers are unchanged - we only patched the LCD string!
|
|
|
|
**BOOM! We successfully changed the LCD text from "Reverse" to "Exploit" without access to the source code!**
|
|
|
|
---
|
|
|
|
## Part 12: Summary and Review
|
|
|
|
### What We Accomplished
|
|
|
|
1. **Learned about constants** - `#define` macros vs `const` variables
|
|
2. **Understood I2C communication** - Two-wire protocol for peripheral communication
|
|
3. **Explored C structs** - How the Pico SDK abstracts hardware
|
|
4. **Mastered the macro chain** - From `I2C_PORT` to `0x40098000`
|
|
5. **Examined structs in GDB** - Inspected memory layout of `i2c_inst_t`
|
|
6. **Analyzed instruction encodings** - Both `movs` (8-bit) and `movw` (16-bit) immediates in the hex editor
|
|
7. **Patched a string literal** - Changed LCD display text from "Reverse" to "Exploit"
|
|
|
|
### #define vs const Summary
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| #define FAV_NUM 42 |
|
|
| ------------------- |
|
|
| - Text replacement at compile time |
|
|
| - No memory allocated |
|
|
| - Cannot take address (&FAV_NUM is invalid) |
|
|
| - In binary: value appears as immediate (movs r1, #0x2a) |
|
|
| - To hack: patch the instruction operand |
|
|
+-----------------------------------------------------------------+
|
|
| const int OTHER_FAV_NUM = 1337 |
|
|
| ------------------------------ |
|
|
| - Theoretically in .rodata, but compiler optimized it away |
|
|
| - Value embedded as immediate: movw r1, #0x539 (32-bit instr) |
|
|
| - Optimization: compiler saw &OTHER_FAV_NUM is never used |
|
|
| - In binary: immediate in instruction, same as #define! |
|
|
| - To hack: patch instruction operand (imm8 byte at offset +2) |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### I2C Configuration Summary
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| I2C Setup Steps |
|
|
| |
|
|
| 1. i2c_init(i2c1, 100000) - Initialize at 100kHz |
|
|
| 2. gpio_set_function(pin, I2C) - Assign pins to I2C |
|
|
| 3. gpio_pull_up(sda_pin) - Enable SDA pull-up |
|
|
| 4. gpio_pull_up(scl_pin) - Enable SCL pull-up |
|
|
| 5. lcd_i2c_init(...) - Initialize the device |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### The Struct Chain
|
|
|
|
```
|
|
+-----------------------------------------------------------------+
|
|
| I2C_PORT -> i2c1 -> &i2c1_inst -> i2c_inst_t |
|
|
| | |
|
|
| +-- hw -> i2c_hw_t * |
|
|
| | +-- 0x40098000 |
|
|
| | |
|
|
| +-- restart_on_next (bool) |
|
|
| |
|
|
+-----------------------------------------------------------------+
|
|
```
|
|
|
|
### Key Memory Addresses
|
|
|
|
| Address | Description |
|
|
| ------------ | ---------------------------------- |
|
|
| `0x10000234` | main() entry point |
|
|
| `0x1000028e` | FAV_NUM value in instruction |
|
|
| `0x10000296` | OTHER_FAV_NUM value in instruction |
|
|
| `0x10003ee8` | "Reverse" string literal (example) |
|
|
| `0x40098000` | I2C1 hardware registers base |
|
|
| `0x2000062C` | i2c1_inst struct in SRAM |
|
|
|
|
---
|
|
|
|
---
|
|
|
|
## Key Takeaways
|
|
|
|
1. **#define is text replacement** - It happens before compilation, no memory used.
|
|
|
|
2. **const creates real variables** - Stored in .rodata, takes memory, has an address.
|
|
|
|
3. **I2C uses two wires** - SDA for data, SCL for clock, pull-ups required.
|
|
|
|
4. **Structs group related data** - The SDK uses them to abstract hardware.
|
|
|
|
5. **Macros can chain** - `I2C_PORT` -> `i2c1` -> `&i2c1_inst` -> hardware pointer.
|
|
|
|
6. **ARM passes args in registers** - r0-r3 for first four arguments.
|
|
|
|
7. **GDB reveals struct layouts** - Examine memory to understand data organization.
|
|
|
|
8. **String hacking requires same length** - Or you'll corrupt adjacent data!
|
|
|
|
9. **Constants aren't constant** - With binary patching, everything can change!
|
|
|
|
10. **Compiler optimization changes code** - `gpio_pull_up` becomes `gpio_set_pulls`.
|
|
|
|
---
|
|
|
|
## Glossary
|
|
|
|
| Term | Definition |
|
|
| ----------------------- | --------------------------------------------------- |
|
|
| **#define** | Preprocessor directive for text replacement |
|
|
| **AAPCS** | ARM Architecture Procedure Call Standard |
|
|
| **const** | Keyword marking a variable as read-only |
|
|
| **Forward Declaration** | Telling compiler a type exists before defining it |
|
|
| **I2C** | Inter-Integrated Circuit - two-wire serial protocol |
|
|
| **Immediate Value** | A constant embedded directly in an instruction |
|
|
| **Open-Drain** | Output that can only pull low, not drive high |
|
|
| **PCF8574** | Common I2C I/O expander chip used in LCD backpacks |
|
|
| **Preprocessor** | Tool that processes code before compilation |
|
|
| **Pull-Up Resistor** | Resistor that holds a line HIGH by default |
|
|
| **SCL** | Serial Clock - I2C timing signal |
|
|
| **SDA** | Serial Data - I2C data line |
|
|
| **Struct** | User-defined type grouping related variables |
|
|
| **typedef** | Creates an alias for a type |
|
|
|
|
---
|
|
|
|
## Additional Resources
|
|
|
|
### I2C Timing Reference
|
|
|
|
| Speed Mode | Maximum Frequency |
|
|
| ---------- | ----------------- |
|
|
| Standard | 100 kHz |
|
|
| Fast | 400 kHz |
|
|
| Fast Plus | 1 MHz |
|
|
|
|
### Common I2C Addresses
|
|
|
|
| Device | Address |
|
|
| --------------------- | ------------- |
|
|
| PCF8574 LCD (default) | `0x27` |
|
|
| PCF8574A LCD | `0x3F` |
|
|
| DS3231 RTC | `0x68` |
|
|
| BMP280 Sensor | `0x76`/`0x77` |
|
|
| SSD1306 OLED | `0x3C`/`0x3D` |
|
|
|
|
### Key ARM Instructions for Constants
|
|
|
|
| Instruction | Description |
|
|
| -------------------- | ------------------------------------------- |
|
|
| `movs rN, #imm` | Load small immediate (0-255) directly |
|
|
| `ldr rN, [pc, #off]` | Load larger value from literal pool |
|
|
| `ldr rN, =value` | Pseudo-instruction for loading any constant |
|
|
|
|
### RP2350 I2C Memory Map
|
|
|
|
| Address | Description |
|
|
| ------------ | ----------------------- |
|
|
| `0x40090000` | I2C0 hardware registers |
|
|
| `0x40098000` | I2C1 hardware registers |
|
|
|
|
---
|
|
|
|
**Remember:** When you see complex nested structures in a binary, take your time to understand the hierarchy. Use GDB to examine struct layouts in memory and trace pointer chains. And always remember - even "constants" can be hacked!
|
|
|
|
Happy hacking! ? |