Skip to content

Vulnerability Patterns

This guide provides detailed implementation patterns for common vulnerabilities in embedded systems, specifically tailored for ARM Cortex-M educational labs.

Memory Corruption Vulnerabilities

Stack Buffer Overflow

Basic Pattern

void basic_stack_overflow(void) {
    char buffer[32];
    uart_puts("Enter name: ");
    uart_gets(buffer);  // No bounds checking
}

Variations

Off-by-One

void off_by_one_overflow(void) {
    char buffer[32];
    int i = 0;

    uart_puts("Enter data: ");
    // Bug: Should be i < 32, not i <= 32
    while (i <= 32 && (buffer[i] = uart_getc()) != '\n') {
        i++;
    }
    buffer[i] = '\0';  // Writes past buffer end
}

Integer Overflow to Buffer Overflow

void integer_overflow_pattern(void) {
    uint8_t size;
    char buffer[256];

    uart_puts("Enter size: ");
    size = uart_getc();  // User controls size

    uart_puts("Enter data: ");
    // If size > 256, wraps around
    for (int i = 0; i < size; i++) {
        buffer[i] = uart_getc();
    }
}

Stack Variable Overwrite

typedef struct {
    char name[32];
    uint32_t priv_level;
    void (*handler)(void);
} user_t;

void struct_overflow(void) {
    user_t user = {
        .priv_level = 0,
        .handler = normal_handler
    };

    uart_puts("Username: ");
    uart_gets(user.name);  // Can overflow into priv_level/handler

    if (user.priv_level > 0) {
        user.handler();  // Control flow hijack
    }
}

ARM-Specific Considerations

// Ensure functions are Thumb-aligned
__attribute__((aligned(2)))
void target_function(void) {
    generate_flag();
}

// Helper to show Thumb bit requirement
void show_function_address(void) {
    uart_printf("Target (raw): 0x%08x\n", (uint32_t)target_function);
    uart_printf("Target (Thumb): 0x%08x\n", (uint32_t)target_function | 1);
}

Heap Exploitation

Use-After-Free

typedef struct node {
    struct node *next;
    void (*process)(char *);
    char data[32];
} node_t;

static node_t *free_list = NULL;

void use_after_free_pattern(void) {
    node_t *n = malloc(sizeof(node_t));
    n->process = safe_process;

    // ... use node ...

    free(n);  // Node freed

    // Bug: Still accessible
    if (user_wants_process) {
        n->process(n->data);  // UAF!
    }
}

Double-Free

void double_free_pattern(void) {
    char *buf1 = malloc(64);
    char *buf2 = malloc(64);

    free(buf1);
    free(buf2);
    free(buf1);  // Double free!

    // Allocations may return same chunk
    char *buf3 = malloc(64);  // Might be buf1
    char *buf4 = malloc(64);  // Also might be buf1!
}

Heap Metadata Corruption

// Simple allocator for teaching
typedef struct chunk {
    size_t size;
    struct chunk *next;
    char data[];
} chunk_t;

void heap_metadata_corruption(void) {
    chunk_t *chunk = (chunk_t *)malloc(32);

    uart_puts("Data: ");
    // Overflow can corrupt size/next fields
    uart_gets(chunk->data);

    free(chunk);  // Uses corrupted metadata
}

Format String Vulnerabilities

Information Disclosure

void format_string_leak(void) {
    char buffer[128];
    uint32_t secret = 0x41414141;

    uart_puts("Log message: ");
    uart_gets(buffer);

    // Direct format string bug
    uart_printf(buffer);  // Can leak stack contents
    uart_printf("\n");
}

// Leak specific values
void targeted_leak(void) {
    struct {
        char tag[8];
        uint32_t secret_key;
        void *function_ptr;
    } data = {
        .tag = "DATA",
        .secret_key = 0xDEADBEEF,
        .function_ptr = critical_function
    };

    char format[64];
    uart_puts("Format: ");
    uart_gets(format);

    // Stack layout predictable
    uart_printf(format, &data);  // %s leaks, %n writes
}

Arbitrary Write

void format_string_write(void) {
    static uint32_t auth_flag = 0;
    char buffer[128];

    uart_printf("Auth flag at: 0x%08x\n", &auth_flag);

    uart_puts("Enter log: ");
    uart_gets(buffer);

    uart_printf(buffer);  // %n can write to auth_flag

    if (auth_flag != 0) {
        grant_access();
    }
}

