Skip to content

Best Practices

This guide outlines technical best practices for developing high-quality embedded security labs that are educational, reliable, and maintainable.

Code Quality Standards

Coding Style

Consistent Formatting

// Use consistent style throughout the lab
// This example follows the EmbSec style guide

#include <embsec/embsec.h>
#include <stdint.h>
#include <stdbool.h>

// Constants in UPPER_CASE
#define BUFFER_SIZE 64
#define MAX_ATTEMPTS 3

// Type definitions
typedef struct {
    uint32_t id;
    char name[32];
    bool is_admin;
} user_t;

// Function prototypes with descriptive names
static void process_user_input(void);
static bool validate_credentials(const char *username, const char *password);
static void vulnerable_function(char *input);

// Global state clearly marked
static volatile uint32_t g_auth_level = 0;
static user_t g_current_user = {0};

// Functions with clear purpose
static void process_user_input(void) {
    char buffer[BUFFER_SIZE];

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

    // Clear control flow
    if (strcmp(buffer, "login") == 0) {
        handle_login();
    } else if (strcmp(buffer, "admin") == 0) {
        handle_admin();
    } else {
        uart_puts("Unknown command\n");
    }
}

Meaningful Names

// BAD: Cryptic names
void f(char *b) {
    char l[32];
    strcpy(l, b);  // What does this do?
}

// GOOD: Self-documenting
void process_username(char *username_input) {
    char local_username[MAX_USERNAME_LENGTH];
    strcpy(local_username, username_input);  // Clear intent
}

Comment Strategy

// DO: Explain the "why", not the "what"

// Delay to make timing attacks measurable in QEMU
delay_ms(10);

// Allocate on heap to ensure consistent addresses across runs
user_t *user = malloc(sizeof(user_t));

// DON'T: State the obvious
i++;  // Increment i

Memory Safety

Buffer Handling

// Always provide safe alternatives alongside vulnerable functions
void safe_string_copy(char *dest, const char *src, size_t dest_size) {
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0';
}

// Vulnerable function for teaching
void vulnerable_string_copy(char *dest, const char *src) {
    strcpy(dest, src);  // No bounds checking - intentional!
}

// Clear which function is vulnerable
void process_input(void) {
    char buffer[32];

    #ifdef SECURE_MODE
    safe_string_copy(buffer, user_input, sizeof(buffer));
    #else
    vulnerable_string_copy(buffer, user_input);  // VULNERABLE
    #endif
}

Resource Management

// Track allocations to prevent leaks
typedef struct allocation {
    void *ptr;
    size_t size;
    struct allocation *next;
} allocation_t;

static allocation_t *g_allocations = NULL;

void *tracked_malloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr) {
        allocation_t *track = malloc(sizeof(allocation_t));
        track->ptr = ptr;
        track->size = size;
        track->next = g_allocations;
        g_allocations = track;
    }
    return ptr;
}

void cleanup_all_allocations(void) {
    while (g_allocations) {
        allocation_t *next = g_allocations->next;
        free(g_allocations->ptr);
        free(g_allocations);
        g_allocations = next;
    }
}

Error Handling

Graceful Failures

// Provide useful error messages
bool load_config(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) {
        uart_printf("Error: Cannot open config file '%s'\n", filename);
        uart_puts("Using default configuration\n");
        return load_default_config();
    }

    // Process file...
    fclose(fp);
    return true;
}

// Recover from student errors
void student_function_wrapper(void) {
    // Save state before student code
    uint32_t saved_sp = get_sp();

    __try {
        student_provided_exploit();
    } __except {
        uart_puts("\n[ERROR] Exploit crashed - recovering\n");
        set_sp(saved_sp);
        show_hint_about_crash();
    }
}

Defensive Programming

// Validate all inputs, even in vulnerable functions
void vulnerable_but_stable(void) {
    char buffer[32];
    char *input = get_user_input();

    // Sanity check to prevent total crash
    if (!input || strlen(input) > 1024) {
        uart_puts("Input too large - truncating\n");
        input[1024] = '\0';
    }

    // Still vulnerable but won't crash QEMU
    strcpy(buffer, input);
}

Security Considerations

Flag Generation Security

Proper Flag Generation

// Use the SDK's secure flag generation
#include <embsec/flag.h>

