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的镜像mmap0x10000000,同时mmap 0x20000000作为数据区,0xEFFFC000作为stack. 然后通过0x40000000 + (appid << 24)找到属于这个app的共享内存,将其remap到0x30000000,并将其他app的共享内存设为只读。做完这些之后,主进程将为app设置特权级与seccomp filter,随后跳转到0x10000000处执行。故sys文件就是从0开始的代码,但基址是0x10000000.

在app运行时,可以通过fd 0,1与主进程交互,发送的request中可以指定offsetlength两个字段,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.syscrypto.sys,这两个sys都是通过RPC向外提供服务的。随后init.sys进入到一个类似命令行的循环中,可以使用downloadrun两个命令。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.

参考利用

test.c
exp.py

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也收不到响应了。

参考利用

test.c
exp.py

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使用。