HFCTF
没空去结束了之后简单做了一下pwn
gogogo
go写的,不太会分析,首先运行起来,搜索 LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
能找到 main_main
但是断点下来没有用,搜了一下一个go程序的启动过程
1 | // The main goroutine. |
于是决定去看一下 runtime.main
,然后就可以找到一个里面一堆 printf
的调用
1 | runtime_startTemplateThread(); |
里面每一个printf都打印单个字符(所以搜那个字符串定位不到这个函数)
1 | while ( (unsigned __int64)&v156 <= *(_QWORD *)(v0 + 16) ) |
然后输入1416925456就可以进入一个猜数字游戏
1 | else if ( v2 != 1416925456 ) |
go的题目多半都是溢出,在这个函数里搜索一下read,scanf等等相关内容可以注意到
1 | .text:0000000000494AE2 lea rax, unk_49D900 |
接下来就是怎么达到这里
1 | .text:0000000000491E85 lea rcx, aDDDD ; "%d %d %d %d" |
依次输入四个数字就可以开始猜,然后搜索一下Bulls and Cows游戏,返回正确且位置正确的个数和正确但位置错误的个数,能写个简单的猜测算法(其实也不用,自己玩完再send payload就好了)
开graph view开一下,溢出点在EXIT的位置,输入EXIT选项之后就可以传payload
算法也很容易,先生成一个全部的可能选取集合,然后抽一个数输入,得到 bulls and cows
的值,将现在的输入和集合内的数进行比较,如果bull和cow都相同,就说明这个是可选项,反复操作,直到只剩下一个选项即可。
最后直接ret2syscall即可,exp如下
1 | from ntpath import join |
hfdev
这个感觉最麻烦的地方是没有DeviceState
的符号,但是应该很容易可以注意到在 hfdev_process
中
1 | if ( (unsigned __int64)(unsigned __int16)result > *(_QWORD *)(a1 + 2672) ) |
这一部分的>=
很可能会导致一个byte的溢出,
接下来分析realize函数,注意到
1 | v2 = g_malloc0(48LL); |
v1[563]
处存放了一个QEMUTimer,回调函数为 hfdev_func
然后
1 | v3 = qemu_bh_new_full(hfdev_process, v1, "hfdev_process"); |
v1[564]
处存放了一个qemu bottom half,结构体如下
1 | struct QEMUBH { |
而
1 | static gboolean |
中 aio_dispatch
会调用 aio_bh_poll
,其中会调用 aio_bh_call
执行bottom half的callback
以及 hfdev_port_write
中
1 | case 12LL: |
qemu_bh_schedule
会执行 aio_notify
,所以 case 12:
的时候会触发这个bh callback
1 | v1[335] = 0LL; |
然后就是init pmio,大小32 bytes。接下来分析 hfdev_process
函数
1 | case 0x30: |
在 case 0x30:
的时候会调用timer_mod
,
1 | /** |
可以知道这会触发之前的timer,也就是会接着调用hfdev_func
,其中会修改state->pad3[1832]
,所以根据v12
的值,只能调用一次,同时可以注意到,hfdev_func
中
1 | v1 = *(_QWORD *)&a1->pad3[24]; |
a1>pad3[16]
不会被置0,所以如果能够多次触发,就能实现越界读写。注意到
1 | case 0x10: |
可以先让a1->pad3[16]=512
,第一次触发timer时,a1->pad3[16]+=0x100
,也就是a1->pad3[16]=768
,而上面1 byte溢出的溢出地址就是由a1->pad3[16]
控制的,这个时候可以溢出到
a1->pad3[1832]
也就是控制timer是否能够触发的标志位,就能实现多次触发时钟。因为能够多次触发时钟,所以我们可以获得一个很大范围的数据的读取,再看hfdev_process
中case 0x20:
的部分(这里还原了一些符号)
1 | case 0x20: |
这里可以让我们把数据读取回一个给定的物理地址,而v7
是我们可控的长度
1 | *(_QWORD *)&a1->pad3[1840] = (char *)v1 + v11; |
对比这个*(_QWORD *)&a1->pad3[1840]
,就是timer callback中的src地址,我们可以泄漏这个地址(v1 + v11,符号化之后即为hfdev_state中控制结构体的起始地址加上第二个参数)。这个地址在堆区,可以先泄漏这个地址,然后计算出timer的地址。虽然在case 0x20:
对v11
的大小做了检查,但是由于之前增加了cursor
导致我们使用hfdev_process
和hfdev_func
都能够访问更大的范围,所以在触发timer,callback还未执行前,可以重新利用0x2022
功能修改掉hfdev_func
中的复制目标,正好,在hfdev_port_write
的case 0x10:
中就有能够修改callback触发延迟的功能。
1 | case 10LL: |
这样我们就可以在触发前利用溢出,将bh->cb
或者timer->cb
复制到timer
(这时的cursor指向timer)上,接下来就可以计算pie偏移,程序基址,system plt等等。
然后根据得到的地址,我们可以伪造一个timer,因为我们之前已经得到了发送的control结构的地址,而这个结构中有一块很大的buffer,足够构造一个fake timer (或者fake bh也可以) 将这个timer的callback设置为system
,并且opaque设置为命令的地址,这样该指针就会被rdi寄存器传递,然后触发timer即可(注意fake timer也要有timer_list)完整exp如下。
1 |
|
总体思路不难,但细节需要考虑一下,如果缺少对qemu方面的基本了解的话,逆向可能会有点麻烦。
babygame
确实是最简单的,第一个栈溢出泄漏一个栈地址,然后用字符串格式化泄漏canary和libc,改返回地址到main
,再次栈溢出跳转one gadget即可。
1 | from pwn import * |
vdq
rust写的,不太懂,ida分析出来非常复杂,对rust字符串的分析稍微有点问题(因为字符串结尾的问题),经过逆向分析,程序需要用["op1", "op2", "op3"]\n$
这样的方式输入,没有去除符号,所以能直接确定op的类型,有add,remove,view,append,archive,并且add,append需要数据输入。
因为输入很简单,但整个程序分析起来很复杂,先选择fuzz一下,脚本如下
1 | import random |
很快找到了一个double free,简化一下获得的poc
1 | ["Add", "Add", "Remove", "Archive", "Add", "Add", "Add", "View", "Remove", "Remove", "Remove"] |
调试定位崩溃位置
1 | gef➤ bt |
然后定位崩溃的位置
1 | .text:000000000000E2CC loc_E2CC: ; CODE XREF: vdq::handle_opr_lst::h7fb2393547b96358+7DA↑j |
查了一下drop_in_place
会释放掉内存,这里的这个container里面存了vdq::Note
,推断很有可能一个容器里一个note出现了不止一次或者一个note同时出现在两个容器(notes和archived_notes)
接下来缩减一下fuzz得到的poc,首先考虑的是View
,直觉上看view的功能应该只是打印一下内容,不会对触发漏洞造成太大的影响,但是从poc里去掉View了之后,崩溃就消失了,那么View很可能有重要的影响。
所以调整一下fuzzer,只使用三个操作Add, Remove, View,并且设定前几个操作只能Add,然后继续fuzz
1 | ["Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add"] |
拿到的一个比较好看的poc,之后的exp也是基于它编写
看看View的实现
1 | case 4u: |
看不懂,这里分别搜索print
,iterator::for_each
,make_contiguous
的vulnerability,搜到了 CVE-2020-36318. 根据容器的结构继续分析
1 | 00000000 alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> struc ; (sizeof=0x20, align=0x8, copyof_185) |
通过调试可以发现,deque的初始容量是4,当容器满后,capacity会翻倍,tail指向低地址尾部,head指向高地址头部
1 | gef➤ x/3xg $rdi |
同时可以发现,pop之后,容器对应的index不会立刻清除,而是移动了tail
1 | gef➤ x/4xg $rdi |
调试poc,观察view前后的变化
1 | gef➤ x/16xg 0x007ffc84bda940 |
综上(虽然不知道漏洞具体的细节)但是我们可以得到利用策略如下
- UaF一个大chunk,View得到libcbase,freehook,system等等地址
- 再次UaF,利用输入时的
get_raw_line
会申请一块地址存放输入数据的特性,控制大小,申请到一个vdq::Note
(并且由于UaF,这个Note现在处于使用中),修改掉对应的buffer的地址到freehook上,注意要加一个偏移量,因为append会在buffer的后面添加数据,让buffer的末尾刚好在freehook就行 - 使用Append,修改freehook为system,然后随便使用一个会调用
get_raw_line
的功能,输入/bin/sh即可。
完整exp
1 | from pwn import * |
mva
给定的程序实现了一个vm,指令长度4 bytes,可以发现,在mul
时没有检测操作数大小,导致越界读取
1 | case 0xDu: |
1 | movsx eax, [rbp+var_249] |
从汇编可以看出,可以传入负数,然后读出栈上的其它数据,然后再看
1 | case 0xEu: |
这里对第一个操作数的大小检查带符号,所以可以用负数绕过,达到越界写,这样我们可以覆盖虚拟机栈指针,虽然这只能写入一个负数,但是通过
1 | mov [rbp+rax*2+stack], dx |
rax*2可以引发整数溢出,再次把数值变为正数,从而将虚拟机栈顶指向返回地址,完整exp如下
1 | from pwn import * |