// DON'T hardcode flags
// BAD:
const char *flag = "embsec{1234567890abcdef}";

// GOOD: Generate based on salt
void reveal_flag(void) {
    generate_flag();  // Uses LAB_SALT from compile time
    uart_printf("Flag: %s\n", embsec_flag);
}

// Ensure flag is only accessible through exploit
void init_flag_protection(void) {
    // Clear flag memory
    memset(embsec_flag, 0, sizeof(embsec_flag));

    // Only generate when needed
    g_flag_generated = false;
}

Salt Management

# CMakeLists.txt - Keep salts unique and secret
embsec_add_lab(
    NAME my-lab
    SALT "${MY_LAB_SECRET_SALT_2024}"  # Use environment variable
)
# .env file (git ignored)
export MY_LAB_SECRET_SALT_2024="super_secret_unique_value_xyz123"

Preventing Unintended Solutions

Validate Exploitation Path

// Track how the student reached the flag
typedef enum {
    PATH_NONE = 0,
    PATH_BUFFER_OVERFLOW = 1,
    PATH_FORMAT_STRING = 2,
    PATH_LOGIC_BUG = 4
} exploit_path_t;

static exploit_path_t g_exploit_path = PATH_NONE;

void win_function(void) {
    // Log how we got here
    if (g_exploit_path == PATH_NONE) {
        uart_puts("Warning: Reached win function through unintended path\n");
    }

    generate_flag();
    uart_printf("Flag: %s\n", embsec_flag);

    // Log for analysis
    uart_printf("Exploit path: 0x%x\n", g_exploit_path);
}

// Mark exploitation points
void vulnerable_function(void) {
    char buffer[32];
    g_exploit_path |= PATH_BUFFER_OVERFLOW;
    uart_gets(buffer);  // Overflow here
}

Input Validation

// Prevent simple bypasses
void check_input_validity(const char *input) {
    // Disallow shell metacharacters if not needed
    const char *forbidden = ";&|`$";

    for (int i = 0; forbidden[i]; i++) {
        if (strchr(input, forbidden[i])) {
            uart_puts("Invalid character in input\n");
            return;
        }
    }

    process_input(input);
}

Documentation Standards

Code Documentation

Function Headers

/**
 * @brief Process user authentication request
 * 
 * This function handles the authentication flow, including
 * password verification and privilege escalation. Contains
 * an intentional buffer overflow vulnerability in the username
 * field for educational purposes.
 * 
 * @param username User-provided username (UNSAFE - no bounds check)
 * @param password User-provided password
 * @return true if authentication successful, false otherwise
 * 
 * @note VULNERABLE: Username buffer can be overflowed
 * @note Target address for exploitation: &grant_admin_access
 */
bool authenticate_user(const char *username, const char *password) {
    char username_buffer[32];  // Vulnerable buffer

    // Intentional vulnerability for teaching
    strcpy(username_buffer, username);

    return check_password(username_buffer, password);
}

Inline Documentation

void complex_vulnerability(void) {
    // Step 1: Leak the stack canary value
    uint32_t canary = *(uint32_t *)get_canary_address();

    // Step 2: Build payload preserving canary
    struct {
        char overflow[64];
        uint32_t saved_canary;
        uint32_t saved_fp;
        uint32_t saved_lr;
    } payload;

    // Step 3: Overflow with canary preservation
    memset(payload.overflow, 'A', sizeof(payload.overflow));
    payload.saved_canary = canary;  // Preserve canary
    payload.saved_fp = 0x41414141;   // Dummy frame pointer
    payload.saved_lr = (uint32_t)win_function | 1;  // Target (Thumb)

    // Trigger vulnerability
    vulnerable_memcpy(&payload, sizeof(payload));
}

Student-Facing Documentation

Clear README Structure

# Lab X: Vulnerability Type

## Overview
Brief description of what students will learn.

## Learning Objectives
By completing this lab, you will:

- Understand [specific vulnerability]
- Learn how to [exploitation technique]
- Practice [debugging skill]

## Background
### Technical Concept
Explanation of the vulnerability type...

### ARM-Specific Details
Important ARM architecture notes...

## Setup Instructions

1. Build the lab
2. Run in QEMU
3. Connect debugger (optional)