GOT/Function Pointer Overwrite

// Simplified GOT-like structure for embedded
typedef struct {
    void (*puts)(const char *);
    void (*printf)(const char *, ...);
    void (*system)(const char *);
} got_t;

static got_t got = {
    .puts = uart_puts,
    .printf = uart_printf,
    .system = NULL  // Not implemented
};

void got_overwrite_pattern(void) {
    char buffer[128];

    uart_printf("GOT at: 0x%08x\n", &got);
    uart_gets(buffer);

    // Format string can overwrite function pointers
    uart_printf(buffer);

    // Later call uses corrupted pointer
    got.puts("Test message\n");
}

Logic Vulnerabilities

Integer Overflow/Underflow

Arithmetic Overflow

void withdraw_funds(uint32_t amount) {
    static uint32_t balance = 1000;

    // Bug: No overflow check
    if (balance >= amount) {
        balance -= amount;  // Can underflow!
        uart_printf("New balance: %u\n", balance);

        if (balance > 1000000) {  // Underflowed!
            grant_admin_access();
        }
    }
}

Size Calculation Errors

void process_packet(void) {
    struct {
        uint16_t type;
        uint16_t length;
        char data[256];
    } packet;

    uart_read(&packet.type, 2);
    uart_read(&packet.length, 2);

    // Integer overflow in calculation
    size_t total_size = packet.length + sizeof(packet.type) + sizeof(packet.length);

    if (total_size <= sizeof(packet)) {
        uart_read(packet.data, packet.length);  // Overflow!
    }
}

Type Confusion

Union Type Confusion

typedef enum { TYPE_INT, TYPE_PTR, TYPE_FUNC } data_type_t;

typedef struct {
    data_type_t type;
    union {
        uint32_t integer;
        void *pointer;
        void (*function)(void);
    } value;
} variant_t;

void type_confusion_pattern(void) {
    variant_t data;

    // Set as integer
    data.type = TYPE_INT;
    data.value.integer = uart_get_hex();

    // ... later ...

    // Bug: Doesn't check type!
    if (user_wants_execute) {
        data.value.function();  // Treats integer as function!
    }
}

Race Conditions

TOCTOU (Time-of-Check Time-of-Use)

static volatile uint32_t privilege_level = 0;

void toctou_pattern(void) {
    // Check
    if (privilege_level == 0) {
        uart_puts("Access denied\n");
        return;
    }

    // Window of opportunity here!
    delay_ms(100);  // Simulate processing

    // Use
    if (privilege_level > 0) {  // Could have changed!
        perform_privileged_operation();
    }
}

// ISR that can trigger during window
void TIMER0_IRQHandler(void) {
    if (special_condition()) {
        privilege_level = 1;  // Race condition!
    }
}

State Machine Confusion

typedef enum {
    STATE_INIT,
    STATE_AUTH,
    STATE_READY,
    STATE_EXEC
} state_t;

static state_t current_state = STATE_INIT;

void state_confusion_pattern(void) {
    switch (current_state) {
        case STATE_INIT:
            if (user_provides_password()) {
                current_state = STATE_AUTH;
            }
            break;

        case STATE_AUTH:
            if (verify_password()) {
                current_state = STATE_READY;
            }
            break;

        case STATE_READY:
            // Bug: No state validation
            execute_command();  // What if we skip auth?
            break;
    }
}

// Backdoor: Hidden state transition
void debug_command(void) {
    if (uart_getc() == 0x42) {
        current_state = STATE_READY;  // Skip auth!
    }
}

Cryptographic Vulnerabilities

Weak Random Number Generation

Predictable Seed

void weak_random_pattern(void) {
    // Bad: Uses predictable seed
    srand(get_system_ticks());  // Ticks since boot

    uint32_t session_key = rand();
    uart_printf("Session established: %08x\n", session_key);
}

// Better (but still weak for teaching)
void slightly_better_random(void) {
    uint32_t seed = get_system_ticks();
    seed ^= (uint32_t)&seed;  // Stack address
    seed ^= read_temp_sensor();  // Some entropy

    srand(seed);
}

