《固件安全基线自查清单课程》
第2章 固件开发安全基线
2. 固件开发安全基线
2.1 安全编码规范
安全编码规范是固件安全的第一道防线。在资源受限的嵌入式环境中,开发者必须遵循严格的编码准则,以消除常见的安全漏洞。核心原则包括:最小权限、防御性编程、以及避免使用不安全的库函数。
安全编码规范检查项
| 检查项 | 要求 | 违规示例 |
|---|---|---|
| 缓冲区操作 | 使用安全函数(snprintf, strncpy) | strcpy(buf, input); |
| 整数处理 | 检查整数溢出/下溢 | if (a + b > MAX) 未检查 |
| 指针使用 | 使用前判空,释放后置NULL | free(p); free(p); |
| 格式化字符串 | 禁止将用户输入作为格式化参数 | printf(user_input); |
// 安全编码示例:安全字符串拷贝
void safe_copy(char *dest, size_t dest_size, const char *src) {
if (dest == NULL || src == NULL || dest_size == 0) {
return; // 防御性检查
}
size_t src_len = strlen(src);
if (src_len >= dest_size) {
// 截断并确保以null结尾
memcpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
} else {
memcpy(dest, src, src_len + 1);
}
}
2.2 输入验证
固件接收来自各种接口(UART、SPI、I2C、网络、传感器)的输入数据。所有外部输入必须被视为不可信数据,并经过严格的验证才能使用。验证策略包括:白名单、长度检查、范围检查、格式检查。
输入验证清单
- 长度验证:所有输入缓冲区必须检查长度,防止缓冲区溢出。
- 类型验证:确保输入数据符合预期的数据类型(整数、枚举、固定字符串)。
- 范围验证:数值型输入必须在预期的有效范围内(如温度传感器值 0-100°C)。
- 白名单策略:优先使用白名单(允许列表)而非黑名单(禁止列表)。
- 协议验证:对于通信协议,验证帧头、帧尾、校验和、序列号等字段。
// 输入验证示例:验证UART接收的指令
typedef enum { CMD_READ = 0x01, CMD_WRITE = 0x02, CMD_RESET = 0x03 } command_t;
bool validate_command(uint8_t *buffer, size_t len) {
// 1. 长度检查
if (len < 3 || len > 64) return false;
// 2. 帧头检查
if (buffer[0] != 0xAA || buffer[len-1] != 0x55) return false;
// 3. 命令白名单检查
command_t cmd = (command_t)buffer[1];
if (cmd != CMD_READ && cmd != CMD_WRITE && cmd != CMD_RESET) return false;
// 4. 校验和验证
uint8_t checksum = 0;
for (size_t i = 0; i < len-1; i++) checksum ^= buffer[i];
if (checksum != buffer[len-1]) return false;
return true;
}
2.3 输出编码
固件输出数据到显示设备、日志接口、网络连接等场景时,必须进行适当的编码,以防止注入攻击和信息泄露。输出编码确保数据在目标上下文中被正确解释,而不是被当作代码执行。
输出编码规则
| 输出上下文 | 编码方式 | 说明 |
|---|---|---|
| HTML页面 | HTML实体编码 | < → <,> → > |
| URL参数 | URL编码 | 空格 → %20,特殊字符百分号编码 |
| 日志文件 | 转义换行/控制字符 | 防止CRLF注入伪造日志条目 |
| 终端/串口 | 过滤ANSI转义序列 | 防止终端控制命令注入 |
| SQL查询 | 参数化查询/转义 | 防止SQL注入(如适用) |
// 输出编码示例:日志安全输出
void safe_log_output(const char *message) {
// 过滤/转义可能破坏日志格式的字符
for (const char *p = message; *p != '\0'; p++) {
switch (*p) {
case '\n':
uart_send("\\n");
break;
case '\r':
uart_send("\\r");
break;
case '%':
uart_send("%%"); // 防止格式化字符串攻击
break;
default:
uart_send_char(*p);
break;
}
}
uart_send("\r\n");
}
2.4 内存安全
内存安全是固件安全中最关键也最容易出问题的领域。嵌入式系统通常没有MMU,内存错误可能导致任意代码执行。必须严格管理内存的分配、使用和释放,并防范缓冲区溢出、释放后使用、双重释放等漏洞。
内存安全最佳实践
- 静态分配优先:在嵌入式系统中优先使用静态内存分配,避免动态内存带来的不确定性。
- 边界检查:所有数组和缓冲区操作必须进行边界检查,使用带长度参数的函数。
- 指针安全:释放内存后立即将指针置为NULL,防止悬空指针。
- 内存池:使用固定大小的内存池替代通用malloc/free,减少碎片和漏洞。
- 栈保护:启用编译器栈保护选项(如-fstack-protector-strong)。
- ASLR支持:如果硬件支持,启用地址空间布局随机化。
// 内存安全示例:安全的内存池分配器
#define POOL_SIZE 10
#define BLOCK_SIZE 64
typedef struct {
uint8_t data[BLOCK_SIZE];
bool in_use;
} memory_block_t;
static memory_block_t memory_pool[POOL_SIZE];
void *safe_malloc(void) {
for (int i = 0; i < POOL_SIZE; i++) {
if (!memory_pool[i].in_use) {
memory_pool[i].in_use = true;
return memory_pool[i].data;
}
}
return NULL; // 内存不足
}
void safe_free(void *ptr) {
if (ptr == NULL) return; // 防御性检查
for (int i = 0; i < POOL_SIZE; i++) {
if (memory_pool[i].data == ptr) {
// 清空数据防止信息泄露
memset(memory_pool[i].data, 0, BLOCK_SIZE);
memory_pool[i].in_use = false;
return;
}
}
// 指针不在池中,记录错误
log_error("Attempted to free invalid pointer");
}
本节要点总结
固件开发安全基线是构建安全嵌入式系统的基石。安全编码规范从源头减少漏洞;输入验证确保所有外部数据经过严格检查;输出编码防止数据被恶意解释;内存安全则是防止代码执行攻击的最后防线。这四方面相互补充,共同构成固件安全开发的完整防护体系。