国际强队p4首次举办CTF,一天时间确实有些紧张,而且只有一道内核pwn题。不同于传统内核pwn使用设备驱动来模拟内核漏洞的方式,该题实现了另一种可执行格式的装载和运行,利用方式也略有不同。

Linux可执行文件的解析

在kernel module中可以注册新的可执行文件解析器,具体结构如下

1
2
3
4
5
6
7
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};

以上是ELF文件的格式结构,同时在模块初始化时,需要调用register_binfmt(&elf_format)对相应文件格式进行注册。这样当内核在execve一个文件时,就能够调用其中的load_binary函数来进行装载了。在调用时会传入一个linux_binprm结构,内含一个小的128字节buffer,便于模块对其头部进行解析。若需要之后的数据,则需要从其中的file结构体指针来读取。

P4文件格式

p4fmt.ko实现了对p4格式的可执行文件的解析,文件需要以P4字节开头。Header中存储了版本、入口点、类型等信息。后面由若干个segment构成,每个segment能够指定其加载的内存地址、偏移和权限。大体来说与ELF的segment装载过程很类似,但没有ELF的动态装载过程,最后直接从入口点开始执行。

漏洞分析

Leak

文件头部指定了segment段表在文件中的offset,而解析时没有对其进行检查,导致在寻找段表时能够越过128字节的buffer读取。在linux_binprm结构中,在buffer后面存在一个cred指针。

1
2
3
4
5
6
7
8
9
10
0xffff8d78c5805048:	0x0000000101003450	0x0000000000000090 <------- buf
0xffff8d78c5805058: 0x0000000000000000 0x0000000000000000
0xffff8d78c5805068: 0x0000000000000000 0x0000000000000000
0xffff8d78c5805078: 0x0000000000000000 0x0000000000000000
0xffff8d78c5805088: 0x0000000000000000 0x0000000000000000
0xffff8d78c5805098: 0x0000000000000000 0x0000000000000000
0xffff8d78c58050a8: 0x0000000000000000 0x0000000000000000
0xffff8d78c58050b8: 0x0000000000000000 0x0000000000000000
0xffff8d78c58050c8: 0x00007fffffffef93 0x0000000100000001
0xffff8d78c58050d8: 0x0000000000000000 0xffff8d78c759f600 <------- cred pointer

在找到段表之后,紧接着会调用vm_mmap设置相应的page,并使用printk来打出调用时的参数。那么当我们把offset设置到cred指针附近时,就能够通过打印出参数来读到cred的指针了。

任意地址写0

在设置segment权限时,如果addr & 8 == 1,则表示此segment不是从文件中load进来的而是直接进行全0映射。那么在vm_mmap调用结束后,模块调用__clear_user来将用户态的page清零。

但我在实验之后发现,__clear_user不仅能够将用户态的page清零,甚至能够将内核态的数据清零。而且其调用时的地址和长度不用对齐也能成功。那么我们通过构造segment表中的恶意表项,给出内核空间的地址,尽管vm_mmap会失败(这里也没有检查返回值),之后的__clear_user还是会正常执行。从而我们拥有了几乎任意地址写0的能力。

利用

在有了上面两者之后,我们就可以思考利用过程了。简单来说,首先泄露出cred的地址,然后将cred中的uid等数据改为0,在模块解析的最后,调用install_exec_creds为新进程安装cred,实际应该就是将linux_binprm中的cred结构拷贝过来。这样在最后start_thread并回到用户态之后,新进程就获得了root权限。

但考虑到leak时,段表实际处于我们不可控的区域,故整个过程需要分2步进行。在leak出某个cred结构的地址后,动态产生另一个p4可执行文件,目的是覆盖其中的uid等数据,并寄希望于解析时linux_binrpm中的cred刚好指向之前leak的地址。经过一番尝试,我发现还是有可能在多次执行之后撞到同一个地址的。

之后的工作,就是让第二个p4文件加载2个segment,一个用来改写cred,另一个正常的段来执行shellcode起shell即可。

参考脚本

总结

本题主要的工作量还是在于逆出p4文件的格式并能够自由构造,其中leak的问题还是比较容易发现。但之后要做改写还是需要对这些函数的机制有一定的了解,否则就只能一遍遍地进行尝试。我在很长一段时间都在调整vm_mmap的参数,希望能够将文件数据映射到内核空间。最后在测试了__clear_user的功能之后才有了一定的思路。

参考资料

Linux Kernel - 可执行程序的加载过程
binfmt_cgc.c
Playing with binary formats
Linux Kernel 5.0 - binfmts.h