Reused Nonces

static uint32_t nonce_counter = 0;

void encrypt_message(uint8_t *msg, size_t len) {
    uint32_t nonce = nonce_counter;  // Bug: Doesn't increment!

    // Simplified CTR mode
    for (size_t i = 0; i < len; i += 4) {
        uint32_t keystream = generate_keystream(nonce, i/4);
        *(uint32_t *)(msg + i) ^= keystream;
    }

    uart_write(msg, len);
}

Timing Attacks

Password Comparison

// Vulnerable to timing attack
bool check_password_bad(const char *input) {
    const char *correct = "SuperSecret123!";

    for (int i = 0; i < strlen(correct); i++) {
        if (input[i] != correct[i]) {
            return false;  // Early return leaks info
        }
        delay_us(10);  // Makes timing difference measurable
    }
    return true;
}

// Still vulnerable but harder
bool check_password_harder(const char *input) {
    const char *correct = "SuperSecret123!";
    int differences = 0;

    for (int i = 0; i < 15; i++) {
        differences |= (input[i] ^ correct[i]);
    }

    return differences == 0;
}

Cache Timing (Simulated)

// Simulate cache-like timing behavior
uint32_t cached_value = 0;
uint32_t cache_tag = 0;

uint32_t read_secure_memory(uint32_t addr) {
    if ((addr & 0xFFFFF000) == cache_tag) {
        // Cache hit - fast
        delay_us(1);
        return cached_value;
    } else {
        // Cache miss - slow
        delay_us(100);
        cache_tag = addr & 0xFFFFF000;
        cached_value = *(uint32_t *)addr;
        return cached_value;
    }
}

Hardware-Specific Vulnerabilities

Glitching Points

Simple Glitch Target

void glitch_vulnerable_auth(void) {
    uint32_t password_correct = 0;

    // Check password
    if (check_password_complex()) {
        password_correct = 1;
    }

    // Glitch target - single instruction
    if (password_correct == 1) {  // Glitch here!
        grant_access();
    }
}

// More realistic glitch target
void glitch_with_loop(void) {
    int auth_count = 0;

    // Multiple checks
    for (int i = 0; i < 3; i++) {
        if (check_password_part(i)) {
            auth_count++;  // Glitch to skip increment
        }
    }

    if (auth_count == 3) {  // Or glitch comparison
        grant_access();
    }
}

DMA Abuse

DMA Race Condition

static uint8_t secure_buffer[64];
static volatile bool dma_busy = false;

void dma_vulnerability(void) {
    // Start DMA transfer
    dma_busy = true;
    DMA_StartTransfer(user_buffer, secure_buffer, 64);

    // Bug: Doesn't wait for completion
    if (verify_buffer(secure_buffer)) {  // Race condition!
        process_secure_data();
    }
}

void DMA_IRQHandler(void) {
    dma_busy = false;
    // But secure_buffer might be partially updated!
}

Peripheral Abuse

UART Overflow

void uart_irq_vulnerability(void) {
    static char rx_buffer[32];
    static int rx_index = 0;

    // Bug: No bounds check in ISR
    if (UART0->FR & UART_FR_RXFF) {
        rx_buffer[rx_index++] = UART0->DR;  // Can overflow!

        if (rx_buffer[rx_index-1] == '\n') {
            process_command(rx_buffer);
            rx_index = 0;
        }
    }
}

Implementation Guidelines

Making Vulnerabilities Educational

Clear Vulnerable Pattern

// GOOD: Obvious vulnerability for teaching
void educational_overflow(void) {
    char buffer[32];  // Clear size

    uart_printf("Buffer at: %p\n", buffer);  // Help student
    uart_printf("Enter up to 32 chars: ");   // Hint at limit

    // Obviously vulnerable
    while ((*buffer++ = uart_getc()) != '\n');
}

// BAD: Too obscure
void obscure_vulnerability(void) {
    char b[0x20];
    for(int i=0;uart_rx()!=0xa;)b[i++]=uart_rx();
}

Progressive Difficulty

// Level 1: Direct exploitation
void level1_overflow(void) {
    char buffer[32];
    uart_gets(buffer);  // Simple overflow

    if (auth_flag) {
        win();
    }
}

