57 KiB
Week 11: Structures and Functions in Embedded Systems: Debugging and Hacking w/ IR Remote Control and NEC Protocol Basics
🎯 What You'll Learn This Week
By the end of this tutorial, you will be able to:
- Understand C structures (structs) and how they organize related data
- Know how structs are represented in memory and assembly code
- Understand the NEC infrared (IR) protocol for remote control communication
- Create and use functions with parameters and return values
- Identify struct member access patterns in Ghidra
- Recognize how compilers "flatten" structs into individual operations
- Hack GPIO pin assignments to swap LED behavior
- Understand the security implications of log/behavior desynchronization
- Analyze .elf files in addition to .bin files in Ghidra
📚 Part 1: Understanding C Structures (Structs)
What is a Struct?
A structure (or struct) is a user-defined data type that groups related variables together under one name. Think of it like a form with multiple fields - each field can hold different types of data, but they all belong together.
// Define a struct type
typedef struct {
uint8_t led1_pin; // GPIO pin for LED 1
uint8_t led2_pin; // GPIO pin for LED 2
uint8_t led3_pin; // GPIO pin for LED 3
bool led1_state; // Is LED 1 on?
bool led2_state; // Is LED 2 on?
bool led3_state; // Is LED 3 on?
} simple_led_ctrl_t;
┌─────────────────────────────────────────────────────────────────┐
│ Structure as a Container │
│ │
│ simple_led_ctrl_t leds │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ led1_pin: 16 led2_pin: 17 led3_pin: 18 ││
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ││
│ │ │ 16 │ │ 17 │ │ 18 │ ││
│ │ └────────┘ └────────┘ └────────┘ ││
│ │ ││
│ │ led1_state: false led2_state: false led3_state: false ││
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ││
│ │ │ false │ │ false │ │ false │ ││
│ │ └────────┘ └────────┘ └────────┘ ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ All 6 members live together as ONE variable called "leds" │
│ │
└─────────────────────────────────────────────────────────────────┘
Why Use Structs?
| Without Structs (Messy!) | With Structs (Clean!) |
|---|---|
uint8_t led1_pin = 16; |
simple_led_ctrl_t leds; |
uint8_t led2_pin = 17; |
leds.led1_pin = 16; |
uint8_t led3_pin = 18; |
leds.led2_pin = 17; |
bool led1_state = false; |
leds.led3_pin = 18; |
bool led2_state = false; |
leds.led1_state = false; |
bool led3_state = false; |
... (all in one container!) |
Benefits of Structs:
- Organization - Related data stays together
- Readability - Code is easier to understand
- Maintainability - Changes are easier to make
- Scalability - Easy to add more LEDs or features
- Passing to Functions - Pass one struct instead of many variables
📚 Part 2: Struct Memory Layout
How Structs are Stored in Memory
When you create a struct, the compiler places each member in consecutive memory locations:
┌─────────────────────────────────────────────────────────────────┐
│ Memory Layout of simple_led_ctrl_t │
│ │
│ Address Member Size Value │
│ ───────────────────────────────────────────────────────────── │
│ 0x2000000 led1_pin 1 byte 16 (0x10) │
│ 0x2000001 led2_pin 1 byte 17 (0x11) │
│ 0x2000002 led3_pin 1 byte 18 (0x12) │
│ 0x2000003 led1_state 1 byte 0 (false) │
│ 0x2000004 led2_state 1 byte 0 (false) │
│ 0x2000005 led3_state 1 byte 0 (false) │
│ │
│ Total struct size: 6 bytes │
│ │
└─────────────────────────────────────────────────────────────────┘
Accessing Struct Members
Use the dot operator (.) to access members:
simple_led_ctrl_t leds;
// Set values
leds.led1_pin = 16;
leds.led1_state = true;
// Read values
printf("Pin: %d\n", leds.led1_pin);
Pointer to Struct (Arrow Operator)
When you have a pointer to a struct, use the arrow operator (->):
simple_led_ctrl_t leds;
simple_led_ctrl_t *ptr = &leds; // Pointer to the struct
// These are equivalent:
leds.led1_pin = 16; // Using dot with struct variable
ptr->led1_pin = 16; // Using arrow with pointer
(*ptr).led1_pin = 16; // Dereferencing then dot (same thing)
┌─────────────────────────────────────────────────────────────────┐
│ Dot vs Arrow Operator │
│ │
│ struct_variable.member ◄── Use with actual struct │
│ │
│ pointer_to_struct->member ◄── Use with pointer to struct │
│ │
│ The arrow (->) is shorthand for (*pointer).member │
│ │
└─────────────────────────────────────────────────────────────────┘
📚 Part 3: Designated Initializers
Clean Struct Initialization
C allows you to initialize struct members by name using designated initializers:
simple_led_ctrl_t leds = {
.led1_pin = 16,
.led2_pin = 17,
.led3_pin = 18,
.led1_state = false,
.led2_state = false,
.led3_state = false
};
Benefits:
- Clear which value goes to which member
- Order doesn't matter (can rearrange lines)
- Self-documenting code
- Easy to add new members later
📚 Part 4: Understanding the NEC IR Protocol
What is Infrared (IR) Communication?
Infrared communication uses invisible light pulses to send data. Your TV remote uses IR to send commands to your TV. The LED in the remote flashes on and off very quickly in specific patterns that represent different buttons.
┌─────────────────────────────────────────────────────────────────┐
│ IR Communication │
│ │
│ Remote Control IR Receiver │
│ ┌──────────┐ ┌──────────┐ │
│ │ Button │ │ │ │
│ │ 1 │ ─── IR Light Pulses ──► │ ████ │ │
│ │ ┌───┐ │ ~~~~~~~~~~~~► │ Sensor │ │
│ │ │ ● │ │ │ │ │
│ │ └───┘ │ └────┬─────┘ │
│ │ IR LED │ │ │
│ └──────────┘ ▼ │
│ GPIO Pin │
│ (Digital signal) │
│ │
└─────────────────────────────────────────────────────────────────┘
The NEC Protocol
NEC is one of the most common IR protocols. When you press a button, the remote sends:
- Leader pulse - 9ms HIGH, 4.5ms LOW (says "attention!")
- Address - 8 bits identifying the device
- Address Inverse - 8 bits (for error checking)
- Command - 8 bits for the button pressed
- Command Inverse - 8 bits (for error checking)
┌─────────────────────────────────────────────────────────────────┐
│ NEC Protocol Frame │
│ │
│ ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │
│ │ Leader │ Address │ Address │ Command │ Command │ Stop │ │
│ │ Pulse │ 8-bit │ Inverse │ 8-bit │ Inverse │ Bit │ │
│ │ 9+4.5ms │ │ 8-bit │ │ 8-bit │ │ │
│ └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ │
│ │
│ Total: 32 bits of data (+ leader + stop) │
│ │
└─────────────────────────────────────────────────────────────────┘
NEC Command Codes for Our Remote
| Button | NEC Command Code | Hex Value |
|---|---|---|
| 1 | 0x0C | 12 |
| 2 | 0x18 | 24 |
| 3 | 0x5E | 94 |
Note: Different remotes have different codes. These are specific to our example remote.
📚 Part 5: Understanding Functions in C
What is a Function?
A function is a reusable block of code that performs a specific task. Functions help organize code and avoid repetition.
// Function definition
int add_numbers(int a, int b) {
return a + b;
}
// Function call
int result = add_numbers(5, 3); // result = 8
Function Components
┌─────────────────────────────────────────────────────────────────┐
│ Anatomy of a Function │
│ │
│ return_type function_name ( parameters ) { │
│ // function body │
│ return value; │
│ } │
│ │
│ Example: │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ int ir_to_led_number ( int ir_command ) { ││
│ │ ─── ─────────────── ─────────────── ││
│ │ │ │ │ ││
│ │ │ │ └── Parameter (input) ││
│ │ │ └── Function name ││
│ │ └── Return type (what it gives back) ││
│ │ ││
│ │ if (ir_command == 0x0C) return 1; ◄── Body ││
│ │ if (ir_command == 0x18) return 2; ││
│ │ return 0; ◄── Return value ││
│ │ } ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────┘
Types of Functions
| Type | Description | Example |
|---|---|---|
| No params, no return | Just does something | void leds_all_off(void) |
| With params, no return | Takes input, no output | void blink_led(pin, count) |
| No params, with return | No input, gives output | int ir_getkey(void) |
| With params, with return | Takes input, gives output | int ir_to_led_number(cmd) |
📚 Part 6: Functions with Struct Pointers
Passing Structs to Functions
When passing a struct to a function, you usually pass a pointer to avoid copying all the data:
// Function takes a POINTER to the struct
void leds_all_off(simple_led_ctrl_t *leds) {
gpio_put(leds->led1_pin, false); // Use arrow operator!
gpio_put(leds->led2_pin, false);
gpio_put(leds->led3_pin, false);
}
// Call with address-of operator
simple_led_ctrl_t my_leds;
leds_all_off(&my_leds); // Pass the ADDRESS of my_leds
┌─────────────────────────────────────────────────────────────────┐
│ Passing Struct by Pointer │
│ │
│ main() { │
│ simple_led_ctrl_t leds; ◄── Struct lives here │
│ leds_all_off(&leds); ◄── Pass ADDRESS (pointer) │
│ } │ │
│ │ │
│ ▼ │
│ leds_all_off(simple_led_ctrl_t *leds) { │
│ gpio_put(leds->led1_pin, false); │
│ ──── │
│ │ │
│ └── Arrow because leds is a POINTER │
│ } │
│ │
│ WHY use pointers? │
│ • Efficient: Only 4 bytes (address) instead of entire struct │
│ • Allows modification: Function can change the original │
│ │
└─────────────────────────────────────────────────────────────────┘
📚 Part 7: How Compilers Handle Structs
Struct "Flattening" in Assembly
When the compiler converts your C code to assembly, it "flattens" struct operations into individual memory accesses:
C Code:
gpio_init(leds.led1_pin); // leds.led1_pin = 16
gpio_init(leds.led2_pin); // leds.led2_pin = 17
gpio_init(leds.led3_pin); // leds.led3_pin = 18
Assembly (what the compiler produces):
movs r0, #0x10 ; r0 = 16 (led1_pin value)
bl gpio_init ; call gpio_init(16)
movs r0, #0x11 ; r0 = 17 (led2_pin value)
bl gpio_init ; call gpio_init(17)
movs r0, #0x12 ; r0 = 18 (led3_pin value)
bl gpio_init ; call gpio_init(18)
┌─────────────────────────────────────────────────────────────────┐
│ Struct Flattening │
│ │
│ C Level (High-level abstraction): │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ gpio_init(leds.led1_pin); ││
│ │ gpio_init(leds.led2_pin); ││
│ │ gpio_init(leds.led3_pin); ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ │
│ │ Compiler transforms │
│ ▼ │
│ Assembly Level (Flattened): │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ movs r0, #16 ; Just the VALUE, no struct reference ││
│ │ bl gpio_init ││
│ │ movs r0, #17 ; Next value directly ││
│ │ bl gpio_init ││
│ │ movs r0, #18 ; Next value directly ││
│ │ bl gpio_init ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ The struct abstraction DISAPPEARS at the assembly level! │
│ We just see individual values being loaded and used. │
│ │
└─────────────────────────────────────────────────────────────────┘
Why This Matters for Reverse Engineering
- In Ghidra, you won't always see "struct" - just individual values
- You must recognize PATTERNS (sequential values like 16, 17, 18)
- Understanding flattening helps you reconstruct the original struct
📚 Part 8: Setting Up Your Environment
Prerequisites
Before we start, make sure you have:
- A Raspberry Pi Pico 2 board
- A Raspberry Pi Pico Debug Probe
- Ghidra installed (for static analysis)
- Python installed (for UF2 conversion)
- A serial monitor (PuTTY, minicom, or screen)
- An IR receiver module (like VS1838B)
- An IR remote control (any NEC-compatible remote)
- Three LEDs (red, green, yellow) with resistors
- The sample projects:
0x0023_structuresand0x0026_functions
Hardware Setup
IR Receiver Wiring:
| IR Receiver Pin | Pico 2 Pin |
|---|---|
| VCC | 3.3V |
| GND | GND |
| OUT/DATA | GPIO 5 |
LED Wiring:
| LED | GPIO Pin | Resistor |
|---|---|---|
| Red | GPIO 16 | 220Ω-330Ω |
| Green | GPIO 17 | 220Ω-330Ω |
| Yellow | GPIO 18 | 220Ω-330Ω |
┌─────────────────────────────────────────────────────────────────┐
│ Complete Wiring Diagram │
│ │
│ Pico 2 Components │
│ ┌──────────┐ │
│ │ │ ┌─────────────┐ │
│ │ GPIO 5 │──────────────┤ IR Receiver │ │
│ │ │ │ (VS1838B) │ │
│ │ │ └──────┬──────┘ │
│ │ │ │ │
│ │ GPIO 16 │───[220Ω]───(RED LED)────┐ │
│ │ │ │ │
│ │ GPIO 17 │───[220Ω]───(GRN LED)────┤ │
│ │ │ │ │
│ │ GPIO 18 │───[220Ω]───(YEL LED)────┤ │
│ │ │ │ │
│ │ 3.3V │─────────────────────────┼── IR VCC │
│ │ │ │ │
│ │ GND │─────────────────────────┴── All GNDs │
│ │ │ │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Project Structure
Embedded-Hacking/
├── 0x0023_structures/
│ ├── build/
│ │ ├── 0x0023_structures.uf2
│ │ └── 0x0023_structures.bin
│ ├── main/
│ │ └── 0x0023_structures.c
│ └── ir.h
├── 0x0026_functions/
│ ├── build/
│ │ ├── 0x0026_functions.uf2
│ │ ├── 0x0026_functions.bin
│ │ └── 0x0026_functions.elf
│ ├── main/
│ │ └── 0x0026_functions.c
│ └── ir.h
└── uf2conv.py
🔬 Part 9: Hands-On Tutorial - Structures Code
Step 1: Review the Source Code
Let's examine the structures code:
File: 0x0023_structures.c
#include <stdio.h>
#include <stdbool.h>
#include "pico/stdlib.h"
#include "ir.h"
#define IR_PIN 5
typedef struct {
uint8_t led1_pin;
uint8_t led2_pin;
uint8_t led3_pin;
bool led1_state;
bool led2_state;
bool led3_state;
} simple_led_ctrl_t;
int main(void) {
stdio_init_all();
simple_led_ctrl_t leds = {
.led1_pin = 16,
.led2_pin = 17,
.led3_pin = 18,
.led1_state = false,
.led2_state = false,
.led3_state = false
};
gpio_init(leds.led1_pin); gpio_set_dir(leds.led1_pin, GPIO_OUT);
gpio_init(leds.led2_pin); gpio_set_dir(leds.led2_pin, GPIO_OUT);
gpio_init(leds.led3_pin); gpio_set_dir(leds.led3_pin, GPIO_OUT);
ir_init(IR_PIN);
printf("IR receiver on GPIO %d ready\n", IR_PIN);
while (true) {
int key = ir_getkey();
if (key >= 0) {
printf("NEC command: 0x%02X\n", key);
// Turn all off first
leds.led1_state = false;
leds.led2_state = false;
leds.led3_state = false;
// Check NEC codes
if (key == 0x0C) leds.led1_state = true; // GPIO16
if (key == 0x18) leds.led2_state = true; // GPIO17
if (key == 0x5E) leds.led3_state = true; // GPIO18
// Apply states
gpio_put(leds.led1_pin, leds.led1_state);
gpio_put(leds.led2_pin, leds.led2_state);
gpio_put(leds.led3_pin, leds.led3_state);
sleep_ms(10);
} else {
sleep_ms(1);
}
}
}
Step 2: Understand the Program Flow
┌─────────────────────────────────────────────────────────────────┐
│ Program Flow │
│ │
│ 1. Initialize UART (stdio_init_all) │
│ 2. Create LED struct with pins 16, 17, 18 │
│ 3. Initialize GPIO pins as outputs │
│ 4. Initialize IR receiver on GPIO 5 │
│ 5. Enter infinite loop: │
│ a. Check for IR key press │
│ b. If key received: │
│ - Print the NEC command code │
│ - Turn all LEDs off │
│ - Check which button: 0x0C, 0x18, or 0x5E │
│ - Turn on the matching LED │
│ - Apply states to GPIO pins │
│ c. Sleep briefly and repeat │
│ │
└─────────────────────────────────────────────────────────────────┘
Step 3: Flash the Binary to Your Pico 2
- Hold the BOOTSEL button on your Pico 2
- Plug in the USB cable (while holding BOOTSEL)
- Release BOOTSEL - a drive called "RPI-RP2" appears
- Drag and drop
0x0023_structures.uf2onto the drive - The Pico will reboot and start running!
Step 4: Verify It's Working
Open PuTTY (115200 baud) and test:
- Press "1" on remote → Red LED lights, terminal shows
NEC command: 0x0C - Press "2" on remote → Green LED lights, terminal shows
NEC command: 0x18 - Press "3" on remote → Yellow LED lights, terminal shows
NEC command: 0x5E
🔬 Part 10: Debugging with GDB (Structures)
Step 5: Start OpenOCD (Terminal 1)
Open a terminal and start OpenOCD:
openocd ^
-s "C:\Users\flare-vm\.pico-sdk\openocd\0.12.0+dev\scripts" ^
-f interface/cmsis-dap.cfg ^
-f target/rp2350.cfg ^
-c "adapter speed 5000"
You should see output indicating OpenOCD connected successfully to your Pico 2 via the Debug Probe.
Step 6: Start GDB (Terminal 2)
Open a new terminal and launch GDB with the binary:
arm-none-eabi-gdb build\0x0023_structures.elf
Step 7: Connect to the Remote Target
In GDB, connect to OpenOCD:
target remote :3333
Step 8: Halt the Running Binary
Stop the processor:
monitor halt
Step 9: Examine Main Function
Disassemble around main to see struct initialization:
disassemble 0x10000234,+200
Look for the struct member initialization sequence (mov instructions with values 16, 17, 18).
Step 10: Set a Breakpoint at Main
break *0x10000234
Reset and run to hit the breakpoint:
monitor reset halt
continue
Step 11: Examine the Struct on the Stack
After stepping into main, the struct is initialized on the stack. Examine it:
stepi 20
x/6xb $sp
You should see the struct layout: 10 11 12 00 00 00 (pins 16, 17, 18 and three false states).
Step 12: Watch GPIO Initialization
Set a breakpoint on gpio_init and watch each LED pin get initialized:
break *0x10000260
continue
info registers r0
You should see r0 = 0x10 (16), 0x11 (17), 0x12 (18) for each call.
Step 13: Examine IR Key Processing
Set a breakpoint after ir_getkey returns:
break *0x10000290
continue
Press a button on the remote, then check:
info registers r0
You'll see the NEC code (0x0C, 0x18, or 0x5E).
Step 14: Watch the Conditional Checks
Step through the NEC code comparisons:
stepi 10
info registers
Watch for cmp r0, #0x0c, cmp r0, #0x18, cmp r0, #0x5e instructions.
Step 15: Examine gpio_put Arguments
Before each gpio_put call, check the pin and state:
break *0x100002a0
continue
info registers r0 r1
r0 = GPIO pin number, r1 = state (0 or 1).
Step 16: Exit GDB
When done exploring:
quit
🔬 Part 11: Setting Up Ghidra for Structures
Step 17: Start Ghidra
Open a terminal and type:
ghidraRun
Step 18: Create a New Project
- Click File → New Project
- Select Non-Shared Project
- Click Next
- Enter Project Name:
0x0023_structures - Click Finish
Step 19: Import the Binary
- Navigate to the
0x0023_structures/build/folder - Drag and drop the
.binfile into Ghidra's project window
Step 20: Configure the Binary Format
Click the three dots (…) next to "Language" and:
- Search for "Cortex"
- Select ARM Cortex 32 little endian default
- Click OK
Click the "Options…" button and:
- Change Block Name to
.text - Change Base Address to
10000000 - Click OK
Step 21: Analyze the Binary
- Double-click on the file in the project window
- A dialog asks "Analyze now?" - Click Yes
- Use default analysis options and click Analyze
Wait for analysis to complete.
🔬 Part 12: Resolving Functions - Structures Project
Step 22: Navigate to Main
- Press
G(Go to address) and type10000234 - Right-click → Edit Function Signature
- Change to:
int main(void) - Click OK
Step 23: Resolve stdio_init_all
At address 0x10000236:
- Double-click on the called function
- Right-click → Edit Function Signature
- Change to:
bool stdio_init_all(void) - Click OK
Step 24: Identify gpio_init from Struct Pattern
Look for three consecutive calls with values 16, 17, 18:
movs r0, #0x10 ; 16 = GPIO16 (led1_pin)
bl FUN_xxxxx ; gpio_init
movs r0, #0x11 ; 17 = GPIO17 (led2_pin)
bl FUN_xxxxx ; gpio_init
movs r0, #0x12 ; 18 = GPIO18 (led3_pin)
bl FUN_xxxxx ; gpio_init
This pattern reveals the struct members! Update the function signature:
- Right-click → Edit Function Signature
- Change to:
void gpio_init(uint gpio) - Click OK
Step 25: Resolve ir_init
Look for a function call with GPIO 5:
movs r0, #0x5 ; GPIO 5 for IR receiver
bl FUN_xxxxx ; ir_init
- Right-click → Edit Function Signature
- Change to:
void ir_init(uint pin) - Click OK
Step 26: Resolve printf
Right after ir_init, look for the "IR receiver on GPIO" string being loaded:
- Right-click → Edit Function Signature
- Change to:
int printf(char *format, ...) - Check the Varargs checkbox
- Click OK
Step 27: Resolve ir_getkey
Look for a function that returns a value checked against conditions:
bl FUN_xxxxx ; Call ir_getkey
cmp r0, #0 ; Check if >= 0
blt no_key ; If negative, no key pressed
- Right-click → Edit Function Signature
- Change to:
int ir_getkey(void) - Click OK
Step 28: Resolve sleep_ms
Look for calls with 10 (0x0A) or 1 (0x01):
movs r0, #0x0A ; 10 milliseconds
bl FUN_xxxxx ; sleep_ms
- Right-click → Edit Function Signature
- Change to:
void sleep_ms(uint ms) - Click OK
🔬 Part 13: Recognizing Struct Patterns in Assembly
Step 29: Identify GPIO Set Direction
After each gpio_init, look for direction setting:
mov.w r4, #0x1 ; direction = output (1 = GPIO_OUT)
mcrr p0, 0x4, r3, r4 ; Configure GPIO direction register
This is the compiler's version of gpio_set_dir(pin, GPIO_OUT).
Step 30: Map the Struct Members
Create a mental (or written) map:
┌─────────────────────────────────────────────────────────────────┐
│ Struct Member Mapping │
│ │
│ Assembly Value → Struct Member → Physical LED │
│ ───────────────────────────────────────────────────────────── │
│ 0x10 (16) → led1_pin → Red LED │
│ 0x11 (17) → led2_pin → Green LED │
│ 0x12 (18) → led3_pin → Yellow LED │
│ │
│ NEC Code → State Member → Action │
│ ───────────────────────────────────────────────────────────── │
│ 0x0C → led1_state=true → Red LED ON │
│ 0x18 → led2_state=true → Green LED ON │
│ 0x5E → led3_state=true → Yellow LED ON │
│ │
└─────────────────────────────────────────────────────────────────┘
🔬 Part 14: Hacking Structures
Step 31: Open the Bytes Editor
- Click Window → Bytes
- Click the pencil icon to enable editing
Step 32: Swap LED Pin Assignments
We'll swap the red and green LED pins to reverse their behavior!
Find the gpio_init calls:
- Locate where
0x10(16) is loaded for led1_pin - Change
0x10to0x11(swap red to green's pin) - Locate where
0x11(17) is loaded for led2_pin - Change
0x11to0x10(swap green to red's pin)
Before:
LED 1 (0x0C) → GPIO 16 → Red LED
LED 2 (0x18) → GPIO 17 → Green LED
After:
LED 1 (0x0C) → GPIO 17 → Green LED (SWAPPED!)
LED 2 (0x18) → GPIO 16 → Red LED (SWAPPED!)
Step 33: Export and Flash
- Click File → Export Program
- Set Format to Binary
- Name:
0x0023_structures-h.bin - Click OK
Convert and flash:
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0023_structures
python ..\uf2conv.py build\0x0023_structures-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
Step 34: Verify the Hack
Open PuTTY and test:
- Press "1" on remote → GREEN LED lights (was red!)
- Terminal still shows
NEC command: 0x0C - Press "2" on remote → RED LED lights (was green!)
- Terminal still shows
NEC command: 0x18
The log says one thing, but the hardware does another!
🔬 Part 15: Security Implications - Log Desynchronization
The Danger of Mismatched Logs
┌─────────────────────────────────────────────────────────────────┐
│ Log vs Reality Desynchronization │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Terminal Log │ │ Physical LEDs │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ NEC: 0x0C │ ◄─────── │ GREEN LED on │ ◄── Mismatch! │
│ │ (expects RED) │ │ (not red!) │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ NEC: 0x18 │ ◄─────── │ RED LED on │ ◄── Mismatch! │
│ │ (expects GREEN) │ │ (not green!) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ The OPERATOR sees correct logs but WRONG physical behavior! │
│ │
└─────────────────────────────────────────────────────────────────┘
Real-World Example: Stuxnet
Stuxnet was a cyberweapon that:
- Attacked Iranian nuclear centrifuges
- Made centrifuges spin at dangerous speeds
- Fed FALSE "everything normal" data to operators
- Operators saw stable readings while equipment was destroyed
Our LED example demonstrates the same principle:
- Logs show expected behavior
- Hardware performs different actions
- Attackers can hide malicious activity
🔬 Part 16: Functions Project - Advanced Code
Step 35: Review the Functions Code
File: 0x0026_functions.c (key functions shown)
// Map IR command to LED number
int ir_to_led_number(int ir_command) {
if (ir_command == 0x0C) return 1;
if (ir_command == 0x18) return 2;
if (ir_command == 0x5E) return 3;
return 0;
}
// Get GPIO pin for LED number
uint8_t get_led_pin(simple_led_ctrl_t *leds, int led_num) {
if (led_num == 1) return leds->led1_pin;
if (led_num == 2) return leds->led2_pin;
if (led_num == 3) return leds->led3_pin;
return 0;
}
// Turn off all LEDs
void leds_all_off(simple_led_ctrl_t *leds) {
gpio_put(leds->led1_pin, false);
gpio_put(leds->led2_pin, false);
gpio_put(leds->led3_pin, false);
}
// Blink an LED
void blink_led(uint8_t pin, uint8_t count, uint32_t delay_ms) {
for (uint8_t i = 0; i < count; i++) {
gpio_put(pin, true);
sleep_ms(delay_ms);
gpio_put(pin, false);
sleep_ms(delay_ms);
}
}
// Main command processor
int process_ir_led_command(int ir_command, simple_led_ctrl_t *leds, uint8_t blink_count) {
if (!leds || ir_command < 0) return -1;
leds_all_off(leds);
int led_num = ir_to_led_number(ir_command);
if (led_num == 0) return 0;
uint8_t pin = get_led_pin(leds, led_num);
blink_led(pin, blink_count, 50);
gpio_put(pin, true);
return led_num;
}
Step 36: Understand the Function Call Chain
┌─────────────────────────────────────────────────────────────────┐
│ Function Call Chain │
│ │
│ main() │
│ │ │
│ └──► process_ir_led_command(key, &leds, 3) │
│ │ │
│ ├──► leds_all_off(&leds) │
│ │ └──► gpio_put() × 3 │
│ │ │
│ ├──► ir_to_led_number(ir_command) │
│ │ └──► returns 1, 2, or 3 │
│ │ │
│ ├──► get_led_pin(&leds, led_num) │
│ │ └──► returns GPIO pin number │
│ │ │
│ ├──► blink_led(pin, 3, 50) │
│ │ └──► gpio_put() + sleep_ms() in loop │
│ │ │
│ └──► gpio_put(pin, true) │
│ │
└─────────────────────────────────────────────────────────────────┘
Step 37: Flash and Test
- Flash
0x0026_functions.uf2to your Pico 2 - Open PuTTY
- Press remote buttons:
- "1" → Red LED blinks 3 times, then stays on
- "2" → Green LED blinks 3 times, then stays on
- "3" → Yellow LED blinks 3 times, then stays on
🔬 Part 17: Debugging with GDB (Functions)
Step 38: Start OpenOCD (Terminal 1)
Open a terminal and start OpenOCD:
openocd ^
-s "C:\Users\flare-vm\.pico-sdk\openocd\0.12.0+dev\scripts" ^
-f interface/cmsis-dap.cfg ^
-f target/rp2350.cfg ^
-c "adapter speed 5000"
You should see output indicating OpenOCD connected successfully to your Pico 2 via the Debug Probe.
Step 39: Start GDB (Terminal 2)
Open a new terminal and launch GDB with the binary:
arm-none-eabi-gdb build\0x0026_functions.elf
Step 40: Connect to the Remote Target
In GDB, connect to OpenOCD:
target remote :3333
Step 41: Halt the Running Binary
Stop the processor:
monitor halt
Step 42: Examine the Function Layout
Disassemble to see the multiple functions:
disassemble 0x10000234,+300
You'll see multiple function prologues (push) and epilogues (pop) for the helper functions.
Step 43: Set Breakpoints on Key Functions
Set breakpoints on the helper functions:
break *0x10000234
break *0x10000280
break *0x100002a0
Reset and run:
monitor reset halt
continue
Step 44: Trace the Function Call Chain
When you press a remote button, step through the calls:
stepi 50
info registers
Watch the call chain: process_ir_led_command → leds_all_off → ir_to_led_number → get_led_pin → blink_led.
Step 45: Examine ir_to_led_number
When the comparison function runs, check the return value:
info registers r0
For button "1", you should see r0 = 1. For button "2", r0 = 2.
Step 46: Watch the blink_led Loop
Set a breakpoint inside blink_led and watch it execute 3 times:
break *0x100002c0
continue
info registers r0 r1
r0 = pin number, r1 = state (alternates 0 and 1).
Step 47: Examine Pointer Dereference
Watch how the struct pointer is used to get LED pins:
x/6xb $r0
This shows the struct contents when leds pointer is in r0.
Step 48: Watch Return Values
After function calls, check return values in r0:
stepi 20
info registers r0
Step 49: Exit GDB
When done exploring:
quit
🔬 Part 18: Analyzing .ELF Files in Ghidra
Step 50: Create New Ghidra Project
- Create project:
0x0026_functions - Import the
.elffile (NOT the .bin this time!)
Why Use .ELF Instead of .BIN?
| Feature | .BIN File | .ELF File |
|---|---|---|
| Symbols | None | Function/variable names |
| Sections | Raw bytes only | .text, .data, .rodata, etc. |
| Debug info | None | May include debug symbols |
| Size | Smaller | Larger |
| Use case | Flashing to hardware | Analysis and debugging |
Step 51: Import and Analyze the .ELF
- Drag and drop the
.elffile into Ghidra - Ghidra automatically detects ARM format!
- Click Yes to analyze
- Wait for analysis to complete
Step 52: Explore the Symbol Tree
With .ELF files, you get more information:
- Look at the Symbol Tree panel
- Expand Functions - you may see named functions!
- Expand Labels - data labels may appear
🔬 Part 19: Hacking the Functions Project
Step 53: Find LED Pin Values
Look for the struct initialization pattern:
movs r0, #0x10 ; led1_pin = 16
movs r0, #0x11 ; led2_pin = 17
movs r0, #0x12 ; led3_pin = 18
Step 54: Swap LED 1 and LED 3
We'll swap the red (GPIO 16) and yellow (GPIO 18) LEDs:
Find and patch in the .bin file:
- Change
0x10(16) to0x12(18) - Change
0x12(18) to0x10(16)
Before:
Button 1 → LED 1 → GPIO 16 → Red
Button 3 → LED 3 → GPIO 18 → Yellow
After:
Button 1 → LED 1 → GPIO 18 → Yellow (SWAPPED!)
Button 3 → LED 3 → GPIO 16 → Red (SWAPPED!)
Step 55: Export the Patched .BIN
Important: Even though we analyzed the .elf, we patch the .bin!
- Open the original
.binfile in Ghidra (or a hex editor) - Apply the patches
- Export as
0x0026_functions-h.bin
Step 56: Convert and Flash
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0026_functions
python ..\uf2conv.py build\0x0026_functions-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
Step 57: Verify the Hack
Open PuTTY and test:
- Press "1" → YELLOW LED blinks (was red!)
- Terminal shows:
LED 1 activated on GPIO 16(WRONG - it's actually GPIO 18!) - Press "3" → RED LED blinks (was yellow!)
- Terminal shows:
LED 3 activated on GPIO 18(WRONG - it's actually GPIO 16!)
Again, logs don't match reality!
📊 Part 20: Summary and Review
What We Accomplished
- Learned C structures - Grouping related data together
- Understood struct memory layout - How members are stored consecutively
- Mastered dot and arrow operators - Accessing struct members
- Learned the NEC IR protocol - How remotes communicate
- Understood functions with parameters - Passing data in and out
- Saw struct flattening in assembly - How compilers transform structs
- Analyzed .ELF files - Getting more symbol information
- Hacked GPIO assignments - Swapping LED behavior
- Discovered log desynchronization - Security implications
Struct Operations Summary
┌─────────────────────────────────────────────────────────────────┐
│ Struct Operations │
│ │
│ Definition: │
│ typedef struct { │
│ uint8_t pin; │
│ bool state; │
│ } led_t; │
│ │
│ Creation: │
│ led_t led = { .pin = 16, .state = false }; │
│ │
│ Access (variable): led.pin │
│ Access (pointer): ptr->pin or (*ptr).pin │
│ │
│ Passing to function: void func(led_t *led) │
│ Calling: func(&led) │
│ │
└─────────────────────────────────────────────────────────────────┘
Function Types Summary
┌─────────────────────────────────────────────────────────────────┐
│ Function Patterns │
│ │
│ No params, no return: │
│ void leds_all_off(void) │
│ │
│ With params, no return: │
│ void blink_led(uint8_t pin, uint8_t count, uint32_t delay) │
│ │
│ No params, with return: │
│ int ir_getkey(void) │
│ │
│ With params, with return: │
│ int ir_to_led_number(int ir_command) │
│ │
│ With struct pointer: │
│ uint8_t get_led_pin(simple_led_ctrl_t *leds, int led_num) │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Memory Addresses
| Memory Address | Description |
|---|---|
0x10000234 |
main() function |
0x10 (16) |
GPIO 16 - Red LED (led1_pin) |
0x11 (17) |
GPIO 17 - Green LED (led2_pin) |
0x12 (18) |
GPIO 18 - Yellow LED (led3_pin) |
0x05 |
GPIO 5 - IR receiver |
0x0C |
NEC code for button 1 |
0x18 |
NEC code for button 2 |
0x5E |
NEC code for button 3 |
✅ Practice Exercises
Exercise 1: Add a Fourth LED
Modify the struct to include a fourth LED on GPIO 19.
Hint: Add led4_pin and led4_state members.
Exercise 2: Change Blink Count
Find and modify the blink count from 3 to 5 blinks.
Hint: Look for the value passed to process_ir_led_command.
Exercise 3: Swap All Three LEDs
Create a rotation where 1→Green, 2→Yellow, 3→Red.
Hint: Patch all three GPIO values.
Exercise 4: Change Blink Speed
Make the LEDs blink faster by changing the delay from 50ms to 25ms.
Hint: Find 0x32 (50) in the function parameters.
Exercise 5: Disable One LED
Make button 2 do nothing (LED stays off).
Hint: NOP out the gpio_put call or change the NEC code comparison.
🎓 Key Takeaways
-
Structs group related data - Better organization than separate variables
-
Dot operator for variables, arrow for pointers -
.vs-> -
Designated initializers are cleaner -
.member = valuesyntax -
Compilers flatten structs - You see values, not struct names, in assembly
-
NEC protocol uses 8-bit commands - 0x0C, 0x18, 0x5E for our buttons
-
Functions separate concerns - Each function does one job
-
.ELF files contain more info than .BIN - Symbols, sections, debug data
-
Log desynchronization is dangerous - Logs can lie about real behavior
-
Pattern recognition is key - Consecutive values like 16, 17, 18 reveal structs
-
Always patch the .bin for flashing - .elf is for analysis only
📖 Glossary
| Term | Definition |
|---|---|
| Arrow Operator (->) | Accesses struct member through a pointer |
| Designated Initializer | Syntax .member = value for struct initialization |
| Dot Operator (.) | Accesses struct member from a struct variable |
| .ELF File | Executable and Linkable Format - contains symbols |
| Flattening | Compiler converting structs to individual values |
| IR (Infrared) | Invisible light used for remote control |
| Log Desynchronization | When logs don't match actual system behavior |
| Member | A variable inside a struct |
| NEC Protocol | Common IR communication standard |
| Struct | User-defined type grouping related variables |
| typedef | Creates an alias for a type |
🔗 Additional Resources
NEC IR Command Reference
| Button | Command | Binary |
|---|---|---|
| 1 | 0x0C | 0000 1100 |
| 2 | 0x18 | 0001 1000 |
| 3 | 0x5E | 0101 1110 |
GPIO Pin Quick Reference
| GPIO | Default Function | Our Usage |
|---|---|---|
| 5 | General I/O | IR Receiver |
| 16 | General I/O | Red LED |
| 17 | General I/O | Green LED |
| 18 | General I/O | Yellow LED |
Struct Size Calculation
| Type | Size (bytes) |
|---|---|
uint8_t |
1 |
bool |
1 |
uint16_t |
2 |
uint32_t |
4 |
int |
4 |
float |
4 |
pointer |
4 (on ARM32) |
🚨 Real-World Implications
What You've Learned in This Course
Over these weeks, you've built skills that few people possess:
- Hardware fundamentals - GPIO, I2C, PWM, IR protocols
- Reverse engineering - Ghidra, disassembly, function identification
- Binary patching - Modifying compiled code
- Security awareness - Understanding vulnerabilities
The Power and Responsibility
The techniques you've learned can be used for:
Good:
- Security research
- Debugging proprietary systems
- Understanding how things work
- Career in cybersecurity
Danger:
- Unauthorized system access
- Sabotage of critical infrastructure
- Fraud and deception
Always use your skills ethically and legally!
Keep Learning
This is just the beginning:
- Explore more complex protocols (SPI, CAN bus)
- Learn dynamic analysis with debuggers
- Study cryptographic implementations
- Practice on CTF challenges
Congratulations on completing this course! You now have the curiosity, persistence, and skills that embedded systems engineers and security researchers thrive on. Keep experimenting, documenting, and sharing your work. The world needs more builders and defenders like you!
Happy hacking! 🔧