## The Challenge
Your task is to...

## Hints (Progressive Disclosure)
<details>
<summary>Hint 1 (No points deducted)</summary>
General guidance about approach
</details>

<details>
<summary>Hint 2 (-25 points)</summary>
More specific direction
</details>

<details>
<summary>Hint 3 (-50 points)</summary>
Key insight needed
</details>

## Tools and Resources

- ARM GDB
- Python for exploit development
- Hex dump utilities

## Submission
Submit your exploit script that retrieves the flag.

Instructor Documentation

INSTRUCTOR.md Template

# Lab X: Quick Reference

## Vulnerability Details

- **Type**: Buffer overflow in `process_input()` line 45
- **Buffer size**: 32 bytes
- **Overflow target**: Return address at offset 64
- **Target function**: `win_function` at 0x00001234

## Key Addresses (Fixed)

- Win function: 0x00001234 (remember Thumb bit: 0x00001235)
- Debug function: 0x00001300
- String table: 0x00002000

## Common Student Mistakes

1. **Forgetting Thumb bit**: ARM functions need LSB=1
2. **Wrong endianness**: Use little-endian packing
3. **Missing newline**: Input processing expects '\n'

## Testing Commands
```bash
# Quick exploit test
echo -e 'AAAA....\x35\x12\x00\x00' | ./run_lab.sh

# Full exploit
python3 solution/exploit.py | ./run_lab.sh

# Debug session
./debug_lab.sh -x lab-name

Debugging Strategies

Built-in Debugging Features

Debug Menu Implementation

void show_debug_menu(void) {
    uart_puts("\n=== Debug Menu ===\n");
    uart_puts("1. Show memory map\n");
    uart_puts("2. Dump stack\n");
    uart_puts("3. Show registers\n");
    uart_puts("4. Toggle verbose mode\n");
    uart_puts("5. Return to main\n");
}

void debug_command_handler(char cmd) {
    switch (cmd) {
        case '1':
            show_memory_map();
            break;
        case '2':
            dump_stack(32);  // 32 words
            break;
        case '3':
            dump_registers();
            break;
        case '4':
            g_verbose_mode = !g_verbose_mode;
            uart_printf("Verbose mode: %s\n", 
                       g_verbose_mode ? "ON" : "OFF");
            break;
    }
}

Memory Inspection Helpers

void examine_memory(uint32_t addr, size_t words) {
    uart_printf("\nMemory dump from 0x%08x:\n", addr);

    for (size_t i = 0; i < words; i += 4) {
        uart_printf("%08x: ", addr + i*4);

        for (size_t j = 0; j < 4 && i+j < words; j++) {
            uint32_t *ptr = (uint32_t *)(addr + (i+j)*4);
            uart_printf("%08x ", *ptr);
        }

        uart_puts("  ");

        // ASCII representation
        for (size_t j = 0; j < 16 && i*4+j < words*4; j++) {
            uint8_t byte = *(uint8_t *)(addr + i*4 + j);
            uart_putc(isprint(byte) ? byte : '.');
        }

        uart_puts("\n");
    }
}

Logging and Tracing

Execution Tracing

#ifdef ENABLE_TRACE
#define TRACE(fmt, ...) \
    uart_printf("[TRACE] %s:%d: " fmt "\n", __func__, __LINE__, ##__VA_ARGS__)
#else
#define TRACE(fmt, ...) ((void)0)
#endif

void vulnerable_function(void) {
    char buffer[32];

    TRACE("Entering with SP=0x%08x", get_sp());
    TRACE("Buffer at 0x%08x", (uint32_t)buffer);

    uart_gets(buffer);

    TRACE("Input length: %d", strlen(buffer));
    TRACE("Returning to 0x%08x", get_return_address());
}

State Logging

typedef struct {
    uint32_t timestamp;
    const char *event;
    uint32_t data;
} log_entry_t;

static log_entry_t g_event_log[MAX_LOG_ENTRIES];
static int g_log_index = 0;

void log_event(const char *event, uint32_t data) {
    g_event_log[g_log_index].timestamp = get_system_ticks();
    g_event_log[g_log_index].event = event;
    g_event_log[g_log_index].data = data;
    g_log_index = (g_log_index + 1) % MAX_LOG_ENTRIES;
}

void dump_event_log(void) {
    uart_puts("\n=== Event Log ===\n");
    for (int i = 0; i < MAX_LOG_ENTRIES; i++) {
        int idx = (g_log_index + i) % MAX_LOG_ENTRIES;
        if (g_event_log[idx].event) {
            uart_printf("[%08d] %s: 0x%08x\n",
                       g_event_log[idx].timestamp,
                       g_event_log[idx].event,
                       g_event_log[idx].data);
        }
    }
}

Performance Optimization

Memory Efficiency

Minimize RAM Usage

// Use const for read-only data (stored in flash)
const char * const menu_options[] = {
    "1. Login",
    "2. Debug Info",
    "3. Exit"
};

// Pack structures
typedef struct __attribute__((packed)) {
    uint8_t type;
    uint8_t flags;
    uint16_t length;
    uint32_t data;
} message_t;

// Use bitfields for flags
typedef struct {
    uint32_t is_authenticated : 1;
    uint32_t is_admin : 1;
    uint32_t debug_enabled : 1;
    uint32_t reserved : 29;
} status_flags_t;

Optimize String Usage

// Reuse format strings
static const char fmt_hex[] = "0x%08x";
static const char fmt_addr[] = "Address: 0x%08x\n";

// Use string tables
const char * const error_messages[] = {
    [ERR_NONE] = "Success",
    [ERR_AUTH] = "Authentication failed",
    [ERR_OVERFLOW] = "Buffer overflow detected",
    [ERR_INVALID] = "Invalid input"
};

Code Size Optimization

Combine Similar Functions

// Instead of multiple similar functions
void print_buffer_address(void) {
    uart_printf("Buffer: 0x%08x\n", (uint32_t)buffer);
}

void print_stack_address(void) {
    uart_printf("Stack: 0x%08x\n", get_sp());
}

// Use a generic function
void print_labeled_address(const char *label, uint32_t addr) {
    uart_printf("%s: 0x%08x\n", label, addr);
}

Testing Checklist

Pre-Release Validation

Functionality Tests

  • Lab compiles without warnings
  • Runs in QEMU without crashes
  • Normal path works as expected
  • Vulnerability is exploitable
  • Flag generation works correctly
  • All menu options function

Security Tests

  • Flag not accessible without exploit
  • No unintended solutions exist
  • Input validation prevents crashes
  • Memory bounds are respected
  • Salt is unique across labs

Educational Tests

  • Learning objectives are met
  • Difficulty is appropriate
  • Hints provide good guidance
  • Debug features work correctly
  • Documentation is clear

Technical Tests

  • Works on Linux/Mac/Windows
  • Automated tests pass
  • Memory usage within limits
  • Performance is acceptable
  • No hardcoded addresses

Continuous Improvement

Collect Metrics

// Track how students solve the lab
typedef struct {
    uint32_t attempts;
    uint32_t hints_used;
    uint32_t time_to_solve;
    exploit_path_t path_taken;
} student_metrics_t;

void log_solution_metrics(void) {
    uart_printf("\n=== Solution Metrics ===\n");
    uart_printf("Attempts: %d\n", g_metrics.attempts);
    uart_printf("Hints used: %d\n", g_metrics.hints_used);
    uart_printf("Time: %d seconds\n", g_metrics.time_to_solve);
    uart_printf("Path: %s\n", get_path_name(g_metrics.path_taken));
}

Version Control

// Track lab version in binary
const char * const LAB_VERSION = "1.2.3";
const char * const LAB_BUILD_DATE = __DATE__ " " __TIME__;

void show_version_info(void) {
    uart_printf("Lab Version: %s\n", LAB_VERSION);
    uart_printf("Built: %s\n", LAB_BUILD_DATE);
    uart_printf("SDK Version: %s\n", EMBSEC_SDK_VERSION);
}

Summary

Following these best practices ensures your labs are:

  • Educational: Clear learning path with appropriate difficulty
  • Reliable: Consistent behavior across platforms
  • Maintainable: Clean code that's easy to update
  • Secure: Proper flag generation and protection
  • Debuggable: Built-in tools for troubleshooting

Remember: The goal is student learning, not frustration. Make vulnerabilities discoverable, provide good debugging tools, and ensure exploits work reliably.