// Level 2: Requires address leak
void level2_overflow(void) {
    char buffer[32];
    void (*func)() = normal_function;

    if (debug_mode) {
        uart_printf("func at: %p\n", &func);
    }

    uart_gets(buffer);  // Overflow to func
    func();
}

// Level 3: ASLR bypass needed
void level3_overflow(void) {
    // Randomize stack location
    char padding[rand() % 64];
    char buffer[32];

    // Student must leak and calculate
    uart_gets(buffer);
}

Safe Implementation Practices

Contain Failures

// Wrap vulnerable code
void safe_vulnerable_wrapper(void) {
    // Save critical state
    uint32_t saved_sp;
    __asm__("mov %0, sp" : "=r"(saved_sp));

    // Run vulnerable code
    actually_vulnerable();

    // Restore if needed
    __asm__("mov sp, %0" : : "r"(saved_sp));
}

Provide Recovery

void recoverable_exploit(void) {
    // Set up watchdog
    WDT_Enable(5000);  // 5 second timeout

    // Vulnerable code here
    vulnerable_function();

    // If we get here, disable watchdog
    WDT_Disable();
}

void WDT_Handler(void) {
    // Reset to known state
    uart_puts("\n[RESET] Recovering from crash\n");
    NVIC_SystemReset();
}

Memory Protection

// Define safe regions for student exploitation
#define STUDENT_STACK_START 0x20000000
#define STUDENT_STACK_END   0x20001000
#define STUDENT_HEAP_START  0x20001000  
#define STUDENT_HEAP_END    0x20002000

void check_memory_access(uint32_t addr) {
    if (addr < STUDENT_STACK_START || addr > STUDENT_HEAP_END) {
        uart_puts("[ERROR] Access violation\n");
        reset_lab();
    }
}

Testing Vulnerability Patterns

Verify Exploitability

def test_vulnerability_exists(self):
    """Ensure the vulnerability is actually exploitable"""
    self.start_qemu()

    # Test benign input first
    output = self.send_input("A" * 20)
    self.assertIn("normal", output.lower())

    # Test malicious input
    output = self.send_input("A" * 100)
    self.assertNotIn("normal", output.lower())

    # Test precise exploitation
    payload = "A" * 64 + p32(0x1234)
    output = self.send_input(payload)
    self.assertIn("exploited", output)

Ensure Deterministic Behavior

def test_deterministic_addresses(self):
    """Verify addresses don't change between runs"""
    addresses = []

    for _ in range(3):
        self.start_qemu()
        output = self.get_debug_info()

        addr = self.extract_address(output, 
                                   r'Target: 0x([0-9a-fA-F]+)')
        addresses.append(addr)

        self.tearDown()
        self.setUp()

    # All addresses should be identical
    self.assertEqual(len(set(addresses)), 1,
                    f"Non-deterministic addresses: {addresses}")

Common Pitfalls to Avoid

Don't Make It Impossible

// BAD: No way to discover this
void impossible_to_find(void) {
    if (*(uint32_t *)0x12345678 == 0xDEADBEEF) {
        win();  // How would they know?
    }
}

// GOOD: Discoverable through analysis
void discoverable(void) {
    uint32_t magic = 0xDEADBEEF;
    uart_printf("Debug: magic at %p\n", &magic);

    if (user_input == magic) {
        win();
    }
}

Don't Break QEMU

// BAD: Crashes emulator
void crash_qemu(void) {
    void (*bad_ptr)() = (void *)0xFFFFFFFF;
    bad_ptr();  // Invalid execution
}

// GOOD: Contained failure  
void safe_failure(void) {
    void (*ptr)() = get_user_pointer();

    if ((uint32_t)ptr >= 0x00000000 && 
        (uint32_t)ptr <= 0x00040000) {
        ptr();  // Only call if in code region
    }
}

Don't Require Specific Timing

// BAD: Requires precise timing
void timing_dependent(void) {
    start_timer();

    if (get_input() == 'A' && 
        elapsed_ms() == 1337) {  // Nearly impossible
        win();
    }
}

// GOOD: Timing window
void timing_window(void) {
    start_timer();

    if (get_input() == 'A' && 
        elapsed_ms() >= 1000 && 
        elapsed_ms() <= 2000) {  // 1 second window
        win();
    }
}