51 KiB
Week 10: Conditionals in Embedded Systems: Debugging and Hacking Static & Dynamic Conditionals w/ SG90 Servo Motor PWM Basics
🎯 What You'll Learn This Week
By the end of this tutorial, you will be able to:
- Understand the difference between static and dynamic conditionals in C
- Know how if/else statements and switch/case blocks work at the assembly level
- Understand Pulse Width Modulation (PWM) and how it controls servo motors
- Calculate PWM timing from system clock to servo pulse width
- Identify conditional branches in Ghidra (beq, bne instructions)
- Hack string literals and timing delays in binary files
- Modify branch targets to change program flow
- Create "stealth" functionality by NOP-ing out print statements
- Understand IEEE-754 floating-point for angle calculations
📚 Part 1: Understanding Conditionals in C
What Are Conditionals?
Conditionals are programming structures that make decisions. They let your program choose different paths based on whether a condition is true or false. Think of them like a fork in the road - the program checks a condition and decides which way to go.
Two Types of Conditionals
| Type | Description | Example |
|---|---|---|
| Static | Condition value is known/fixed at compile time | if (choice == 1) where choice never changes |
| Dynamic | Condition value changes based on runtime input | if (choice == getchar()) where user types input |
📚 Part 2: Static Conditionals
What Makes a Conditional "Static"?
A static conditional is one where the outcome is predetermined because the condition variable never changes during program execution:
int choice = 1; // This NEVER changes!
while (true) {
if (choice == 1) {
printf("1\r\n"); // This ALWAYS runs
} else if (choice == 2) {
printf("2\r\n"); // This NEVER runs
} else {
printf("?\r\n"); // This NEVER runs
}
}
┌─────────────────────────────────────────────────────────────────┐
│ Static Conditional Flow │
│ │
│ choice = 1 (set once, never changes) │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ choice == 1 │────YES────► printf("1") │
│ └─────────────┘ │
│ │NO (never taken) │
│ ▼ │
│ ┌─────────────┐ │
│ │ choice == 2 │────YES────► printf("2") (never reached) │
│ └─────────────┘ │
│ │NO │
│ ▼ │
│ printf("?") (never reached) │
│ │
│ The branching logic EXISTS but only ONE path ever executes! │
│ │
└─────────────────────────────────────────────────────────────────┘
The if/else Statement
The if/else structure checks conditions in order:
if (choice == 1) {
// Do something if choice is 1
} else if (choice == 2) {
// Do something if choice is 2
} else {
// Do something for all other values
}
The switch Statement
The switch statement is another way to handle multiple conditions:
switch (choice) {
case 1:
printf("one\r\n");
break;
case 2:
printf("two\r\n");
break;
default:
printf("??\r\n");
}
Key Differences:
| Feature | if/else | switch |
|---|---|---|
| Condition | Any boolean expression | Single variable comparison |
| Values | Ranges, complex logic | Discrete values only |
| Fall-through | No | Yes (without break) |
| Readability | Good for 2-3 conditions | Better for many conditions |
📚 Part 3: Dynamic Conditionals
What Makes a Conditional "Dynamic"?
A dynamic conditional is one where the condition variable changes based on runtime input:
uint8_t choice = 0;
while (true) {
choice = getchar(); // User types a key - VALUE CHANGES!
if (choice == '1') {
printf("1\r\n");
} else if (choice == '2') {
printf("2\r\n");
} else {
printf("??\r\n");
}
}
┌─────────────────────────────────────────────────────────────────┐
│ Dynamic Conditional Flow │
│ │
│ ┌────────────────┐ │
│ │ choice=getchar │◄──── User types 'x' on keyboard │
│ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ choice=='1' │────YES────► printf("1"), move servo │
│ └─────────────┘ │
│ │NO │
│ ▼ │
│ ┌─────────────┐ │
│ │ choice=='2' │────YES────► printf("2"), move servo │
│ └─────────────┘ │
│ │NO │
│ ▼ │
│ printf("??") │
│ │ │
│ └──────────────────► Loop back to getchar() │
│ │
│ EACH iteration can take a DIFFERENT path! │
│ │
└─────────────────────────────────────────────────────────────────┘
The getchar() Function
getchar() reads a single character from the serial terminal:
uint8_t choice = getchar(); // Waits for user to type something
- Returns the ASCII value of the key pressed
'1'= 0x31,'2'= 0x32,'x'= 0x78,'y'= 0x79- Blocks (waits) until a key is pressed
📚 Part 4: Understanding PWM (Pulse Width Modulation)
What is PWM?
PWM (Pulse Width Modulation) is a technique for controlling power by rapidly switching a signal on and off. The ratio of "on time" to "off time" determines the average power delivered.
┌─────────────────────────────────────────────────────────────────┐
│ PWM Signal - 50% Duty Cycle │
│ │
│ HIGH ─┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ │ │ │ │ │ │
│ LOW └─────┘ └─────┘ └─────┘ └───── │
│ ◄──T──► │
│ ON OFF │
│ │
│ Duty Cycle = ON time / Total period = 50% │
│ │
└─────────────────────────────────────────────────────────────────┘
PWM for Servo Control
Servo motors use PWM differently - they care about the pulse width, not the duty cycle percentage:
┌─────────────────────────────────────────────────────────────────┐
│ Servo PWM Signal (50 Hz = 20ms period) │
│ │
│ 0° Position (1ms pulse): │
│ HIGH ─┐ │
│ │ 1ms │
│ LOW └────────────────────────────── (19ms) ────────── │
│ ◄────────────── 20ms ─────────────────────────► │
│ │
│ 90° Position (1.5ms pulse): │
│ HIGH ─────┐ │
│ │ 1.5ms │
│ LOW └─────────────────────────── (18.5ms) ─────── │
│ │
│ 180° Position (2ms pulse): │
│ HIGH ─────────┐ │
│ │ 2ms │
│ LOW └───────────────────────── (18ms) ─────── │
│ │
└─────────────────────────────────────────────────────────────────┘
The Magic Numbers
| Angle | Pulse Width | PWM Ticks (at 1MHz) |
|---|---|---|
| 0° | 1000 µs | 1000 |
| 90° | 1500 µs | 1500 |
| 180° | 2000 µs | 2000 |
📚 Part 5: PWM Timing Calculations
From 150 MHz to 50 Hz
The RP2350's system clock runs at 150 MHz (150 million cycles per second). A servo needs a 50 Hz signal (one pulse every 20 ms). How do we bridge this gap?
┌─────────────────────────────────────────────────────────────────┐
│ Clock Division Chain │
│ │
│ System Clock: 150 MHz │
│ │ │
│ │ ÷ 150 (clock divider) │
│ ▼ │
│ PWM Tick Rate: 1 MHz (1 tick = 1 microsecond) │
│ │ │
│ │ Count to 20,000 (wrap value = 19,999) │
│ ▼ │
│ PWM Frequency: 50 Hz (20 ms period) │
│ │
└─────────────────────────────────────────────────────────────────┘
The Math
Step 1: Clock Division
PWM Tick Rate = System Clock ÷ Divider
1,000,000 Hz = 150,000,000 Hz ÷ 150
Step 2: Frame Period
Period = (Wrap Value + 1) × Tick Duration
20 ms = 20,000 ticks × 1 µs/tick
Step 3: Pulse Width to Ticks
Ticks = Pulse Width (µs) × 1 tick/µs
1500 ticks = 1500 µs × 1
Worked Example: 90° Angle
Let's calculate what happens when we command 90°:
-
Angle to Pulse Width:
Pulse = MIN + (angle/180) × (MAX - MIN) Pulse = 1000 + (90/180) × (2000 - 1000) Pulse = 1000 + 0.5 × 1000 Pulse = 1500 µs -
Pulse to PWM Ticks:
Level = 1500 µs × 1 tick/µs = 1500 ticks -
Hardware Timing:
- Signal HIGH for 1500 ticks (1.5 ms)
- Signal LOW for 18,500 ticks (18.5 ms)
- Total period: 20,000 ticks (20 ms)
📚 Part 6: Understanding the SG90 Servo Motor
What is the SG90?
The SG90 is a small, inexpensive hobby servo motor commonly used in robotics projects:
┌─────────────────────────────────────────────────────────────────┐
│ SG90 Servo Motor │
│ │
│ ┌───────────────┐ │
│ │ ┌─────┐ │ │
│ │ │ ARM │ │ ◄── Rotates 0° to 180° │
│ │ └──┬──┘ │ │
│ │ │ │ │
│ │ ┌────┴────┐ │ │
│ │ │ MOTOR │ │ │
│ │ │ GEAR │ │ │
│ │ │ BOX │ │ │
│ │ └─────────┘ │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────┼───────┐ │
│ │ │ │ │
│ ORANGE RED BROWN │
│ Signal VCC GND │
│ (PWM) (5V) │
│ │
└─────────────────────────────────────────────────────────────────┘
SG90 Specifications
| Parameter | Value |
|---|---|
| Voltage | 4.8V - 6V (typically 5V) |
| Rotation | 0° to 180° |
| Pulse Width | 1000µs - 2000µs |
| Frequency | 50 Hz (20ms period) |
| Stall Current | ~650mA (can spike to 1A+) |
Wire Colors
| Wire Color | Function | Connect To |
|---|---|---|
| Brown | GND | Ground |
| Red | VCC | 5V Power (VBUS) |
| Orange | Signal | GPIO Pin (PWM) |
📚 Part 7: Power Supply Safety
⚠️ CRITICAL WARNING ⚠️
NEVER power the servo directly from the Pico's 3.3V pin!
Servos can draw over 1000mA during movement spikes. The Pico's 3.3V regulator cannot handle this and you will:
- Cause brownouts (Pico resets)
- Damage the Pico's voltage regulator
- Potentially damage your USB port
Correct Power Setup
┌─────────────────────────────────────────────────────────────────┐
│ CORRECT Power Wiring │
│ │
│ USB ────► VBUS (5V) ───┬──► Servo VCC (Red) │
│ │ │
│ └──► Capacitor (+) │
│ │ │
│ Pico GND ──────────────┬────┴──► Capacitor (-) │
│ │ │
│ └──────► Servo GND (Brown) │
│ │
│ Pico GPIO 6 ──────────────────► Servo Signal (Orange) │
│ │
│ IMPORTANT: Use a 1000µF 25V capacitor across the servo │
│ power to absorb current spikes! │
│ │
└─────────────────────────────────────────────────────────────────┘
Why the Capacitor?
The 1000µF capacitor acts as a tiny battery:
- Absorbs sudden current demands when servo moves
- Prevents voltage drops that could reset the Pico
- Smooths out electrical noise
📚 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 SG90 servo motor
- A 1000µF 25V capacitor
- The sample projects:
0x001d_static-conditionalsand0x0020_dynamic-conditionals
Hardware Setup
Connect your servo like this:
| Servo Wire | Pico 2 Pin |
|---|---|
| Brown (GND) | GND |
| Red (VCC) | VBUS (5V) |
| Orange (Signal) | GPIO 6 |
┌─────────────────────────────────────────────────────────────────┐
│ Servo Wiring with Capacitor │
│ │
│ Pico 2 SG90 Servo │
│ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │
│ │ GPIO 6 │─────── Orange ───────►│ Signal │ │
│ │ │ │ │ │
│ │ VBUS(5V) │───┬─── Red ──────────►│ VCC │ │
│ │ │ │ │ │ │
│ │ GND │───┼─── Brown ────────►│ GND │ │
│ │ │ │ └──────────┘ │
│ └──────────┘ │ │
│ │ ┌─────────┐ │
│ └────┤ + CAP - ├──── GND │
│ │ 1000µF │ │
│ │ 25V │ │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Project Structure
Embedded-Hacking/
├── 0x001d_static-conditionals/
│ ├── build/
│ │ ├── 0x001d_static-conditionals.uf2
│ │ └── 0x001d_static-conditionals.bin
│ ├── main/
│ │ └── 0x001d_static-conditionals.c
│ └── servo.h
├── 0x0020_dynamic-conditionals/
│ ├── build/
│ │ ├── 0x0020_dynamic-conditionals.uf2
│ │ └── 0x0020_dynamic-conditionals.bin
│ ├── main/
│ │ └── 0x0020_dynamic-conditionals.c
│ └── servo.h
└── uf2conv.py
🔬 Part 9: Hands-On Tutorial - Static Conditionals Code
Step 1: Review the Source Code
Let's examine the static conditionals code:
File: 0x001d_static-conditionals.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "servo.h"
#define SERVO_GPIO 6
int main(void) {
stdio_init_all();
int choice = 1; // STATIC - never changes!
servo_init(SERVO_GPIO);
while (true) {
// if/else conditional
if (choice == 1) {
printf("1\r\n");
} else if (choice == 2) {
printf("2\r\n");
} else {
printf("?\r\n");
}
// switch/case conditional
switch (choice) {
case 1:
printf("one\r\n");
break;
case 2:
printf("two\r\n");
break;
default:
printf("??\r\n");
}
// Servo movement
servo_set_angle(0.0f);
sleep_ms(500);
servo_set_angle(180.0f);
sleep_ms(500);
}
}
Step 2: Understand the Program Flow
Since choice = 1 and NEVER changes:
┌─────────────────────────────────────────────────────────────────┐
│ Static Conditional Execution │
│ │
│ Every loop iteration: │
│ │
│ 1. Check if (choice == 1) → TRUE → print "1" │
│ 2. Check switch case 1 → MATCH → print "one" │
│ 3. Move servo to 0° │
│ 4. Wait 500ms │
│ 5. Move servo to 180° │
│ 6. Wait 500ms │
│ 7. Repeat forever... │
│ │
│ Output always: "1" then "one" (forever) │
│ Servo: sweeps 0° → 180° → 0° → 180° (forever) │
│ │
└─────────────────────────────────────────────────────────────────┘
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
0x001d_static-conditionals.uf2onto the drive - The Pico will reboot and start running!
Step 4: Verify It's Working
Check the serial monitor (PuTTY at 115200 baud):
1
one
1
one
1
one
...
Watch the servo:
- It should sweep from 0° to 180° every second
- The movement is continuous and repetitive
🔬 Part 10: Debugging with GDB (Static Conditionals)
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\0x001d_static-conditionals.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 the conditionals:
disassemble 0x10000234,+200
Look for comparison and branch instructions that implement the if/else and switch logic.
Step 10: Set a Breakpoint at Main
break *0x10000234
Reset and run to hit the breakpoint:
monitor reset halt
continue
Step 11: Find the Comparison Instructions
Step through and examine the comparison:
stepi 20
info registers
Look for cmp instructions that compare the choice variable (value 1).
Step 12: Examine the Printf Arguments
Before printf calls, check r0 for the format string address:
x/s $r0
You should see strings like "1\r\n" or "one\r\n".
Step 13: Watch the Servo Commands
Set a breakpoint on servo_set_angle:
break *0x10000280
continue
Check the floating-point register for the angle:
info registers s0
Step 14: Examine the Timing Delay
Set a breakpoint on sleep_ms and check the delay value:
break *0x10000290
continue
info registers r0
You should see 0x1f4 (500 decimal) for the 500ms delay.
Step 15: Step Through the Loop
Watch one complete iteration:
stepi 100
info registers
Notice how the static conditional always takes the same branch path.
Step 16: Exit GDB
When done exploring:
quit
🔬 Part 11: Setting Up Ghidra for Static Conditionals
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:
0x001d_static-conditionals - Click Finish
Step 19: Import the Binary
- Open your file explorer
- Navigate to the
0x001d_static-conditionals/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 in Ghidra (Static)
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: Resolve servo_init
Look for a function call where r0 is loaded with 0x6 (GPIO pin 6):
movs r0, #0x6 ; GPIO pin 6
bl FUN_xxxxx ; servo_init
- Right-click → Edit Function Signature
- Change to:
void servo_init(uint pin) - Click OK
Step 25: Resolve puts
Look for function calls that load string addresses into r0:
ldr r0, =0x10001c54 ; Address of "1" string
bl FUN_xxxxx ; puts
How do we know it's puts?
- It takes a single string argument
- The hex
0x31is ASCII "1" - The hex
0x0dis carriage return "\r" - We see "1" echoed in PuTTY
- Right-click → Edit Function Signature
- Change to:
int puts(char *s) - Click OK
Step 26: Resolve servo_set_angle
Look for a function that loads float constants. Inside the function, you'll find:
0x7D0(2000 decimal) - maximum pulse width0x3E8(1000 decimal) - minimum pulse width
These are the servo pulse limits!
- Right-click → Edit Function Signature
- Change to:
void servo_set_angle(float degrees) - Click OK
Step 27: Resolve sleep_ms
Look for a function where r0 is loaded with 0x1f4 (500 decimal):
ldr r0, =0x1f4 ; 500 milliseconds
bl FUN_xxxxx ; sleep_ms
- Right-click → Edit Function Signature
- Change to:
void sleep_ms(uint ms) - Click OK
🔬 Part 13: Hacking Static Conditionals
Step 28: Open the Bytes Editor
- Click Window → Bytes
- A new panel appears showing raw hex bytes
- Click the pencil icon to enable editing
Step 29: Hack #1 - Change "1" to "2"
Find the string "1" in memory:
- Double-click on the address reference for "1" (around
0x10001c54) - You'll see
31(ASCII for "1") - Change
31to32(ASCII for "2")
Step 30: Hack #2 - Change "one" to "fun"
Find the string "one":
- Navigate to the "one" string address
- Find bytes
6f 6e 65("one" in ASCII) - Change to
66 75 6e("fun" in ASCII)
ASCII Reference:
| Character | Hex |
|---|---|
| o | 0x6f |
| n | 0x6e |
| e | 0x65 |
| f | 0x66 |
| u | 0x75 |
| n | 0x6e |
Step 31: Hack #3 - Speed Up the Servo
Find the sleep_ms delay value:
- Look for
0x1f4(500) in the code - This appears TWICE (once for each sleep_ms call)
- Change both from
f4 01to64 00(100 in little-endian)
Before: 500ms delay (servo moves slowly) After: 100ms delay (servo moves FAST!)
Step 32: Export and Flash
- Click File → Export Program
- Set Format to Binary
- Name:
0x001d_static-conditionals-h.bin - Click OK
Convert and flash:
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x001d_static-conditionals
python ..\uf2conv.py build\0x001d_static-conditionals-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
Step 33: Verify the Hacks
Serial output now shows:
2
fun
2
fun
...
The servo now moves 5x faster! It's spinning like crazy!
🔬 Part 14: Dynamic Conditionals - The Source Code
Step 34: Review the Dynamic Code
File: 0x0020_dynamic-conditionals.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "servo.h"
#define SERVO_GPIO 6
int main(void) {
stdio_init_all();
uint8_t choice = 0; // DYNAMIC - changes with user input!
servo_init(SERVO_GPIO);
while (true) {
choice = getchar(); // Wait for keyboard input
if (choice == 0x31) { // '1'
printf("1\r\n");
} else if (choice == 0x32) { // '2'
printf("2\r\n");
} else {
printf("??\r\n");
}
switch (choice) {
case '1':
printf("one\r\n");
servo_set_angle(0.0f);
sleep_ms(500);
servo_set_angle(180.0f);
sleep_ms(500);
break;
case '2':
printf("two\r\n");
servo_set_angle(180.0f);
sleep_ms(500);
servo_set_angle(0.0f);
sleep_ms(500);
break;
default:
printf("??\r\n");
}
}
}
Step 35: Understand the Dynamic Behavior
| User Types | Output | Servo Action |
|---|---|---|
| '1' (0x31) | "1" + "one" | 0° → 180° |
| '2' (0x32) | "2" + "two" | 180° → 0° |
| Anything else | "??" + "??" | No movement |
Step 36: Flash and Test
- Flash
0x0020_dynamic-conditionals.uf2 - Open PuTTY
- Press '1' - servo sweeps one direction
- Press '2' - servo sweeps the other direction
- Press 'x' - prints "??" and no movement
🔬 Part 15: Debugging with GDB (Dynamic Conditionals)
Step 37: 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 38: Start GDB (Terminal 2)
Open a new terminal and launch GDB with the binary:
arm-none-eabi-gdb build\0x0020_dynamic-conditionals.elf
Step 39: Connect to the Remote Target
In GDB, connect to OpenOCD:
target remote :3333
Step 40: Halt the Running Binary
Stop the processor:
monitor halt
Step 41: Examine Main Function
Disassemble around main to see the dynamic conditionals:
disassemble 0x10000234,+250
Look for the getchar call followed by comparison and branch instructions.
Step 42: Set a Breakpoint After getchar
Find where choice gets its value:
break *0x10000250
Reset and continue:
monitor reset halt
continue
Step 43: Watch the Input Value
When you press a key in PuTTY, the breakpoint hits. Check the return value:
info registers r0
If you pressed '1', you should see 0x31. If you pressed '2', you should see 0x32.
Step 44: Trace the Comparison Logic
Step through the comparison instructions:
stepi 10
info registers
Watch for cmp r0, #0x31 and cmp r0, #0x32 instructions.
Step 45: Examine the Branch Decisions
Look at the condition flags after comparison:
info registers cpsr
The zero flag (Z) determines if the branch is taken.
Step 46: Watch Different Input Paths
Continue and press different keys to see how the program takes different branches:
continue
Press '2' in PuTTY, then examine registers again.
Step 47: Examine Servo Control
Set a breakpoint on servo_set_angle:
break *0x10000280
continue
Check the angle value:
info registers s0
Step 48: Exit GDB
When done exploring:
quit
🔬 Part 16: Setting Up Ghidra for Dynamic Conditionals
Step 49: Create New Project
- Create project:
0x0020_dynamic-conditionals - Import the
.binfile - Configure as ARM Cortex, base address
10000000 - Analyze
Step 50: Navigate to Main
Press G and go to 10000234.
Step 51: Resolve Functions
Follow the same process:
- main at
0x10000234→int main(void) - stdio_init_all →
bool stdio_init_all(void) - servo_init →
void servo_init(uint pin) - puts →
int puts(char *s) - servo_set_angle →
void servo_set_angle(float degrees) - sleep_ms →
void sleep_ms(uint ms)
Step 52: Identify getchar
Look for a function that:
- Returns a value in
r0 - That value is then compared against
0x31("1")
bl FUN_xxxxx ; This is getchar!
mov r4, r0 ; Save return value
cmp r4, #0x31 ; Compare to '1'
beq LAB_xxxxx ; Branch if equal
- Right-click → Edit Function Signature
- Change to:
int getchar(void) - Click OK
Step 53: Identify Hardware Addresses
Double-click into stdio_init_all and look for hardware addresses:
ldr r0, =0x40070000 ; UART0 base address
Check the RP2350 datasheet Section 2.2 (Address Map):
0x40070000= UART0
This confirms it's a UART initialization function!
🔬 Part 17: Understanding Branch Instructions
ARM Branch Instructions
| Instruction | Meaning | Condition |
|---|---|---|
b |
Branch (always) | Unconditional jump |
beq |
Branch if Equal | Zero flag set |
bne |
Branch if Not Equal | Zero flag clear |
bgt |
Branch if Greater Than | Signed greater |
blt |
Branch if Less Than | Signed less |
How Conditionals Become Branches
if (choice == 0x31) {
printf("1");
}
Becomes:
cmp r4, #0x31 ; Compare choice to '1'
bne skip_printf ; If NOT equal, skip the printf
; ... printf code here ...
skip_printf:
┌─────────────────────────────────────────────────────────────────┐
│ Conditional Branch Flow │
│ │
│ cmp r4, #0x31 │
│ │ │
│ │ (Sets flags based on r4 - 0x31) │
│ ▼ │
│ beq target_address │
│ │ │
│ ├── If r4 == 0x31: Jump to target_address │
│ │ │
│ └── If r4 != 0x31: Continue to next instruction │
│ │
└─────────────────────────────────────────────────────────────────┘
🔬 Part 18: Advanced Hacking - Creating Stealth Commands
The Goal
We want to create secret commands that:
- Respond to 'x' and 'y' instead of '1' and '2'
- Move the servo WITHOUT printing anything
- Leave NO trace in the terminal
Step 54: Plan the Patches
Original behavior:
- '1' (0x31) → prints "1" and "one", moves servo
- '2' (0x32) → prints "2" and "two", moves servo
Hacked behavior:
- 'x' (0x78) → moves servo SILENTLY
- 'y' (0x79) → moves servo SILENTLY
- '1' (0x31) → prints "1" and "one" (normal)
- '2' (0x32) → prints "2" and "two" (normal)
Step 55: Change Comparison Values
Find the compare instructions:
cmp r4, #0x31 ; Change to #0x78 ('x')
...
cmp r4, #0x32 ; Change to #0x79 ('y')
Step 56: Redirect Branches to Skip Prints
For the stealth keys, we need to jump PAST the printf calls directly to the servo code.
Original flow:
compare → branch → printf("1") → printf("one") → servo code
Hacked flow:
compare 'x' → branch → [skip prints] → servo code
Change the beq target addresses:
- Original:
beq 0x10000270(goes to printf) - Hacked:
beq 0x1000027c(skips to servo)
Step 57: NOP Out Print Calls
NOP (No Operation) is an instruction that does nothing. We use it to "erase" code without changing the size of the binary.
ARM Thumb NOP encoding: 00 bf (2 bytes)
To NOP out a bl puts instruction (4 bytes), use 00 bf 00 bf.
Step 58: Apply All Patches
Here's the complete patch list:
| Offset | Original | Patched | Purpose |
|---|---|---|---|
| Compare1 | 31 |
78 |
Check for 'x' instead of '1' |
| Compare2 | 32 |
79 |
Check for 'y' instead of '2' |
| Branch1 | target=0x270 | target=0x27c | Skip printf for 'x' |
| Branch2 | target=0x29a | target=0x2a6 | Skip printf for 'y' |
| puts1 | bl puts |
nop nop |
Remove print |
| puts2 | bl puts |
nop nop |
Remove print |
Step 59: Hack the Angle Value
Let's also change 180° to 30° for fun!
Original: 0x43340000 (180.0f in IEEE-754)
New: 0x41f00000 (30.0f in IEEE-754)
Calculation for 30.0f:
30.0 = 1.875 × 2^4
Sign = 0
Exponent = 127 + 4 = 131 = 0x83
Mantissa = 0.875 = 0x700000
Binary: 0 10000011 11100000000000000000000
Hex: 0x41f00000
Little-endian: 00 00 f0 41
Step 60: Export and Test
- Export as
0x0020_dynamic-conditionals-h.bin - Convert to UF2:
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0020_dynamic-conditionals
python ..\uf2conv.py build\0x0020_dynamic-conditionals-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
- Flash and test:
- Press '1' → prints "1" and "one", servo moves
- Press '2' → prints "2" and "two", servo moves
- Press 'x' → NO OUTPUT, but servo moves silently!
- Press 'y' → NO OUTPUT, but servo moves silently!
📊 Part 19: Summary and Review
What We Accomplished
- Learned static vs dynamic conditionals - Fixed vs runtime-determined values
- Understood if/else and switch/case - Two ways to branch in C
- Mastered PWM calculations - 150MHz to 50Hz servo signal
- Identified conditional branches in assembly - beq, bne, cmp instructions
- Hacked string literals - Changed "one" to "fun"
- Modified timing values - Sped up servo from 500ms to 100ms
- Created stealth commands - Hidden 'x' and 'y' keys
- NOPed out print statements - Removed logging for stealth
- Redirected branch targets - Changed program flow
Static vs Dynamic Summary
┌─────────────────────────────────────────────────────────────────┐
│ Static Conditionals │
│ ─────────────────── │
│ • Variable set once, never changes │
│ • Same path taken every iteration │
│ • Compiler may optimize out dead branches │
│ • Example: int choice = 1; if (choice == 1) │
├─────────────────────────────────────────────────────────────────┤
│ Dynamic Conditionals │
│ ──────────────────── │
│ • Variable changes based on input/sensors │
│ • Different paths taken based on runtime state │
│ • All branches must remain in binary │
│ • Example: choice = getchar(); if (choice == '1') │
└─────────────────────────────────────────────────────────────────┘
PWM Calculation Summary
┌─────────────────────────────────────────────────────────────────┐
│ Servo PWM Calculation Chain │
│ │
│ Angle (degrees) → Pulse Width (µs) → PWM Ticks → Servo Motion │
│ │
│ 0° → 1000 µs → 1000 ticks → Fully counter-clockwise │
│ 90° → 1500 µs → 1500 ticks → Center position │
│ 180° → 2000 µs → 2000 ticks → Fully clockwise │
│ │
│ Formula: pulse = 1000 + (angle/180) × 1000 │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Memory Addresses
| Memory Address | Description |
|---|---|
0x10000234 |
main() function |
0x40070000 |
UART0 hardware registers |
0x1f4 |
500 (sleep_ms delay) |
0x7D0 |
2000 (max pulse width) |
0x3E8 |
1000 (min pulse width) |
0x43340000 |
180.0f (max angle) |
✅ Practice Exercises
Exercise 1: Change Servo Angle Range
Modify the servo to sweep from 45° to 135° instead of 0° to 180°.
Hint: Calculate IEEE-754 values for 45.0f and 135.0f.
Exercise 2: Add a Third Command
Add support for key '3' that moves the servo to 90° (center position).
Hint: You'll need to find space in the binary or modify existing code.
Exercise 3: Reverse the Servo Direction
Make '1' do what '2' does and vice versa.
Hint: Swap the branch targets.
Exercise 4: Speed Profile
Create fast movement for '1' (100ms) and slow movement for '2' (1000ms).
Hint: Find both sleep_ms calls and patch them differently.
Exercise 5: Complete Stealth Mode
Make ALL servo movements silent - remove ALL printf and puts calls.
Hint: NOP out every output function call.
🎓 Key Takeaways
-
Static conditionals have fixed outcomes - The same path always executes
-
Dynamic conditionals respond to input - Different paths based on runtime state
-
PWM frequency = 50Hz for servos - One pulse every 20ms
-
Pulse width encodes position - 1ms=0°, 1.5ms=90°, 2ms=180°
-
beq = branch if equal - Jumps when comparison matches
-
bne = branch if not equal - Jumps when comparison doesn't match
-
NOP erases code without changing size -
00 bfin ARM Thumb -
Branch targets can be redirected - Change where code jumps to
-
IEEE-754 is needed for angles - Floats have specific bit patterns
-
Stealth requires removing ALL output - NOP out printf AND puts
📖 Glossary
| Term | Definition |
|---|---|
| beq | Branch if Equal - ARM conditional jump |
| bne | Branch if Not Equal - ARM conditional jump |
| Dynamic Conditional | Condition that changes based on runtime input |
| Duty Cycle | Percentage of time signal is HIGH |
| getchar() | C function that reads one character from input |
| NOP | No Operation - instruction that does nothing |
| PWM | Pulse Width Modulation - variable duty cycle signal |
| SG90 | Common hobby servo motor model |
| Static Conditional | Condition with fixed/predetermined outcome |
| switch/case | C structure for multiple discrete value comparisons |
| Wrap Value | PWM counter maximum before reset |
🔗 Additional Resources
ASCII Reference Table
| Character | Hex | Decimal |
|---|---|---|
| '0' | 0x30 | 48 |
| '1' | 0x31 | 49 |
| '2' | 0x32 | 50 |
| 'x' | 0x78 | 120 |
| 'y' | 0x79 | 121 |
| '\r' | 0x0d | 13 |
| '\n' | 0x0a | 10 |
IEEE-754 Common Angles
| Angle | IEEE-754 Hex | Little-Endian Bytes |
|---|---|---|
| 0.0 | 0x00000000 | 00 00 00 00 |
| 30.0 | 0x41f00000 | 00 00 f0 41 |
| 45.0 | 0x42340000 | 00 00 34 42 |
| 90.0 | 0x42b40000 | 00 00 b4 42 |
| 135.0 | 0x43070000 | 00 00 07 43 |
| 180.0 | 0x43340000 | 00 00 34 43 |
ARM Thumb NOP Encodings
| Instruction | Encoding | Size |
|---|---|---|
nop |
00 bf |
2 bytes |
nop.w |
00 f0 00 80 |
4 bytes |
RP2350 Key Addresses
| Address | Peripheral |
|---|---|
0x40070000 |
UART0 |
0x40078000 |
UART1 |
0x40050000 |
PWM |
🚨 Real-World Implications
Why Stealth Commands Matter
The ability to create hidden commands has serious implications:
Legitimate Uses:
- Factory test modes
- Debugging interfaces
- Emergency recovery features
Malicious Uses:
- Backdoors in firmware
- Hidden surveillance features
- Unauthorized control of systems
Real-World Example
Imagine a drone with hacked firmware:
- Normal keys ('1', '2') control it visibly with logging
- Hidden keys ('x', 'y') control it with NO log entries
- An attacker could operate the drone while security monitors show nothing
The Nuclear Fuel Rod Analogy
A fast-moving servo is like a nuclear fuel rod:
- Both are small components with immense power
- Both require precise control to prevent damage
- Both can "go critical" if pushed beyond limits
- Both teach the importance of safety margins
Remember: The techniques you learned today demonstrate how conditional logic can be manipulated at the binary level. Understanding these attacks helps us build more secure embedded systems. Always use your skills ethically and responsibly!
Happy hacking! 🔧