有关SUSCTF2022的一些题目复现。复现的内容有参考队内队员脚本以及官方wp。题目和附件在文末,其中kernel两道题没有做。
happy_tree
这个pwn很有意思,出了一个递归导致的错误,以前还没见过(我做的题目还是太少了…)可能还是可以通过调试看出来。参考的wp是队内的脚本,赛后复现的。
程序主要实现了一个二叉排序树,按照data大小插入对应node左侧或者右侧。数据结构如下所示。这里主要用递归可能比较难理解一点。如果DS学的好可能比较容易。
1 | struct tree |
这个地方主要的漏洞是由于libc版本是2.27,在key位置不会写入数据,导致key位置的right指针在free后不会被清空。会导致如下的情况。
为什么right指向一个index为0的chunk可能不太好理解,是因为free之后,fd变成0,而fd位置也正好是index位置,因此就变成0。此时2依然有3的指针,那么free(0)就相当于free(3)第二次,导致double free。由于libc可以一开始泄露,因此后面的就很简单了。
1 | # 脚本参考albanis师傅 |
rain
一个实现打印字母雨的代码,很有意思。结构体有点小复杂,如下所示。
1 | struct rain |
我们能控制的是table2。程序接受一个buf输入,能够修改雨的类型和table等。逆向起来也不复杂。
1 | def form_buf(height,width,front_colour,back_colour,rainfall,content=0): |
有一个比较明显的漏洞在于config的时候调用了realloc,其中size可控。和pwnable的realloc两道题目很相似。这里控制realloc的size是0就可以实现UAF。但是比较困难的地方在于realloc到一个较大的size时不会从tcache里面取值。这就要求构造fastbin attack。但是即使构造好了,我们的指针也只能指向fastbin的第一个块,这个时候不能进行任何操作,如果改小size,下一次分配的时候通不过fastbin的size检查,如果改大了,会释放原先在fastbin头部的块,造成fastbin top double free的错误。比赛的时候就是卡在这里,没有了思路。
赛后看wp,发现大多都调用了raining刷新结构体。我本来也有想到这个,但是每次一刷新bash就坏掉了,直接EOF,单步调试发现可能是秒数的地方被改掉了(因为用了打unsortedbin的方法泄露libc)看了别人的wp才知道还能用GOT表,当时忘掉了。。。应该是只能通过劫持rain结构体来做。
step1 double free
double free还是很简单的,2.27(1.2)没有任何检查
1 | buf = form_buf(0x1,0x1,0x2,0x1,0x64,b'a'*0x48) #0x90 |
step2 rain
要先rain之后,清空原来的指针,之后malloc,才能利用double_free劫持相应结构体。具体因为在init中,可以看到malloc到了一块可控的大小
之后我们只要控制下面的malloc不会把我们double free的申请出来就行。我们后面申请table的时候再申请一个,就相当于有第二份可控的指针用来修改rain结构体的数据。改其中数据为GOT表即可。
下图为源数据。看的方法是rain之后看0x50的bins中剩下的那个指针。
我们修改之后的我数据长这样。
看似成功了?不!因为此时table2就是这个结构体的起始地址,因此我们将打印出chr(0x50)而不是table中修改的libc!
我到这里也很苦恼,不知道怎么做。因为没办法让table2清空。看了wp才发现。程序中这里很诡异。
这里判断是否打印我们的table时判断条件是table&&*table,感觉这个*table明显是多余的,这也是我们利用的方法。只需要realloc当前rain的table2并在开始字节写入一个p64(0)就能实现*table=0,从而打印table1内容!
1 | buf2 = form_buf(0x50,0x50,0x2,0x1,0x64,p64(0)) |
getlibc之后?
在之前一道realloc-revenge中分析过,如果单纯使用realloc完成tcache攻击,至少需要两个快,否则需要堆溢出或者破坏堆结构,但是只有一次的写入机会。这里getlibc之后依然使用劫持结构体的方法写入freehook。和上述方法几乎完全一样。不过需要注意几点。
- 由于之前double free过rain chunk,因此fd位置被改成rain的height和width。如果再次malloc则会报错。因此我们需要这样修改一次rain。
1 | buf = form_buf(0x0,0x0,0x2,0x1,0x64,payload) # important to write zero, because 0x48 chunk need NULL fd |
- 利用此方法使用tcache写入freehook时,需要注意free_hook地址写成free_hook-0x8(思考一下为什么?)因为我们还需要一个/bin/sh要在某个堆块头部,很容易想到可以放在table2头部,调用realloc(ptr,0)时触发free_hook。但是这样free_hook就会覆盖为/bin/sh。因此写入free_hook-0x8时,可以第一个地方写/bin/sh,第二个地方写system。思路还是基本一致的。
exp
1 | from pwn import * |
反思
自己做题的时候有想过rain这个操作能够更新结构体,其实能够更新也就很方便的可以做出来。但是一旦rain终端就崩了,很是苦恼。现在发现是因为form_buf的时候设置的colour大小出错,本应该是p8,写成了p16。实在是可惜啊。。
不过也通过这道题学到了一个新的思路:如果程序申请了结构体,可以通过想办法double-free打结构体,使得结构体指针可控。其实kernel条件竞争中存在着这样类似的思路(劫持cred结构体)。