Linux mmap安全问题及应对策略

导读mmap(内存映射)是 Linux 下高效 I/O 与进程间通信的重要工具,但如果使用不当或忽视安全细节,可能带来严重的安全风险,包括权限提升、信息泄露、代码注入等。本文将深入剖析 Linux 下与 mmap 相关的典型安全问题,并给出实战级应对策略。文中配有代码示例ASCII 图解详细说明,帮助你快速理解并掌握安全使用 mmap 的最佳实践。

目录

  1. 背景:为什么关注 mmap 的安全问题
  2. mmap 安全风险概览

    • 2.1 权限提升漏洞(Privilege Escalation)
    • 2.2 信息泄漏(Information Disclosure)
    • 2.3 代码注入与执行(Code Injection & Execution)
    • 2.4 竞态条件与 TOCTOU(Time-Of-Check to Time-Of-Use)漏洞
    • 2.5 旁路攻击与内核态攻击(Side-Channel & Kernel Exploits)
  3. 常见漏洞示例与剖析

    • 3.1 匿名映射与未初始化内存读取
    • 3.2 MAP\_FIXED 误用导致任意地址覆盖
    • 3.3 文件映射中 TOCTOU 漏洞示例
    • 3.4 共享映射(MAP\_SHARED)导致的数据竞争与向下权限写入
    • 3.5 只读映射到可写段的保护绕过示例
  4. 安全使用 mmap 的最佳实践

    • 4.1 严格控制权限与标志:PROT\_* 与 MAP\_*
    • 4.2 避免 MAP\_FIXED,优先使用非强制地址映射
    • 4.3 使用 mlock / mlockall 防止页面被换出敏感数据
    • 4.4 使用 MADV\_DONTFORK / MADV\_NOHUGEPAGE 避免子进程继承敏感映射
    • 4.5 及时解除映射与使用 msync 保证数据一致性
  5. 防范 TOCTOU 与缓解竞态条件

    • 5.1 原子性地打开与映射:open+O\_CLOEXEC 与 fstat 一致性检查
    • 5.2 使用 trusted directory 与路径白名单来避免符号链接攻击
    • 5.3 对比文件 fd 与路径:确保映射目标不可被替换
  6. 用户空间与内核空间的安全隔离

    • 6.1 SELinux / AppArmor 策略限制 mmap 行为
    • 6.2 seccomp-BPF 限制 mmap 相关系统调用参数
    • 6.3 /proc/[pid]/maps 监控与审计
  7. 实战案例:修复一个 mmap 漏洞

    • 7.1 漏洞演示:TOCTOU 结合 MAP\_FIXED 的本地提权
    • 7.2 修复思路与安全加强代码
    • 7.3 验证与对比测试
  8. 总结

一、背景:为什么关注 mmap 的安全问题

Linux 下,mmap 系统调用允许进程将一个文件(或匿名内存)直接映射到自身的虚拟地址空间,替代传统的 read/write 方式,实现零拷贝 I/O、按需加载、进程间共享内存等高效操作。然而,正是这种直接操作底层内存映射的特性,一旦使用不当,就有可能打破用户态与内核态之间、不同权限域之间的安全隔离,留出可被利用的攻击面

  • 权限提升:恶意进程或非特权用户通过精心构造的 mmap 参数或竞态条件,获得对根目录、系统库、SetUID 可执行文件等重要区域的写访问或执行能力。
  • 信息泄露:未经初始化的匿名映射或跨用户/跨进程的共享映射,可能泄露内存中的敏感数据(如口令、密钥、私有 API、其他进程遗留的内存内容)。
  • 代码注入与执行:在只读段或库段意外映射成可写可执行后,攻击者可以注入 shellcode 并跳转执行。
  • 竞态条件(TOCTOU):在打开文件到 mmap 映射之间,如果目标文件或路径被替换,就可能导致将恶意文件映射到安全路径下,造成提权或数据劫持。
  • 旁路与内核攻击:虽然不直接由 mmap 引起,但通过内存映射可以实现对 Page Cache、TLB、Side-Channel 状态的分析,间接开启对内核态或其他进程数据的攻击。

因此,在设计与审计 Linux 应用时,务必将 mmap安全性放在与性能并重的位置,既要发挥其高效特性,也要杜绝潜在风险。本文将深入揭示常见的 mmap 安全问题,并给出详实的应对策略


二、mmap 安全风险概览

以下是与 mmap 相关的主要安全风险分类,并在后文中逐一展开深入剖析及代码示例。

