Skip to content

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:

  1. Foundation: Basic concepts (memory, registers, I/O)
  2. Exploration: Simple vulnerabilities with guidance
  3. Application: Complex scenarios requiring synthesis
  4. Mastery: Multi-stage exploits and defenses

Vulnerability Selection

Categories and Progression

Memory Corruption (Beginner → Advanced)

  1. Stack Buffer Overflow
  2. Fixed-size overflows
  3. Variable-size overflows
  4. Off-by-one errors
  5. Integer overflows leading to buffer overflow

  6. Heap Exploitation

  7. Use-after-free
  8. Double-free
  9. Heap overflow
  10. Metadata corruption

  11. Format String

  12. Information disclosure
  13. Arbitrary read
  14. Arbitrary write
  15. Combined with other vulnerabilities

Logic and Control Flow

  1. Authentication Bypass
  2. Weak comparisons
  3. Logic errors
  4. Race conditions
  5. State confusion

  6. Command Injection

  7. Direct injection
  8. Second-order injection
  9. Filter bypass

  10. Type Confusion

  11. Union misuse
  12. Casting errors
  13. Variant types

Cryptographic Weaknesses

  1. Weak Implementations
  2. Poor random number generation
  3. Reused nonces
  4. Timing attacks
  5. Padding oracles

  6. Protocol Flaws

  7. Replay attacks
  8. Man-in-the-middle
  9. 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();
}
// 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:

  1. Multiple solutions: Timing attack OR logic bug
  2. Progressive difficulty: Can brute force, timing attack, or find debug mode
  3. Educational value: Teaches timing attacks and secure coding
  4. Clear feedback: Shows timing measurements
  5. Realistic scenario: Common authentication weaknesses