第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)、安全启动等机制,才能构建真正的可信执行环境。单点防御,永远是不够的。