2.1 权限提升漏洞(Privilege Escalation)

  • 利用 SetUID 可执行文件的映射:攻击者将 SetUID 二进制可执行文件(如 /usr/bin/passwd)通过 mmap 映射为可写区,再修改局部数据或跳转表,从而在内存中注入提权代码。
  • 匿名映射覆盖关键结构:利用 MAP_FIXED 将关键系统内存页(如 GOT、PLT、glibc 数据段)映射到可写空间,修改函数指针或全局变量,实现Root 权限操作。

2.2 信息泄漏(Information Disclosure)

  • 匿名映射后未经初始化的读取:由于 Linux mmapMAP_ANONYMOUS 区域会分配零页,而快速访问可能会暴露先前未被清零的物理页,尤其在内存重用场景下,会读取到其他进程遗留的数据。
  • 共享映射(MAP\_SHARED):多个进程映射同一文件,若未充分验证文件读写权限,被映射进程 A 的敏感数据(如配置文件内容、用户口令)可能被进程 B 读取。

2.3 代码注入与执行(Code Injection & Execution)

  • 绕过 DEP / NX:若将只读段(如 .text 段)误映射成可写可执行(PROT_READ | PROT_WRITE | PROT_EXEC),攻击者可以直接写入并执行恶意代码。
  • 利用 mprotect 提升权限:在某些缺陷中,进程对映射区本只需可读可写,误调用 mprotect 更改为可执行后,一旦控制了写入逻辑,就能完成自内存中跳转执行。

2.4 竞态条件与 TOCTOU(Time-Of-Check to Time-Of-Use)漏洞

  • 打开文件到 mmap 之间的时间窗口:若程序先 stat 或检查权限再 open,攻击者在两者之间替换目标文件或符号链接,就会导致映射到恶意文件。
  • Fork + mmap:父子进程未正确隔离 mmap 区域导致子进程恶意修改共享映射,影响父进程的安全逻辑,产生竞态风险。

2.5 旁路攻击与内核态攻击(Side-Channel & Kernel Exploits)

  • Page Cache 侧信道:攻击者通过访问映射区的缺页行为、测量访问延迟,可以推测其他进程的缓存使用情况,间接泄露信息。
  • 内核溢出与指针篡改:若用户进程能映射到内核的 /dev/mem/dev/kmem 或者不正确使用 CAP_SYS_RAWIO 权限,就可能读取甚至修改内核内存,造成更高级别的系统妥协。

三、常见漏洞示例与剖析

下面以简化代码示例演示典型 mmap 安全漏洞,并配以ASCII 图解帮助理解漏洞原理。

3.1 匿名映射与未初始化内存读取

漏洞示例

某程序想快速分配一段临时缓冲区,使用 MAP_ANONYMOUS,但忘记对内容进行初始化,进而读取了一段“看似随机”的数据——可能暴露物理内存重用前的旧数据。

// uninitialized_mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
    size_t len = 4096; // 一页
    // 匿名映射,申请可读可写
    char *buf = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    if (buf == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    // 忘记初始化,直接读取
    printf("buf[0] = 0x%02x\n", buf[0]);
    // ...
    munmap(buf, len);
    return 0;
}
  • 预期:匿名映射会分配清零页,应输出 0x00
  • 实际风险:如果系统内存页因快速重用而未真正清零(某些旧内核版本或特定配置下),buf[0] 可能为其他进程使用过的数据片段,造成信息泄漏

漏洞剖析

  1. mmap 创建 VMA,但物理页可能从空闲页池中分配
  2. 如果系统未强制清零(例如在启用了大页、性能优化模式下),内核可能直接分配已被释放但尚未清零的物理页。
  3. 用户进程读取时就会看到旧数据。

攻击场景

  • 恶意程序希望窥探敏感数据(如内核内存、其他进程的隐私信息)。
  • 在高并发应用中,很容易在 mmap毫无意识 地读取未初始化缓冲区,导致数据外泄。

3.2 MAP\_FIXED 误用导致任意地址覆盖

漏洞示例

某程序错误地使用 MAP_FIXED 将映射地址硬编码,导致覆盖了堆区或全局数据区,使得攻击者可以调整映射位置,写入任意内存。

