Linux mmap安全问题及应对策略
导读:mmap
(内存映射)是 Linux 下高效 I/O 与进程间通信的重要工具,但如果使用不当或忽视安全细节,可能带来严重的安全风险,包括权限提升、信息泄露、代码注入等。本文将深入剖析 Linux 下与mmap
相关的典型安全问题,并给出实战级应对策略。文中配有代码示例、ASCII 图解和详细说明,帮助你快速理解并掌握安全使用mmap
的最佳实践。
目录
- 背景:为什么关注 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.1 匿名映射与未初始化内存读取
- 3.2 MAP\_FIXED 误用导致任意地址覆盖
- 3.3 文件映射中 TOCTOU 漏洞示例
- 3.4 共享映射(MAP\_SHARED)导致的数据竞争与向下权限写入
- 3.5 只读映射到可写段的保护绕过示例
- 4.1 严格控制权限与标志:PROT\_* 与 MAP\_*
- 4.2 避免 MAP\_FIXED,优先使用非强制地址映射
- 4.3 使用 mlock / mlockall 防止页面被换出敏感数据
- 4.4 使用 MADV\_DONTFORK / MADV\_NOHUGEPAGE 避免子进程继承敏感映射
- 4.5 及时解除映射与使用 msync 保证数据一致性
- 5.1 原子性地打开与映射:open+O\_CLOEXEC 与 fstat 一致性检查
- 5.2 使用 trusted directory 与路径白名单来避免符号链接攻击
- 5.3 对比文件 fd 与路径:确保映射目标不可被替换
- 6.1 SELinux / AppArmor 策略限制 mmap 行为
- 6.2 seccomp-BPF 限制 mmap 相关系统调用参数
- 6.3 /proc/[pid]/maps 监控与审计
- 7.1 漏洞演示:TOCTOU 结合 MAP\_FIXED 的本地提权
- 7.2 修复思路与安全加强代码
- 7.3 验证与对比测试
- 总结
一、背景:为什么关注 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
mmap
对MAP_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]
可能为其他进程使用过的数据片段,造成信息泄漏。
漏洞剖析
- mmap 创建 VMA,但物理页可能从空闲页池中分配。
- 如果系统未强制清零(例如在启用了大页、性能优化模式下),内核可能直接分配已被释放但尚未清零的物理页。
- 用户进程读取时就会看到旧数据。
攻击场景
- 恶意程序希望窥探敏感数据(如内核内存、其他进程的隐私信息)。
- 在高并发应用中,很容易在
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
会强制覆盖已有映射(页表条目),导致程序代码或关键数据区被替换为文件内容,攻击者可借此注入恶意代码或修改变量。
漏洞剖析
MAP_FIXED
告诉内核“无视现有映射,直接将虚拟地址 0x400000 – 0x400FFF 重新映射到文件”。- 如果该地址正被程序或动态链接库使用,原有映射立即失效,不同于
mmap(NULL, ...)
,后者由内核选取不会覆盖已有区域。 - 恶意构造的
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;
}
- 预期:映射指定文件并输出内容。
- 风险:攻击者在
stat
与open
之间,将路径改为指向/etc/shadow
或包含敏感数据的文件,程序仍会根据第一次stat
的大小信息调用mmap
,导致将敏感内容映射并输出。
漏洞剖析
- TOCTOU(Time-Of-Check to Time-Of-Use):在
stat
检查阶段和open + mmap
使用阶段之间,文件或符号链接被替换。 - 程序仍使用第一次
stat
的size
信息,即使实际文件已改变,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
将文件缓冲区在多个用户/进程之间同步,可能导致数据竞争和越权写入。
漏洞剖析
- 线程 A、线程 B 使用 相同文件描述符,并以
MAP_SHARED
映射到相同物理页。 - 线程 B 不应有写入权限,却能通过映射绕过文件系统权限写入数据。
- 若文件原本只允许用户 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 保护。
漏洞剖析
- 初始
mmap(..., PROT_READ, ...)
应只允许读权限,文件内容不可被修改。 - 但是调用
mprotect(map, size, PROT_READ | PROT_WRITE | PROT_EXEC)
直接将映射页设为可写可执行。 - 攻击者注入恶意指令并执行,造成任意代码执行。
四、安全使用 mmap 的最佳实践
针对上述典型漏洞,下面给出在生产环境中安全地使用 mmap 的若干实战建议与代码示例。
4.1 严格控制权限与标志:PROT\_* 与 MAP\_*
最小权限原则:只打开并映射所需权限,避免无谓的读写可执行组合:
- 只需读取时,使用
PROT_READ
+MAP_PRIVATE
; - 只需写入时,使用
PROT_WRITE
+MAP_PRIVATE
(或MAP_SHARED
),并避免设置PROT_EXEC
; - 只需执行时,使用
PROT_READ | PROT_EXEC
,不允许写。
- 只需读取时,使用
杜绝 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);
慎用 MAP\_SHARED:
- 若映射的文件内容不需写回,可优先使用
MAP_PRIVATE
,避免多进程/线程数据竞争。 - 仅在真正需要“多进程共享修改”时,才使用
MAP_SHARED
。
- 若映射的文件内容不需写回,可优先使用
4.2 避免 MAP\_FIXED,优先使用非强制地址映射
- 风险:
MAP_FIXED
会无条件覆盖已有映射,可能覆盖程序、库、堆栈等重要区域。 建议:
- 尽量使用
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);
}
// ...
- 总结:除非必须覆盖已有地址(且明确知晓风险并手动解除),否则不要使用
MAP_FIXED
。
4.3 使用 mlock / mlockall 防止页面被换出敏感数据
- 场景:若映射区域包含敏感数据(如密钥、密码、个人隐私),内核在换页时可能将此页写回交换空间(swap),导致磁盘可被读取、物理内存可被法医工具恢复。
做法:
- 通过
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);
- 注意:
mlock
需要CAP_IPC_LOCK
权限或足够的ulimit -l
限制,否则会失败。若不能mlock
,可考虑定期用memset
将敏感数据清零,降低泄露风险。
4.4 使用 MADV\_DONTFORK / MADV\_NOHUGEPAGE 避免子进程继承敏感映射
- 场景:父进程
fork()
后,子进程继承父的内存映射,包括敏感数据页。若子进程随后被更高权限用户读取,有信息泄漏风险。 做法:
- 对于敏感映射区域调用
madvise(..., MADV_DONTFORK)
,使得在fork()
后子进程不继承该映射; - 对于不希望大页(2MB)参与映射的,调用
madvise(..., MADV_NOHUGEPAGE)
,避免页面拆分或合并导致权限混乱。
- 对于敏感映射区域调用
// 在父进程映射敏感区域后
madvise(sensitive_buf, len, MADV_DONTFORK); // 子进程不继承
madvise(sensitive_buf, len, MADV_NOHUGEPAGE); // 禁用大页
- 注意:
MADV_DONTFORK
对 Linux 2.6.25+ 有效,低版本可能不支持;若必须在子进程中访问,可考虑先 fork,再单独映射。
4.5 及时解除映射与使用 msync 保证数据一致性
- 场景:对于
MAP_SHARED
映射,写入后需要保证数据已同步到磁盘,否则突然崩溃后会造成文件不一致甚至数据损坏。 做法:
- 在写入完成后,调用
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");
}
- 注意:过于频繁调用
msync
会严重影响性能;应按业务需求合理批量同步,避免在高并发场景中造成 I/O 瓶颈。
五、防范 TOCTOU 与缓解竞态条件
TOCTOU(Time-Of-Check to Time-Of-Use)是文件映射中常见的竞态漏洞。以下示例展示几种原子性地打开与映射以及路径白名单等技术,防止攻击者利用竞态条件。
5.1 原子性地打开与映射:open+O\_CLOEXEC 与 fstat 一致性检查
使用 open+O\_CLOEXEC:
O_CLOEXEC
标志确保子进程继承时不会泄露文件描述符,避免恶意在子进程中替换目标文件。
直接通过 fd 获取文件大小,避免先
stat
后open
的 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 与路径白名单来避免符号链接攻击
- 限制应用只能从预先配置的可信目录加载文件,例如
/etc/myapp/
或/usr/local/share/myapp/
,避免用户可控路径。 - 检查路径前缀,禁止符号链接绕过:在
open
后再调用fstat
查看文件的st_dev
、st_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_ino
或st_dev
会不一致,从而拒绝继续映射。
六、用户空间与内核空间的安全隔离
即使在用户层面做了上述优化,仍需借助内核安全机制(如 SELinux、AppArmor、seccomp)来加固 mmap
相关操作的访问控制。
6.1 SELinux / AppArmor 策略限制 mmap 行为
- SELinux:可为进程定义布尔(Boolean)策略,禁止对某些文件进行映射。例如在
/etc/selinux/targeted/contexts/files/file_contexts
中指定/etc/secret(/.*)?
只允许read
,禁止mmap
:
/etc/secret(/.*)? system_u:object_r:secret_data_t:s0
- 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 相关系统调用参数
- 应用场景:在高安全环境(如容器、沙盒)中,使用 seccomp-BPF 对
mmap
、mprotect
等系统调用进行过滤,拒绝所有带有PROT_EXEC
标志的请求,或者拒绝MAP_SHARED
、MAP_FIXED
。 - 示例:使用 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 监控与审计
- 实时监控映射:运维或安全审计人员可以定期
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)。
- 定期主动扫描与告警:安全运维可编写脚本监控特定关键进程的
/proc/[pid]/maps
,一旦检测到带EXEC
或WRITE
的映射,立即告警或终止进程。
七、实战案例:修复一个 mmap 漏洞
7.1 漏洞演示:TOCTOU 结合 MAP\_FIXED 的本地提权
漏洞描述
目标程序 vulnapp
在 /usr/local/bin/vulnapp
下为 SetUID Root 可执行文件。它会:
- 从
/tmp/userid
文件中读取一个管理员的用户 ID,确保只有管理员可映射该文件。 - 在
stat
检查后,将/usr/local/bin/admin.dat
文件通过mmap
映射到默认可写地址。 - 将文件内容读入并检测权限,判断是否为管理员。
漏洞逻辑示例:
// 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;
}
- 攻击者在
/tmp/userid
写入0
,通过管理员检查; - 在
stat(admfile)
与open(admfile)
之间,将/usr/local/bin/admin.dat
替换成任意恶意文件(如包含I am admin
字符串的 shell 脚本); mmap
将恶意文件映射到可写可执行地址,再通过覆盖.text
或 GOT,执行提权。
7.2 修复思路与安全加强代码
- 使用 open + O\_CLOEXEC + fstat 替换 stat + open:避免 TOCTOU。
- 不使用 MAP\_FIXED,而采用非强制映射。
- 限制只读权限,不允许将 admin.dat 映射为可写。
- 添加 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
文件。
- SELinux/AppArmor:确保非管理员用户无法替换
7.3 验证与对比测试
原始漏洞版本:
gcc -o vulnapp vulnapp.c sudo chown root:root vulnapp sudo chmod u+s vulnapp
- 普通用户替换
/usr/local/bin/admin.dat
为自制可执行内容,执行./vulnapp
即可提权。
- 普通用户替换
修复版本:
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
的安全风险,并基于最小权限原则给出了详细的应对策略:
- 严格控制
mmap
的权限标志,避免可写可执行的映射; - 杜绝 MAP\_FIXED 的误用,优先让内核自动选择映射地址;
- 使用 mlock/madvise 等接口防止换页或子进程继承敏感内存;
- 原子性地打开与映射,通过
open + O_CLOEXEC + fstat
避免 TOCTOU; - 结合操作系统安全机制(SELinux / AppArmor / seccomp-BPF),在内核层面进一步限制可疑
mmap
行为; - 及时解除映射并合理使用 msync,确保数据一致性且减少映射生命周期内的攻击面。
通过文中的代码示例与图解,你能更加直观地理解 mmap
在内核中的实现原理与漏洞原理,并在实际项目中落地安全加固。
评论已关闭