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 |