// fixed_mmap_override.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("data.bin", O_RDWR | O_CREAT, 0644);
    ftruncate(fd, 4096);
    // 直接将文件映射到 0x400000 地址(示例值),可能与程序代码段或全局区重叠
    void *addr = (void *)0x400000;
    char *map = mmap(addr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
    if (map == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    // 写入映射区
    strcpy(map, "Injected!");
    printf("写入完成\n");
    munmap(map, 4096);
    close(fd);
    return 0;
}
  • 预期:将 data.bin 的前 4KB 映射到 0x400000。
  • 风险:如果 0x400000 正好是程序的 .text 段或全局变量区,MAP_FIXED 会强制覆盖已有映射(页表条目),导致程序代码或关键数据区被替换为文件内容,攻击者可借此注入恶意代码或修改变量。

漏洞剖析

  1. MAP_FIXED 告诉内核“无视现有映射,直接将虚拟地址 0x400000 – 0x400FFF 重新映射到文件”。
  2. 如果该地址正被程序或动态链接库使用,原有映射立即失效,不同于 mmap(NULL, ...),后者由内核选取不会覆盖已有区域。
  3. 恶意构造的 data.bin 可以包含 shellcode、变量偏移值等,一旦写入并 mprotect 可写可执行,就可直接执行恶意代码。

ASCII 图解

原始进程地址空间:
  ┌─────────────────────────────┐
  │ 0x00400000 ──┐             │
  │               │  .text 段  │
  │               └─────────────┤
  │   ……                        │
  │ 0x00600000 ──┐             │
  │               │  .data 段  │
  │               └─────────────┤
  └─────────────────────────────┘

执行 mmap(MAP_FIXED, addr=0x00400000):
  ┌─────────────────────────────┐
  │ 0x00400000 ──┐  自定义文件映射  │
  │               └─────────────┤
  │   ……                        │
  │                           … │
  └─────────────────────────────┘
原有 .text 段被映射区覆盖 → 程序控制流可被劫持

3.3 文件映射中 TOCTOU 漏洞示例

漏洞示例

程序先检查文件属性再映射,攻击者在两者之间替换文件或符号链接,导致 mmap 到恶意文件。

// toctou_mmap_vuln.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <path>\n", argv[0]);
        return 1;
    }
    const char *path = argv[1];
    struct stat st;

    // 第一次检查
    if (stat(path, &st) < 0) {
        perror("stat");
        return 1;
    }
    if (!(st.st_mode & S_IRUSR)) {
        fprintf(stderr, "文件不可读\n");
        return 1;
    }

    // 攻击者此时替换该路径为恶意文件

    // 重新打开并映射
    int fd = open(path, O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    size_t size = st.st_size;
    void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    // 读取映射内容
    write(STDOUT_FILENO, map, size);
    munmap(map, size);
    close(fd);
    return 0;
}
  • 预期:映射指定文件并输出内容。
  • 风险:攻击者在 statopen 之间,将路径改为指向 /etc/shadow 或包含敏感数据的文件,程序仍会根据第一次 stat 的大小信息调用 mmap,导致将敏感内容映射并输出。

漏洞剖析

  1. TOCTOU(Time-Of-Check to Time-Of-Use):在 stat 检查阶段和 open + mmap 使用阶段之间,文件或符号链接被替换。
  2. 程序仍使用第一次 statsize 信息,即使实际文件已改变,mmap 会成功映射并读取恶意内容。

漏洞利用流程图

┌───────────┐    stat("file")    ┌───────────────┐
│  用户检查  │ ───────────────▶ │  获取 size = N  │
└───────────┘                   └───────────────┘
                                      │
            ◀─ 替换 file 指向恶意文件 ─▶
                                      │
┌──────────┐    open("file")       ┌───────────┐
│  映射阶段  │ ─────────────▶     │  打开恶意文件 │
└──────────┘                      └───────────┘
                                      │
                                mmap(size = N)  ─▶ 映射恶意内容

3.4 共享映射(MAP\_SHARED)导致的数据竞争与向下权限写入

漏洞示例

两个不同用户身份的线程或进程同时 mmap 同一个可写后端文件(如 /tmp/shared.bin),其中一个用户利用映射写入,而另一个用户也能看到并写入,打破了原本的文件权限限制。

// shared_mmap_conflict.c (线程 A)
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

char *shared_mem;

void *threadA(void *arg) {
    // 将 "SecretA" 写入共享映射
    sleep(1);
    strcpy(shared_mem, "SecretA");
    printf("线程A 写入: SecretA\n");
    return NULL;
}

int main() {
    int fd = open("/tmp/shared.bin", O_CREAT | O_RDWR, 0666);
    ftruncate(fd, 4096);
    shared_mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared_mem == MAP_FAILED) { perror("mmap"); exit(1); }

    pthread_t t;
    pthread_create(&t, NULL, threadA, NULL);

    // 线程 B 直接读取,并写入覆盖
    sleep(2);
    printf("线程B 读取: %s\n", shared_mem);
    strcpy(shared_mem, "SecretB");
    printf("线程B 写入: SecretB\n");

    pthread_join(t, NULL);
    munmap(shared_mem, 4096);
    close(fd);
    return 0;
}
  • 预期:文件由拥有同等权限的进程共享,写入互相可见。
  • 风险:若设计上不应让线程 B 覆盖线程 A 的数据,或者分离用户权限,MAP_SHARED 将文件缓冲区在多个用户/进程之间同步,可能导致数据竞争越权写入

