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();
}
}