逃逸初探
2020 TSCTF hellovirtual
是一个虚拟化的题目,在2018 hitcon abyss
改编的题目。这里给出了三个文件hellovirtual,hellokernel,hellousr
,还给出了ld.so.2,libc.so.6
。
hellovirtual
是一个利用KVM api
来做虚拟化的程序,它会加载一个小型的内核hellokernel
,这个内核仅仅实现了内存管理和程序中断的功能,提供了loader
启动和libc
加载的一些syscall
。然后解析ELF
启动一个用户态的程序,这里直接使用的是ld.so.2
来加载hellousr
。hellousr
是一个用户态的程序可以直接在主机上运行。执行流程就是用户态程序hellousr
发生系统调用时,hellokernel
对系统调用进行一些检查,将一些与IO
相关的比如read,write
通过I/O Prot
(CPU in/out
指令)交给hellovirtual
处理。
分析
virtual
先来看virtual
也就是kvm
。先放出几个常用的结构体,和操作的十六进制值。
1 | struct kvm_memory_region |
一个典型的vm
调用如下
1 | // 获取 kvm 句柄 |
但是在pwn
题目中我们最为关心的是程序保护开启的情况,现在知道的NEX
保护也就是不可执行保护是通过ERREF
寄存器开启的,我们看一下该寄存器的定义
我们看到NXE
标志位是1<<11,0x800
。通过ioctl(vcpufd, KVM_SET_SREGS, &sregs)
来设置寄存器的值,而virtual
中并没有对该位进行设置,因此可以判断程序是没有开启NX
保护的。
根据上面的ioctl,request
的十六进制可以看到vcpu
的运行是在偏移0x171a
的位置。发生EXIT_IO
的时候程序执行了两个函数
由于第一个函数是输出错误,因此判断第二个函数是处理IO
相关系统调用的函数,根据函数内部的输出信息结合kernel
我们可以得到最终的kernel
与hypervisor
的交互情况。
kernel
在逆向内核的部分的时候主要关注的有两个点
- 内核地址空间,用户地址空间,页表
- 系统调用表。
首先是entry.s
,最开始的位置,从名称中我们也能看出来,该部分的代码应该是内核的起始代码,在代码中首先将参数取出,随后调用了一个函数,随后就一直执行hlt
。该函数应该就是kernel_main
函数。
结合源码来看kernel_main
函数中首先是初始化了页表,接着初始化了内存分配器,注册系统调用,最后切换到用户空间
我们看一下初始化页表的操作
在初始化页表中,首先读取了rc3
寄存器的值赋值给了pml4
cr3
寄存器是页目录基址寄存器,保存页表目录表的物理地址。pml4
是页表四级映射表。
从循环中可以看出空间的总共大小为0x2000000
,该控件包含用户空间和内核空间。该部分的大小也可以从virtual
中得到。从int_allocator
函数的调用中我们可以得到内核空间的BASE
地址为0x8000000000
,init_allocator
是做了一个0x8000000000-0x8002000000
到0x0-0x2000000
的映射。init_allocator
中的while
循环实际上是一个memset
的过程。
接下来就是注册系统调用了,这里采用的是__writemsr
函数来写模式定义寄存器(Model Specific Register
(wrmsr
) ),这里声明了syscall
入口,也就是syscall_entry
函数的地址
注册完毕系统调用之后就是切换到用户空间执行hellousr
。
系统调用表
从syscall_entry
中继续进行分析,该函数的特征也很明显,进行了一大堆的保存和恢复寄存器的操作,也就是push/pop
。中间调用的函数就是syscall_handler
了。从汇编代码中分析,主要是根据rax
也就是系统调用号跳转到相应的函数去执行,函数的地址= syscall_table+rax*8
。这样我们就找到了系统调用表。根据64
位的系统调用号恢复出系统调用表
其实该表就位于初始分析时棕黄色部分的起始位置。每一个syscall
系统调用都会对应一个hypervisor
的对应的处理函数。
usr
用户态程序很简单,当申请的team=10
时,edit name
编辑会造成一个字节溢出,覆写solgan
的低一字节为0
。
bypass userspace
多层穿透的题目一般有多个flag
,相当于每一层都有一个flag
,先从用户层看起,也就是hellousr
。程序的漏洞很明显,在分配team
超过10
的时候会有一个字节的溢出,会将slogan
指针的低一字节覆写为0
,控制好堆布局就可以将该指针指向team 9
的控制堆块,也就是可以通过10
控制9
实现任意地址的写,而又给出了elf,libc,stack
三个地址中的一个地址,这里选择elf
地址,通过任意地址写将bss
段中判断泄露地址函数执行次数的变量改写,从而多次泄露得到libc,stack
地址。之后通过任意地址写直接覆写返回地址为rop chain
。正常情况下这里应该已经可以读取出flag
来了。
但是kernel
中对open
的系统调用进行了处理,我们看一下
只能打开特定的文件,这里看到对flag
进行了处理,我们看一下hook
函数
程序首先打开了flag
文件,接着mmap
了一块内存,并将flag
的内容读取到了mmap
的地址空间中,并将该地址空间的权限设置为了2
也就是仅可写的权限,最后返回给了用户mmap
地址空间中的地址。
注意到是没有开启NX
的,因此可以直接执行shellcode
。因此我们的rop chain
设置如下,不知道为什么最开始直接在shellcode
中写flag
不行(推测是地址的问题,不知道这东西怎么调试)。
1 | shellcode = asm(shellcraft.open('flag', 0, 0)) |
EXP
1 | # encoding=utf-8 |
bypass kernel
这个是参考官方的WP
。
说明这个函数调用表是一个0x2333
大小的表,在处理系统调用号为0x2333
的时候实现了一个自己的函数,我们看一下,实现了一个简单的菜单,主要有new,show,edit,delete
四个功能。也就是可以从用户空间分配内核空间的堆块。
这里主要存在了两个漏洞,一个是show
中的任意读漏洞,另一个就是delete
函数中的UAF
漏洞。由于kernel
对用户空间进行的open
进行了过滤,并且只能打开flag
,因此这一步我们需要达到内核任意写的效果,通过内核的任意写改写内核代码中open
的过滤的规则,从而可以打开任意的文件。
不过这里的kmalloc,kfree
是作者自己编写的,我们需要逆向一下,逆向之后的代码贴在后面了,从逆向结果中我们可以得到chunk
的结构如下
1 | | size | 0 | << chunk header |
分配chunk
的时候size
需要是0x10
对齐的,并且分配出来的chunk
是以0x80
为单位。堆的管理共分为两个部分,称之为unsorted bin
和sorted bin
。分配的时候如果没有特殊的情况则首先从sorted bin
中进行分配,sorted bin
中的堆块是按照从小到大的顺序进行排列的,如果sorted bin
中没有合适的堆块,那么则在unsorted bin
中分配。在释放的时候如果堆块和unsorted bin
相邻,则将释放的堆块和unsorted bin
进行合并。
可以看到kernel
内部的堆块分配和释放除了对size
进行了检查之外其他的并没有任何的安全检查。我们可以利用double free
分配堆块到buf_list(0x159c0)
中,利用size_list(0x159c8)
中记录的堆块size
作为伪造堆块的size
。
各个重要结构体的偏移如下
1 | buf_list->0x159c0 |
- 首先构造
double free
1 | shellcode = add_kernel(0, 0xf0) * 30 # 防止kernel中原有的chunk的干扰(删除也可) |
/dev/zero
就是hypervisor
监管的内存了,内核空间和用户空间都在这里,调试的方法就是在shellcode
中加上无限循环的shellcode
,在程序陷入循环的时候使用gdb attach
上去,查看内存是否发生了改变。从上面我们可以看到已经出现了double free
。
- 覆写
free chunk->fd
指向size_list
,分配得到chunk_list
的堆块,此时我们即可以控制chunk_list
1 | shellcode += asm(shellcraft.read(0, stack_address, 0xf0)) # 读取我们需要修改改的内容 |
- 覆写
chunk_list
指向open
函数的代码段,覆写open
的代码使得文件名的过滤失效。首先我们看一下代码在什么位置。
只要将0xb73
处的jnz
条件跳转patch
掉就可以打开任意的文件了。所以我们将chunk_list
中的某一个chunk
的ptr
指向该地址,随后修改。
patch sys_open
调用,打开任意文件。
1 | shellcode += edit_kernel(5, 0xb, stack_address+0x8) # write sys_open code address to chunk_list |
从上图中我们可以看到已经成功patch
了sys_open
的函数,现在可以打开任意的文件了。之后就是打开flag
文件读取flag
的过程了,题目中给出了第二个flag
文件的文件名。
1 | shellcode += asm(shellcraft.open('flag_xmzyshypnctql', 0)) |
其中stack
中的内容如下
1 | payload = b'\xc8\x59\x01'.ljust(8, b'\x00') + p32(0xdeadbeef) * 2 + b'\x73\x0b\x00' |
EXP
1 | # encoding=utf-8 |
kmalloc,kfree
1 | // kmalloc |