漏洞剖析

  1. 线程 A、线程 B 使用 相同文件描述符,并以 MAP_SHARED 映射到相同物理页。
  2. 线程 B 不应有写入权限,却能通过映射绕过文件系统权限写入数据。
  3. 若文件原本只允许用户 A 访问,但进程 B 通过共享映射仍能获得写入通道,造成越权写入

3.5 只读映射到可写段的保护绕过示例

漏洞示例

程序先将一个只读文件段映射到内存,然后再通过 mprotect 错误地将其改为可写可执行,导致代码注入。

// ro_to_rw_mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 打开只读文件(假设包含合法的机器码)
    int fd = open("payload.bin", O_RDONLY);
    if (fd < 0) { perror("open"); exit(1); }
    size_t size = lseek(fd, 0, SEEK_END);

    // 先按只读映射
    void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED) { perror("mmap"); exit(1); }

    // 错误地将此内存区域改为可写可执行
    if (mprotect(map, size, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) {
        perror("mprotect");
        munmap(map, size);
        exit(1);
    }

    // 修改映射:注入恶意指令
    unsigned char shellcode[] = { 0x90, 0x90, 0xCC }; // NOP, NOP, int3
    memcpy(map, shellcode, sizeof(shellcode));

    // 跳转到映射区域执行
    ((void(*)())map)();
    munmap(map, size);
    close(fd);
    return 0;
}
  • 预期payload.bin 作为只读数据映射,不应被修改或执行。
  • 风险mprotect 将原本只读、不可执行的映射区域提升为可写可执行,攻击者可通过 memcpy 注入 shellcode,并跳转执行,绕过 DEP/NX 保护。

漏洞剖析

  1. 初始 mmap(..., PROT_READ, ...) 应只允许读权限,文件内容不可被修改。
  2. 但是调用 mprotect(map, size, PROT_READ | PROT_WRITE | PROT_EXEC) 直接将映射页设为可写可执行。
  3. 攻击者注入恶意指令并执行,造成任意代码执行。

四、安全使用 mmap 的最佳实践

针对上述典型漏洞,下面给出在生产环境中安全地使用 mmap 的若干实战建议与代码示例。

4.1 严格控制权限与标志:PROT\_* 与 MAP\_*

  1. 最小权限原则:只打开并映射所需权限,避免无谓的读写可执行组合:

    • 只需读取时,使用 PROT_READ + MAP_PRIVATE
    • 只需写入时,使用 PROT_WRITE + MAP_PRIVATE(或 MAP_SHARED),并避免设置 PROT_EXEC
    • 只需执行时,使用 PROT_READ | PROT_EXEC,不允许写。
  2. 杜绝 PROT\_READ | PROT\_WRITE | PROT\_EXEC

    • 绝大多数场景无需将映射区域同时设为读写执行,一旦出现,极易被滥用进行 JIT 注入或 shellcode 执行。
// 安全示例:读取配置文件,无写入与执行权限
int fd = open("config.json", O_RDONLY);
struct stat st; fstat(fd, &st);
void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); exit(1); }
// 只读使用
// ...
munmap(map, st.st_size);
close(fd);
  1. 慎用 MAP\_SHARED

    • 若映射的文件内容不需写回,可优先使用 MAP_PRIVATE,避免多进程/线程数据竞争。
    • 仅在真正需要“多进程共享修改”时,才使用 MAP_SHARED

4.2 避免 MAP\_FIXED,优先使用非强制地址映射

  1. 风险MAP_FIXED 会无条件覆盖已有映射,可能覆盖程序、库、堆栈等重要区域。
  2. 建议

    • 尽量使用 mmap(NULL, …, MAP_SHARED, fd, offset),由内核分配可用虚拟地址,避免冲突。
    • 若确有固定地址映射需求,务必先调用 munmap(addr, length) 或使用 MAP_FIXED_NOREPLACE(Linux 4.17+)检查是否可用:
// 安全示例:尽量避免 MAP_FIXED,如需强制映射先检查
void *desired = (void *)0x50000000;
void *ret = mmap(desired, length, PROT_READ | PROT_WRITE,
                 MAP_SHARED | MAP_FIXED_NOREPLACE, fd, 0);
if (ret == MAP_FAILED) {
    if (errno == EEXIST) {
        fprintf(stderr, "指定地址已被占用,映射失败\n");
    } else {
        perror("mmap");
    }
    exit(1);
}
// ...
  1. 总结:除非必须覆盖已有地址(且明确知晓风险并手动解除),否则不要使用 MAP_FIXED

