第3章:NuttX文件系统安全:文件权限模型、安全挂载选项、加密文件系统支持
文件系统安全,说白了就是解决两个问题:谁可以访问?数据是否保密?在嵌入式系统里,这俩问题往往被忽视。我见过不少产品,Flash一读,配置文件全裸奔,密钥直接明文存储。嗯,今天我们就来聊聊NuttX在这方面的设计。
3.1 文件权限模型:麻雀虽小五脏俱全
NuttX的权限模型借鉴了Unix,但做了精简。毕竟嵌入式系统资源有限,没必要搞得太复杂。我个人习惯把权限分为三级:用户、组、其他。每个级别有读、写、执行三个位。
核心数据结构:每个文件节点(inode)都包含一个 mode_t 类型的权限字段。
struct inode {
FAR struct inode_ops_s *u.i_ops; // 文件操作接口
mode_t i_mode; // 文件权限
FAR void *i_private; // 私有数据
...
};
权限位定义如下:
| 宏定义 | 值 | 含义 |
|---|---|---|
| S_IRUSR | 00400 | 用户读 |
| S_IWUSR | 00200 | 用户写 |
| S_IXUSR | 00100 | 用户执行 |
| S_IRGRP | 00040 | 组读 |
| S_IWGRP | 00020 | 组写 |
| S_IXGRP | 00010 | 组执行 |
| S_IROTH | 00004 | 其他读 |
| S_IWOTH | 00002 | 其他写 |
| S_IXOTH | 00001 | 其他执行 |
你可能会问:NuttX不是单用户系统吗?搞用户组权限有意义吗?其实有。虽然只有一个root用户,但通过任务分组(group ID),可以实现任务级别的权限隔离。我在项目中就遇到过这种情况:两个传感器任务,一个只能读配置,另一个可以写配置。通过组权限,轻松搞定。
避坑指南:我曾经在调试时发现,明明设置了权限,但文件还是能被任意读写。后来排查发现,NuttX默认编译时没开文件权限检查。需要在配置中使能:
CONFIG_FILE_MODE=y
CONFIG_SCHED_USER_IDENTITY=y
没有这两项,权限位就是个摆设。
3.2 安全挂载选项:别让攻击者钻空子
挂载文件系统时,很多人只关心能不能用,不关心安不安全。其实挂载选项里藏着不少安全门道。
NuttX支持多种文件系统:FAT、ROMFS、PROCFS、NFS等。每种都有对应的挂载选项。我重点说几个跟安全相关的:
- MS_RDONLY:只读挂载。对于存放固件、证书、公钥的分区,强烈建议只读。攻击者就算拿到shell,也改不了。
- MS_NOSUID:禁止setuid位。嵌入式里用得少,但如果你跑着第三方应用,这个选项能防止提权。
- MS_NOEXEC:禁止执行。数据分区挂载时加上这个,防止攻击者上传恶意程序。
// 安全挂载示例:只读 + 禁止执行
int ret = mount("/dev/mtdblock0", "/config", "fatfs",
MS_RDONLY | MS_NOEXEC, NULL);
if (ret < 0) {
_err("安全挂载失败: %d\n", errno);
}
你想想看,如果所有数据分区都加了MS_NOEXEC,缓冲区溢出攻击的威力会大打折扣。攻击者就算写入了shellcode,也没法执行。
注意:MS_RDONLY只对文件系统层有效。如果底层存储设备(如SPI Flash)本身可写,攻击者仍然可以通过直接操作设备节点来绕过。所以,硬件写保护引脚才是终极方案。
3.3 加密文件系统支持:数据安全的最后防线
文件权限和挂载选项防的是软件层面的攻击。但如果设备丢失,攻击者直接拆Flash读数据呢?这时候就需要加密文件系统了。
NuttX本身没有内置像Linux的ext4加密或eCryptfs那样的完整加密文件系统。但别急,我们可以通过两种方式实现:
3.3.1 块设备加密层
在NuttX中,可以在块设备和文件系统之间加一层加密。说白了,就是数据写入块设备前加密,读出后解密。对上层文件系统完全透明。
// 伪代码:加密块设备驱动
static ssize_t enc_read(FAR struct inode *blkdev,
FAR unsigned char *buffer,
blkcnt_t startsector,
unsigned int nsectors) {
// 1. 调用底层驱动读取密文
ssize_t ret = raw_read(blkdev, cipher_buf, startsector, nsectors);
// 2. 解密
decrypt(cipher_buf, buffer, nsectors * SECTOR_SIZE);
return ret;
}
static ssize_t enc_write(FAR struct inode *blkdev,
FAR const unsigned char *buffer,
blkcnt_t startsector,
unsigned int nsectors) {
// 1. 加密
encrypt(buffer, cipher_buf, nsectors * SECTOR_SIZE);
// 2. 调用底层驱动写入密文
return raw_write(blkdev, cipher_buf, startsector, nsectors);
}
我个人建议使用XTS-AES模式。为什么?因为XTS模式专门为磁盘加密设计,每个扇区独立加密,不会因为一个扇区损坏影响其他扇区。我在一个IoT网关项目里用过这种方案,效果不错。
3.3.2 应用层加密
如果不想动底层驱动,也可以在应用层做加密。比如,用mbedTLS或OpenSSL的API,对关键配置文件进行加密存储。
#include <mbedtls/aes.h>
// 加密配置文件
int encrypt_config(const char *plaintext, size_t len,
const unsigned char *key,
unsigned char *ciphertext) {
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, key, 256);
// 注意:实际使用需要处理IV和填充
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT,
plaintext, ciphertext);
mbedtls_aes_free(&aes);
return OK;
}
密钥管理建议:密钥不能硬编码在代码里。我见过太多产品,密钥就写在源码里,反编译一下全暴露。建议的做法是:
- 使用芯片内置的OTP(一次性可编程)区域存储密钥
- 或者从硬件唯一ID(如MCU的UID)派生密钥
- 高级方案:使用SE(安全元件)或TEE(可信执行环境)管理密钥
3.4 实战:构建一个安全文件系统
说了这么多理论,我们来个实际配置。假设我们要做一个安全网关,需要三个分区:
| 分区 | 文件系统 | 挂载点 | 挂载选项 | 加密 |
|---|---|---|---|---|
| Boot/Firmware | ROMFS | /boot | MS_RDONLY | 不需要(只读) |
| 配置数据 | FAT | /config | MS_RDONLY | MS_NOEXEC | 块设备加密(XTS-AES) |
| 日志数据 | FAT | /log | MS_NOEXEC | 不需要(日志可读) |
// 安全启动时的挂载流程
void secure_mount_all(void) {
// 1. 挂载固件分区(只读)
mount("/dev/mtdblock0", "/boot", "romfs", MS_RDONLY, NULL);
// 2. 初始化加密块设备
struct enc_dev_s *enc = enc_init("/dev/mtdblock1",
key_from_otp());
register_blockdriver("/dev/enc_config", enc, 0666, NULL);
// 3. 挂载加密配置分区
mount("/dev/enc_config", "/config", "fatfs",
MS_RDONLY | MS_NOEXEC, NULL);
// 4. 挂载日志分区(可写,但不可执行)
mount("/dev/mtdblock2", "/log", "fatfs", MS_NOEXEC, NULL);
}
这个方案我实际部署过。攻击者拿到设备后,即使拆了Flash,也只能读到加密的配置数据。固件是ROMFS只读的,改不了。日志虽然明文,但里面没有敏感信息。三层防护,各司其职。
最后提醒:安全是一个系统工程。文件系统安全只是其中一环。别忘了配合任务权限、内存保护单元(MPU)、安全启动等机制,才能构建真正的可信执行环境。单点防御,永远是不够的。