Lab Design Principles¶
This guide covers the pedagogical and technical principles for designing effective embedded security labs.
Core Design Philosophy¶
Educational First, Realistic Second¶
Labs should prioritize learning over realism. While real-world accuracy is valuable, educational clarity takes precedence:
- Clear vulnerability boundaries
- Predictable behavior
- Helpful debugging features
- Progressive difficulty
Scaffold Learning¶
Each lab builds on previous knowledge:
- Foundation: Basic concepts (memory, registers, I/O)
- Exploration: Simple vulnerabilities with guidance
- Application: Complex scenarios requiring synthesis
- Mastery: Multi-stage exploits and defenses
Vulnerability Selection¶
Categories and Progression¶
Memory Corruption (Beginner → Advanced)¶
- Stack Buffer Overflow
- Fixed-size overflows
- Variable-size overflows
- Off-by-one errors
-
Integer overflows leading to buffer overflow
-
Heap Exploitation
- Use-after-free
- Double-free
- Heap overflow
-
Metadata corruption
-
Format String
- Information disclosure
- Arbitrary read
- Arbitrary write
- Combined with other vulnerabilities
Logic and Control Flow¶
- Authentication Bypass
- Weak comparisons
- Logic errors
- Race conditions
-
State confusion
-
Command Injection
- Direct injection
- Second-order injection
-
Filter bypass
-
Type Confusion
- Union misuse
- Casting errors
- Variant types
Cryptographic Weaknesses¶
- Weak Implementations
- Poor random number generation
- Reused nonces
- Timing attacks
-
Padding oracles
-
Protocol Flaws
- Replay attacks
- Man-in-the-middle
- Downgrade attacks
Embedded-Specific Vulnerabilities¶
Hardware Interface Attacks¶
// Example: Glitching-vulnerable authentication
void check_password(void) {
if (memcmp(input, password, 16) == 0) {
auth_flag = 1; // Glitch here!
}
__DSB(); // Memory barrier
if (auth_flag == 1) {
grant_access();
}
}
Interrupt and Timing Attacks¶
// Example: TOCTOU in interrupt handler
volatile uint32_t sensor_value;
volatile bool sensor_valid;
void ADC_IRQHandler(void) {
sensor_value = ADC_Read();
sensor_valid = true; // Race condition window
}
void process_sensor(void) {
if (sensor_valid) {
// Value could change here!
if (sensor_value > THRESHOLD) {
trigger_alarm();
}
}
}
Power Analysis Preparation¶
// Example: Power-analysis vulnerable comparison
bool check_pin(uint32_t input) {
uint32_t stored_pin = read_secure_pin();
// Bad: Early return leaks timing
for (int i = 0; i < 4; i++) {
if (((input >> (i*8)) & 0xFF) !=
((stored_pin >> (i*8)) & 0xFF)) {
return false; // Leaks position
}
}
return true;
}
Difficulty Calibration¶
Easy Labs (100-200 points)¶
- Single vulnerability: One clear path to success
- Generous hints: Debug info readily available
- No protections: No ASLR, canaries, or NX
- Example: Basic stack overflow with win function
void vulnerable(void) {
char buffer[32];
uart_gets(buffer); // Obvious overflow
}
void win(void) {
print_flag(); // Clear target
}
Medium Labs (200-400 points)¶
- Multiple steps: Leak → Calculate → Exploit
- Some protections: Basic canaries or checks
- Limited info: Must discover addresses
- Example: Format string + buffer overflow chain
void phase1(void) {
char buffer[64];
uart_printf(buffer); // Leak addresses
}
void phase2(void) {
char buffer[32];
if (auth_level > 0) {
uart_gets(buffer); // Overflow after auth
}
}
Hard Labs (400-600 points)¶
- Complex chains: Multiple vulnerabilities needed
- Active defenses: Canaries, bounds checking, encoding
- Obfuscation: Hidden functionality, anti-debug
- Example: Heap exploitation with ASLR bypass
struct object {
void (*handler)(char *);
char data[32];
};
void process(struct object *obj) {
if (validate_object(obj)) {
obj->handler(obj->data); // Control flow hijack
}
}
Learning Objective Alignment¶
Map Vulnerabilities to Concepts¶
Stack Exploitation Lab¶
Concepts Taught:
- Stack frame layout
- Function epilogue/prologue
- Return address manipulation
- ARM calling conventions
Implementation Requirements:
// Provide clear stack visualization
void debug_stack(void) {
uint32_t sp;
__asm__("mov %0, sp" : "=r"(sp));
uart_printf("Stack dump from 0x%08x:\n", sp);
for (int i = 0; i < 16; i++) {
uint32_t *addr = (uint32_t *)(sp + i*4);
uart_printf(" [%08x]: %08x", addr, *addr);
// Annotate important values
if (*addr == (uint32_t)main) {
uart_printf(" <- return to main");
}
uart_printf("\n");
}
}
Format String Lab¶
Concepts Taught:
- Printf internals
- Variable argument handling
- Memory disclosure
- Arbitrary write primitives
Implementation Requirements:
// Educational format string setup
void log_activity(void) {
char activity[128];
static int log_count = 0;
uart_puts("Enter activity: ");
uart_gets(activity);
// Show format string processing
uart_printf("[LOG %d] ", ++log_count);
uart_printf(activity); // Vulnerable!
uart_printf("\n");
// Provide memory map for reference
if (strstr(activity, "help")) {
show_memory_map();
}
}
Constraint Considerations¶
Hardware Limitations¶
Memory Constraints¶
// BAD: Wastes limited RAM
char huge_buffer[4096]; // Won't fit!
// GOOD: Efficient memory use
char buffer[64];
const char *messages[] = { // In flash
"Option 1",
"Option 2"
};
No Memory Protection¶
// Design around lack of MMU
// BAD: Assumes memory protection
void exploit_me(void) {
char *code = malloc(64);
generate_shellcode(code);
((void(*)())code)(); // Would need NX bypass on modern systems
}
// GOOD: Works within constraints
void exploit_me(void) {
void (*func_ptr)() = normal_function;
overwrite_pointer(&func_ptr); // Redirect to existing code
func_ptr();
}
Debugging Support¶
Progressive Disclosure¶
// Level 1: Basic info
void show_hints_easy(void) {
uart_printf("Buffer at: %p\n", buffer);
uart_printf("Target at: %p\n", win_function);
}
// Level 2: More details
void show_hints_medium(void) {
uart_printf("Current SP: %p\n", get_sp());
uart_printf("Return addr stored at: %p\n", &return_addr);
}
// Level 3: Full debugging
void show_hints_hard(void) {
dump_registers();
dump_stack(32);
show_memory_map();
}
Breadcrumbs for Learning¶
// Leave educational breadcrumbs
void vulnerable_function(void) {
char buffer[32];
#ifdef STUDENT_MODE
// Helpful comment in binary
__asm__(".ascii \"BUFFER_STARTS_HERE\"");
#endif
dangerous_copy(buffer, user_input);
#ifdef STUDENT_MODE
__asm__(".ascii \"RETURN_ADDRESS_NEAR\"");
#endif
}
Anti-Patterns to Avoid¶
Don't Make It Frustrating¶
// BAD: Random crashes
void unstable_vuln(void) {
if (rand() % 10 == 0) {
exploitable(); // Only works 10% of time!
}
}
// GOOD: Deterministic behavior
void stable_vuln(void) {
if (check_condition()) {
exploitable(); // Always works with right input
}
}
Don't Hide Critical Information¶
// BAD: No way to discover addresses
void impossible(void) {
void (*hidden)() = (void *)0x12345678; // How would they know?
}
// GOOD: Discoverable through analysis
void possible(void) {
uart_printf("Debug: function at %p\n", target_function);
// Or leave in strings, symbols, etc.
}
Don't Require Specific Tools¶
// BAD: Requires specific debugger features
// "Set hardware breakpoint at 0x1234, modify R3"
// GOOD: Multiple solution paths
// Can solve with debugger, static analysis, or dynamic testing
Testing Your Design¶
Student Perspective Checklist¶
- Can a student with prerequisites solve this?
- Is the vulnerability discoverable through analysis?
- Are there multiple valid approaches?
- Do hints progressively guide without giving away?
- Is the solution deterministic and repeatable?
Technical Validation¶
- Runs reliably in QEMU
- Exploit works across platforms
- Memory layout is predictable
- No unintended solutions
- Automated tests pass
Educational Value¶
- Clear learning objectives met
- Builds on previous knowledge
- Introduces new concepts appropriately
- Provides "aha!" moment
- Encourages experimentation
Example: Well-Designed Lab¶
Authentication Bypass Lab (Medium Difficulty)¶
#include <embsec/embsec.h>
#define MAX_ATTEMPTS 3
#define PIN_LENGTH 4
static uint8_t correct_pin[PIN_LENGTH] = {1, 2, 3, 4};
static int attempts_left = MAX_ATTEMPTS;
static bool authenticated = false;
// Vulnerable: Timing attack possible
bool check_pin(uint8_t *input) {
for (int i = 0; i < PIN_LENGTH; i++) {
if (input[i] != correct_pin[i]) {
delay_ms(10); // Simulate processing
return false;
}
delay_ms(50); // Longer delay on match!
}
return true;
}
// Educational: Show timing
void attempt_login(void) {
uint8_t pin[PIN_LENGTH];
uint32_t start_time, end_time;
uart_puts("Enter 4-digit PIN: ");
for (int i = 0; i < PIN_LENGTH; i++) {
pin[i] = uart_getc() - '0';
}
start_time = get_timer_us();
bool success = check_pin(pin);
end_time = get_timer_us();
uart_printf("\nTime taken: %d us\n", end_time - start_time);
if (success) {
authenticated = true;
uart_puts("Access granted!\n");
print_flag();
} else {
attempts_left--;
uart_printf("Wrong PIN. %d attempts left.\n", attempts_left);
}
}
// Alternative path: Logic bug
void debug_mode(void) {
uart_puts("Debug mode (disabled in production)\n");
// Oops: Doesn't check if actually disabled!
#ifdef PRODUCTION
return;
#endif
uart_printf("PIN is: ");
for (int i = 0; i < PIN_LENGTH; i++) {
uart_printf("%d", correct_pin[i]);
}
uart_puts("\n");
}
int main(void) {
system_init();
uart_init();
timer_init();
uart_puts("\n=== Secure Authenticator ===\n");
uart_puts("Firmware v2.1.0-beta\n\n"); // Hint: beta = debug features?
while (attempts_left > 0 && !authenticated) {
uart_puts("1. Enter PIN\n");
uart_puts("2. System Info\n");
uart_puts("9. Debug Mode\n"); // Hidden option!
uart_puts("Choice: ");
char choice = uart_getc();
uart_putc(choice);
uart_puts("\n");
switch (choice) {
case '1':
attempt_login();
break;
case '2':
show_system_info();
break;
case '9':
debug_mode();
break;
}
}
if (attempts_left == 0) {
uart_puts("\nSystem locked!\n");
}
return 0;
}
This lab demonstrates good design because:
- Multiple solutions: Timing attack OR logic bug
- Progressive difficulty: Can brute force, timing attack, or find debug mode
- Educational value: Teaches timing attacks and secure coding
- Clear feedback: Shows timing measurements
- Realistic scenario: Common authentication weaknesses