4.3 使用 mlock / mlockall 防止页面被换出敏感数据

  1. 场景:若映射区域包含敏感数据(如密钥、密码、个人隐私),内核在换页时可能将此页写回交换空间(swap),导致磁盘可被读取、物理内存可被法医工具恢复。
  2. 做法

    • 通过 mlock() 将单页锁定在物理内存,或 mlockall() 锁定整个进程地址空间,以防止换出。
size_t len = 4096;
char *buf = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (buf == MAP_FAILED) { perror("mmap"); exit(1); }
// 锁定该页到物理内存
if (mlock(buf, len) < 0) {
    perror("mlock");
    munmap(buf, len);
    exit(1);
}
// 使用敏感数据
strcpy(buf, "TopSecretKey");
// 访问完成后解锁、取消映射
munlock(buf, len);
munmap(buf, len);
  1. 注意mlock 需要 CAP_IPC_LOCK 权限或足够的 ulimit -l 限制,否则会失败。若不能 mlock,可考虑定期用 memset 将敏感数据清零,降低泄露风险。

4.4 使用 MADV\_DONTFORK / MADV\_NOHUGEPAGE 避免子进程继承敏感映射

  1. 场景:父进程 fork() 后,子进程继承父的内存映射,包括敏感数据页。若子进程随后被更高权限用户读取,有信息泄漏风险。
  2. 做法

    • 对于敏感映射区域调用 madvise(..., MADV_DONTFORK),使得在 fork() 后子进程不继承该映射;
    • 对于不希望大页(2MB)参与映射的,调用 madvise(..., MADV_NOHUGEPAGE),避免页面拆分或合并导致权限混乱。
// 在父进程映射敏感区域后
madvise(sensitive_buf, len, MADV_DONTFORK);   // 子进程不继承
madvise(sensitive_buf, len, MADV_NOHUGEPAGE); // 禁用大页
  1. 注意MADV_DONTFORK 对 Linux 2.6.25+ 有效,低版本可能不支持;若必须在子进程中访问,可考虑先 fork,再单独映射。

4.5 及时解除映射与使用 msync 保证数据一致性

  1. 场景:对于 MAP_SHARED 映射,写入后需要保证数据已同步到磁盘,否则突然崩溃后会造成文件不一致甚至数据损坏。
  2. 做法

    • 在写入完成后,调用 msync(map, length, MS_SYNC) 强制同步该段脏页;
    • 在不再使用后及时 munmap(map, length) 释放映射,避免长期占用内存或权限泄露。
memcpy(map, data, data_len);
// 强制同步
if (msync(map, data_len, MS_SYNC) < 0) {
    perror("msync");
}
// 解除映射
if (munmap(map, data_len) < 0) {
    perror("munmap");
}
  1. 注意:过于频繁调用 msync 会严重影响性能;应按业务需求合理批量同步,避免在高并发场景中造成 I/O 瓶颈。

五、防范 TOCTOU 与缓解竞态条件

TOCTOU(Time-Of-Check to Time-Of-Use)是文件映射中常见的竞态漏洞。以下示例展示几种原子性地打开与映射以及路径白名单等技术,防止攻击者利用竞态条件。

5.1 原子性地打开与映射:open+O\_CLOEXEC 与 fstat 一致性检查

  1. 使用 open+O\_CLOEXEC

    • O_CLOEXEC 标志确保子进程继承时不会泄露文件描述符,避免恶意在子进程中替换目标文件。
  2. 直接通过 fd 获取文件大小,避免先 statopen 的 TOCTOU:

    • fstat(fd, &st) 代替 stat(path, &st),确保 fd 与路径保持一致。
const char *path = "/safe/config.cfg";
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) { perror("open"); exit(1); }

struct stat st;
if (fstat(fd, &st) < 0) { perror("fstat"); close(fd); exit(1); }

size_t size = st.st_size;
void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); close(fd); exit(1); }

// 使用映射
// …

munmap(map, size);
close(fd);
  • 解释:一旦 open 成功,fd 就对应了打开时刻的文件;再用 fstat(fd, &st) 获取文件大小,无论路径如何变更,都不会影响 fd 指向的文件。

5.2 使用 trusted directory 与路径白名单来避免符号链接攻击

  1. 限制应用只能从预先配置的可信目录加载文件,例如 /etc/myapp//usr/local/share/myapp/,避免用户可控路径。
  2. 检查路径前缀,禁止符号链接绕过:在 open 后再调用 fstat 查看文件的 st_devst_ino 是否在预期目录范围内。
#include <libgen.h>  // basename, dirname

bool is_under_trusted(const char *path) {
    // 简化示例:仅允许 /etc/myapp/ 下的文件
    const char *trusted_prefix = "/etc/myapp/";
    return strncmp(path, trusted_prefix, strlen(trusted_prefix)) == 0;
}

