P4 Teaser CONFidence CTF 2019 - p4fmt
国际强队p4首次举办CTF,一天时间确实有些紧张,而且只有一道内核pwn题。不同于传统内核pwn使用设备驱动来模拟内核漏洞的方式,该题实现了另一种可执行格式的装载和运行,利用方式也略有不同。
Linux可执行文件的解析
在kernel module中可以注册新的可执行文件解析器,具体结构如下
1 | static struct linux_binfmt elf_format = { |
以上是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 | 0xffff8d78c5805048: 0x0000000101003450 0x0000000000000090 <------- buf |
在找到段表之后,紧接着会调用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