DEF CON CTF Quals 2019 - LCARS
Workload比较大的3个题,这次DEFCON的2天基本都耗在这个题上了,前半段的做题过程被speedrun频繁打断,到了后24个小时才得以集中精力。不过最后还是没能完成第3题,有点遗憾。
主要逻辑
app启动逻辑与RPC
程序实现了一套类似sandbox的运行时环境,主进程LCARS
负责处理app的输出、打开文件、RPC等操作,在程序启动时根据启动参数load相应的文件并加入到自己维护的文件系统中,保存它们的fd. 对于不同文件有不同的权限,例如.sys
文件和.key
文件都是权限为0,普通文件则是权限为3.在app打开文件时,主进程会检查它的特权级是否满足打开该文件的要求,如果不满足则返回错误,如果满足则会通过sendmsg
系统调用传送该文件的fd,并把文件指针重置到头部。
主进程在内存中mmap
了一块共享内存0x40000000-0x50000000
,每个app占用0x10000000
大小。主进程将sys
的镜像mmap
到0x10000000
,同时mmap 0x20000000
作为数据区,0xEFFFC000
作为stack. 然后通过0x40000000 + (appid << 24)
找到属于这个app的共享内存,将其remap到0x30000000
,并将其他app的共享内存设为只读。做完这些之后,主进程将为app设置特权级与seccomp filter,随后跳转到0x10000000
处执行。故sys
文件就是从0开始的代码,但基址是0x10000000
.
在app运行时,可以通过fd 0,1与主进程交互,发送的request中可以指定offset
和length
两个字段,offset
表示buffer在共享内存中的偏移。app将需要传递的内容copy到0x30000000
,主进程在获得参数buffer时就去对应0x40000000 + (appid << 24)
的位置查找就可以了。通过request还可以实现app之间的RPC,获得buffer内容同样是通过读取共享内存完成的。
逆向sys文件
由于sys没有一个特定的格式,直接选择ida64的binary file打开,然后手动设定基址,并添加数据段。之后设置Options -> Compiler,选择GNU C++,就可以在代码段上按C键分析了。
papp文件装载
主进程首先启动的进程是init.sys
,它启动了2个app echo.sys
和crypto.sys
,这两个sys都是通过RPC向外提供服务的。随后init.sys
进入到一个类似命令行的循环中,可以使用download
和run
两个命令。download
能够写任意内容到文件,run
可以启动某个程序。
如果run
的程序不是sys,则init.sys
会启动loader.sys
并加载特定格式的papp文件。根据loader.sys
中的代码,文件格式类似ELF中的program header,可以指定若干个segment以及它们加载的地址和权限。比较特殊的是,在指定段的权限prot
时不仅有rwx位,还可以指定该段具有签名验证和加密,具有这种特性的段,loader.sys
会使用RPC调用crypto.sys
中的RSA签名验证代码和AES解密代码对该段数据签名进行验证及解密。而如果papp文件中存在没有验证的代码段,则该app运行时特权级将被降为3. 在所有的segment都load完之后,loader.sys
发消息给主进程调整特权级,并设置seccomp filter,随后跳转到最后一个代码段开始的位置执行,此时app就开始执行我们编写的shellcode了。
逆向crypto.sys
后发现,进行签名验证的2个publickey被硬编码在binary中了,但只有公钥是构造不了签名的,所以得想别的办法。
编写shellcode与调试
直接写shellcode有点繁琐,我就直接采取用c编写,编译成地址无关代码然后导出.text段的方式,然后在脚本中再将.text段封装成papp文件。之前在进行内核利用时为了缩减elf的大小也使用过不链接glibc,所有系统调用和底层函数都自己实现,然后用gcc -nostdlib -e main
进行编译,由于loader是从头开始执行的所以main函数要放在最前面。之后再用objcopy
将.text段的原始数据提取出来。其实如果能够更了解gcc的一些特殊写法和开关选项应该不用这么麻烦,还是对编译这一块不是太熟悉。
在调试时,由于启动sys的过程中会mmap直接映射文件,在attach时可以首先列出所有的LCARS
进程,然后逐一确认/proc/<pid>/maps
里面的Image,有选择地attach. 但是loader.sys
因为存活时间短,只能选择在其中patch出int 3
指令,并在主进程中设置fork-mode为child断下来。
LCARS000
远程有一个flag1.papp
文件,应该是调用了crypto.sys
中的一些功能进行flag的加密,并输出到终端。由于进行RPC调用时,需要将数据copy到0x30000000
的共享内存中,而一个app退出后,再次复用该段共享内存时并没有对数据进行清零。故我们首先运行flag1.papp
随后运行我们自己上传的papp文件,直接输出0x30000000
内存对应的内容,在其中就能够看到flag.
参考利用
LCARS022
远程有一个flag22.txt
文件,该文件在我们特权级为3的app中是读不到的,所以我们需要想办法干扰loader.sys
,抢在loader降权之前接管程序运行。这个版本的loader实现中没有校验mmap
的地址,导致我们能够将一些已经存在的page再次mmap
,然后写入我们构造的数据。原本我想通过破坏0x10000000
这块的几个page来直接劫持控制流,但后来避免不了崩溃只得作罢。
最后我选择了mmap
共享内存0x300000000
中的一个page和crypto.sys
对应的只读内存中的一个page. 这样就将原本的shared的page给变成了private,意味着失去了和其他app通信的能力变成了我们能够控制的数据。通过合理构造这2个page中的数据,能够伪造RPC的返回包,并通过签名校验和解密的过程,且能够将权限维持在1. 当以这种状态执行代码时,就能够读取到flag22.txt
文件了。
不过在完成签名代码段加载之后,为了能够正常发送request,需要把数据段头部一个表示当前共享内存top的变量(0x20000004
)改到private page外面,否则之后自己发送的request也收不到响应了。
参考利用
LCARS333
这题放的时间有点太晚了,比赛结束前2小时30分钟才放出。打开一看,在mmap
之前添加了一个mprotect
的尝试,如果mprotect
不报错的话说明这个page之前已经存在,loader就直接退出了。想了2个小时,没能找到很好的方法,坐等其他writeup.
后记
作者放出了challenge的源码和利用: https://github.com/o-o-overflow/dc2019q-LCARS000
在loader.sys
中如果load papp的过程发生了错误,则会unmap
之前记录的所有segment,但在这其中存在一个OOB Read. 只要将一个segment恰好放到data段的后面(0x20004000
的位置)并构造数据就能够munmap
任意内存。接下来的利用思路就和我利用LCARS022
的类似了,munmap
之后再通过load segment将内存mmap
回来,将shared memory变为private. 由于报错之后loader需要接收RPC才能进行下一次load过程,故首先需要一个LCARS022
中已经利用成功的app向loader发送RPC多次load。
实际上我的LCARS022
做法并不是intended way,在2中需要通过AES-CBC模式的IV来改掉已经签名的page当中的前16个字节,这样至少我们能够控制代码的前16个字节,再通过向栈的后面map一个segment来进行ROP. 这样在3中仍然可以用这种方法来进行利用,但这种方法的特权级不能读到flag333.txt
,只是用来向loader发送RPC使用。