int secure_open(const char *path) {
    if (!is_under_trusted(path)) {
        fprintf(stderr, "不在可信目录内,拒绝访问: %s\n", path);
        return -1;
    }
    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0) return -1;
    // 可额外检查符号链接深度等
    return fd;
}

int main(int argc, char *argv[]) {
    if (argc < 2) { fprintf(stderr, "Usage: %s <path>\n", argv[0]); return 1; }
    const char *path = argv[1];
    int fd = secure_open(path);
    if (fd < 0) return 1;
    struct stat st; fstat(fd, &st);
    void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
    // 读取与处理
    munmap(map, st.st_size);
    close(fd);
    return 0;
}
  • 说明:仅在可信目录下的文件才允许映射,符号链接或其他路径将被拒绝。更严格可结合 realpath()frealpathat() 确保路径规范化后再比较。

5.3 对比文件 fd 与路径:确保映射目标不可被替换

为了更加保险,可在 open 之后调用 fstat,再与 stat(path) 做对比,确保路径和文件描述符指向的是相同的底层文件。

bool is_same_file(int fd, const char *path) {
    struct stat st_fd, st_path;
    if (fstat(fd, &st_fd) < 0) return false;
    if (stat(path, &st_path) < 0) return false;
    return (st_fd.st_dev == st_path.st_dev) && (st_fd.st_ino == st_path.st_ino);
}

int main(int argc, char *argv[]) {
    const char *path = argv[1];
    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0) { perror("open"); exit(1); }

    // 检查文件是否被替换
    if (!is_same_file(fd, path)) {
        fprintf(stderr, "TOCTOU 检测:路径与 fd 不匹配\n");
        close(fd);
        exit(1);
    }

    struct stat st; fstat(fd, &st);
    void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    // ...
    return 0;
}
  • 说明:在 open(path)fstat(fd)stat(path) 三步中间如果出现文件替换,st_inost_dev 会不一致,从而拒绝继续映射。

六、用户空间与内核空间的安全隔离

即使在用户层面做了上述优化,仍需借助内核安全机制(如 SELinux、AppArmor、seccomp)来加固 mmap 相关操作的访问控制

6.1 SELinux / AppArmor 策略限制 mmap 行为

  1. SELinux:可为进程定义布尔(Boolean)策略,禁止对某些文件进行映射。例如在 /etc/selinux/targeted/contexts/files/file_contexts 中指定 /etc/secret(/.*)? 只允许 read,禁止 mmap
/etc/secret(/.*)?    system_u:object_r:secret_data_t:s0
  1. AppArmor:通过 profile 限制应用只能对特定目录下的文件 r/w/m
/usr/bin/myapp {
  /etc/secret/** r,  # 只读
  /etc/secret/*.dat rm,  # 允许 mmap(m),但禁止写
  deny /etc/secret/* w,  # 禁止写
}
  • m 表示可对文件进行 mmap,r 表示可读。通过组合控制,需要谨慎授予 m 权限,仅在必要时启用。

6.2 seccomp-BPF 限制 mmap 相关系统调用参数

  1. 应用场景:在高安全环境(如容器、沙盒)中,使用 seccomp-BPF 对 mmapmprotect 等系统调用进行过滤,拒绝所有带有 PROT_EXEC 标志的请求,或者拒绝 MAP_SHAREDMAP_FIXED
  2. 示例:使用 libseccomp 定义规则,只允许带有 PROT_READ | PROT_WRITE 的映射,拒绝 PROT_EXEC
#include <seccomp.h>
#include <errno.h>
#include <stdio.h>

int setup_seccomp() {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
    if (!ctx) return -1;

    // 禁止所有带有 PROT_EXEC 的 mmap
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mmap), 1,
                     SCMP_A2(SCMP_CMP_MASKED_EQ, PROT_EXEC, PROT_EXEC));
    // 禁止 MAP_FIXED
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mmap), 1,
                     SCMP_A3(SCMP_CMP_MASKED_EQ, MAP_FIXED, MAP_FIXED));
    // 禁止 mprotect 将可执行权限加到任何地址
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mprotect), 1,
                     SCMP_A1(SCMP_CMP_MASKED_EQ, PROT_EXEC, PROT_EXEC));

    if (seccomp_load(ctx) < 0) { seccomp_release(ctx); return -1; }
    seccomp_release(ctx);
    return 0;
}

int main() {
    if (setup_seccomp() != 0) {
        fprintf(stderr, "seccomp 设置失败\n");
        return 1;
    }
    // 下面的 mmap 若尝试带 PROT_EXEC 或 MAP_FIXED,将被拒绝
    return 0;
}
  • 解释:上述规则为:

    • mmap 第 3 个参数(prot)里,如果 PROT_EXEC 位被设置,就拒绝调用;
    • 若调用 mmap 时指定了 MAP_FIXED 标志,也被拒绝;
    • mprotect 同理,禁止任何对映射区添加可执行权限。

6.3 /proc/[pid]/maps 监控与审计

  1. 实时监控映射:运维或安全审计人员可以定期 cat /proc/[pid]/maps,查看进程映射列表,识别是否存在可执行可写映射、MAP\_FIXED 等风险行为。
# 查看 pid=1234 进程映射情况
cat /proc/1234/maps

典型输出示例:

00400000-0040c000 r-xp 00000000 08:01 123456 /usr/bin/myapp
0060b000-0060c000 r--p 0000b000 08:01 123456 /usr/bin/myapp
0060c000-0060d000 rw-p 0000c000 08:01 123456 /usr/bin/myapp
00e33000-00e54000 rw-p 00000000 00:00 0      [heap]
7f7a40000000-7f7a40021000 rw-p 00000000 00:00 0 
7f7a40021000-7f7a40023000 r--p 00000000 08:01 654321 /usr/lib/libc.so.6
7f7a40023000-7f7a400f3000 r-xp 00002000 08:01 654321 /usr/lib/libc.so.6
7f7a400f3000-7f7a40103000 r--p 000e2000 08:01 654321 /usr/lib/libc.so.6
7f7a40103000-7f7a40104000 r--p 00102000 08:01 654321 /usr/lib/libc.so.6
7f7a40104000-7f7a40105000 rw-p 00103000 08:01 654321 /usr/lib/libc.so.6
...
7f7a40200000-7f7a40221000 rw-p 00000000 00:00 0      [anonymous:secure]
...
  • 审计重点

    • rw-p + x:可读可写可执行区域是高风险,应尽快定位并修复;
    • MAP_SHARED(通常在映射一个磁盘文件时可看到 s 标识);
    • 匿名映射中的敏感关键字(如 [heap][stack][anonymous:secure] 等),特别是它们的权限位(rwx)。
  1. 定期主动扫描与告警:安全运维可编写脚本监控特定关键进程的 /proc/[pid]/maps,一旦检测到带 EXECWRITE 的映射,立即告警或终止进程。

七、实战案例:修复一个 mmap 漏洞

7.1 漏洞演示:TOCTOU 结合 MAP\_FIXED 的本地提权

漏洞描述

目标程序 vulnapp/usr/local/bin/vulnapp 下为 SetUID Root 可执行文件。它会:

  1. /tmp/userid 文件中读取一个管理员的用户 ID,确保只有管理员可映射该文件。
  2. stat 检查后,将 /usr/local/bin/admin.dat 文件通过 mmap 映射到默认可写地址。
  3. 将文件内容读入并检测权限,判断是否为管理员。

漏洞逻辑示例:

// vulnapp.c (SetUID Root)
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *uidfile = "/tmp/userid";
    const char *admfile = "/usr/local/bin/admin.dat";
    struct stat st;
    // 检查 /tmp/userid 是否可读
    if (stat(uidfile, &st) < 0) { perror("stat uidfile"); exit(1); }
    if (!(st.st_mode & S_IRUSR)) {
        fprintf(stderr, "无权限\n"); exit(1);
    }
    // 读取 uid
    FILE *f = fopen(uidfile, "r");
    int uid = -1;
    fscanf(f, "%d", &uid);
    fclose(f);
    if (uid != 0) {
        fprintf(stderr, "非管理员\n"); exit(1);
    }
    // TOCTOU 漏洞点:此处攻击者可替换 admfile
    if (stat(admfile, &st) < 0) { perror("stat admfile"); exit(1); }
    int fd = open(admfile, O_RDWR);
    size_t size = st.st_size;
    // MAP_FIXED 将 admin.dat 映射到默认地址(覆盖 .text 段或 GOT)
    void *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
    if (map == MAP_FAILED) { perror("mmap"); exit(1); }
    // 检查映射内容
    char buffer[32];
    strncpy(buffer, (char *)map, 31);
    buffer[31] = '\0';
    if (strcmp(buffer, "I am admin") != 0) {
        fprintf(stderr, "文件校验失败\n"); exit(1);
    }
    // 以管理员身份执行敏感操作
    system("id");
    munmap(map, size);
    close(fd);
    return 0;
}
  1. 攻击者在 /tmp/userid 写入 0,通过管理员检查;
  2. stat(admfile)open(admfile) 之间,将 /usr/local/bin/admin.dat 替换成任意恶意文件(如包含 I am admin 字符串的 shell 脚本);
  3. mmap 将恶意文件映射到可写可执行地址,再通过覆盖 .text 或 GOT,执行提权。

7.2 修复思路与安全加强代码

  1. 使用 open + O\_CLOEXEC + fstat 替换 stat + open:避免 TOCTOU。
  2. 不使用 MAP\_FIXED,而采用非强制映射。
  3. 限制只读权限,不允许将 admin.dat 映射为可写。
  4. 添加 SELinux/AppArmor 策略,禁止非管理员用户修改 admin.dat。
// vulnapp_secure.c (SetUID Root)
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *uidfile = "/tmp/userid";
    const char *admfile = "/usr/local/bin/admin.dat";

    // 1. 原子打开
    int fd_uid = open(uidfile, O_RDONLY | O_CLOEXEC);
    if (fd_uid < 0) { perror("open uidfile"); exit(1); }
    struct stat st_uid;
    if (fstat(fd_uid, &st_uid) < 0) { perror("fstat uidfile"); close(fd_uid); exit(1); }
    if (!(st_uid.st_mode & S_IRUSR)) { fprintf(stderr, "无权限读取 userid\n"); close(fd_uid); exit(1); }

    // 2. 读取 UID
    FILE *f = fdopen(fd_uid, "r");
    if (!f) { perror("fdopen"); close(fd_uid); exit(1); }
    int uid = -1;
    fscanf(f, "%d", &uid);
    fclose(f);
    if (uid != 0) { fprintf(stderr, "非管理员\n"); exit(1); }

    // 3. 原子打开 admin.dat
    int fd_adm = open(admfile, O_RDONLY | O_CLOEXEC);
    if (fd_adm < 0) { perror("open admfile"); exit(1); }
    struct stat st_adm;
    if (fstat(fd_adm, &st_adm) < 0) { perror("fstat admfile"); close(fd_adm); exit(1); }

    // 4. 只读映射,无 MAP_FIXED
    size_t size = st_adm.st_size;
    void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd_adm, 0);
    if (map == MAP_FAILED) { perror("mmap"); close(fd_adm); exit(1); }

    // 5. 校验映射内容
    char buffer[32];
    strncpy(buffer, (char *)map, 31); buffer[31] = '\0';
    if (strcmp(buffer, "I am admin") != 0) {
        fprintf(stderr, "文件校验失败\n");
        munmap(map, size);
        close(fd_adm);
        exit(1);
    }
    // 6. 执行管理员操作
    system("id");

    munmap(map, size);
    close(fd_adm);
    return 0;
}

安全点说明

  • 使用 open(..., O_RDONLY | O_CLOEXEC) + fstat(fd, &st):在同一文件描述符上检查权限与大小,无 TOCTOU。
  • 不使用 MAP_FIXED:映射不会覆盖程序或库段,减少任意内存覆写风险。
  • PROT_READ + MAP_PRIVATE:只读私有映射,无法写入底层文件,也无法执行其中代码。
  • 添加操作系统强制策略(需在系统配置):

    • SELinux/AppArmor:确保非管理员用户无法替换 /usr/local/bin/admin.dat 文件。

7.3 验证与对比测试

  1. 原始漏洞版本

    gcc -o vulnapp vulnapp.c
    sudo chown root:root vulnapp
    sudo chmod u+s vulnapp
    • 普通用户替换 /usr/local/bin/admin.dat 为自制可执行内容,执行 ./vulnapp 即可提权。
  2. 修复版本

    gcc -o vulnapp_secure vulnapp_secure.c
    sudo chown root:root vulnapp_secure
    sudo chmod u+s vulnapp_secure
    • 由于 fstat + open 原子映射,以及 PROT_READ | MAP_PRIVATE,无论如何替换 admin.dat,映射后不可写、不可执行,且文件检查只能读取到预期内容,就算路径被替换,也会检测失败并退出。

八、总结

本文从权限提升、信息泄漏、代码注入、竞态条件、内核侧信道等多个角度,系统性地剖析了 Linux 下 mmap 的安全风险,并基于最小权限原则给出了详细的应对策略

  1. 严格控制 mmap 的权限标志,避免可写可执行的映射;
  2. 杜绝 MAP\_FIXED 的误用,优先让内核自动选择映射地址;
  3. 使用 mlock/madvise 等接口防止换页或子进程继承敏感内存;
  4. 原子性地打开与映射,通过 open + O_CLOEXEC + fstat 避免 TOCTOU;
  5. 结合操作系统安全机制(SELinux / AppArmor / seccomp-BPF),在内核层面进一步限制可疑 mmap 行为;
  6. 及时解除映射并合理使用 msync,确保数据一致性且减少映射生命周期内的攻击面。

通过文中的代码示例图解,你能更加直观地理解 mmap 在内核中的实现原理与漏洞原理,并在实际项目中落地安全加固

最后修改于:2025年06月03日 14:57

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日