HITB GSEC CTF 2017-1000levels
1.与之前BCTF 2017-100levels一模一样,只不过最大值变成了1000关,所以这里也同样可以用爆破来做,但是可以用另一种方法,vsyscall。
2.进入IDA可以看到有一个hint函数,而且里面有system函数,但是很奇怪:
1 | #注释头 |
这个代码没怎么看懂,还是看下汇编代码:
这里就是将system的地址赋值给rax,然后rax给栈上的[rbp+var_110]赋值。之后也没有什么其它的更改栈上[rbp+var_110]的操作,所以进入hint函数之后,一定会将system函数放到栈上,通过调试也可以看出来。
3.之后进入go函数,发现如果第一次输入负数,原本将关卡数赋值给[rbp+var_110]的操作就不会被执行,那么[rbp+var_110]上保存的仍然是system函数的地址。之后再输入关卡数,直接加到[rbp+var_110]上,那么如果第一次输入负数,第二次输入system函数和one_gadget的偏移,那么就变相将[rbp+var_110]上存放的内容保存为one_gadget的地址。
▲这里需要注意的是,[rbp+var_110]是在hint函数中被赋值的,而go函数中用到的也是[rbp+var_110]这个变量,但是不同函数栈肯定是不同的,所以这里两个[rbp+var_110]是不是一样的就值得思考一下。看程序可以发现,hint函数和go函数都是在main函数中调用的,那么如果调用的时候两处的rsp是一样的就可以保证两个函数的rbp一样,也就代码[rbp+var_110]也是一样的。查看汇编代码:
可以看到从读取选项数据之后,到判断语句一直没有push,pop之类的操作,也就是说程序无论是运行到hint函数还是go函数时,main函数栈的状态都是一样的,从而导致进入这两个函数中的栈底也都是同一个地址,那么[rbp+var_110]也就一样,所以用hint函数来为[rbp+var_110]赋值成system函数,再用go函数来为[rbp+var_110]赋值为one_gadget这条路是可以的,同样可以调试来确定一下。
4.那么赋值之后进入level关卡函数,由于递归关系,最后一关的栈是和go函数的栈连在一起的,所以可以通过最后一关的栈溢出抵达go函数的栈,从而抵达[rbp+var_110]这个地址处。
5.但是栈溢出只能修改数据,就算控制eip,但是也并不知道[rbp+var_110]处的真实地址,只能通过调试来知道偏移是多少。所以这里需要用的vsyscall来将rsp下挪到[rbp+var_110]处从而执行vsyscall的ret操作来执行[rbp+var_110]处的代码,也就是one_gadget。
6.这里看一下vsyscall处的数据:
▲vsyscall的特点:
(1)某些版本存在,需要用到gdb来查看,IDA中默认不可见。
(2)地址不受到ASLR和PIE的影响,固定是0xffffffffff600000-0xffffffffff601000。
(3)不能从中间进入,只能从函数开头进入,意味着不能直接调用里面的syscall。这里vsyscall分为三个函数,从上到下依次是
A.gettimeofday: 0xffffffffff600000
B.time: 0xffffffffff600400
C.getcpu: 0xffffffffff600800
(4)gettimeofday函数执行成功时返回值就是0,保存在rax寄存器中。这就为某些one_gadget创造了条件。
7.观察代码可以发现,三个函数执行成功之后相当于一个ret操作,所以如果我们将gettimeofday放在eip处,那么就相当于放了一个ret操作上去,而ret操作又相当于pop eip,那么就相当于直接将rsp往下拉了一个单位。如果我们多次调用gettimeofday,那么就可以将rsp下拉多个单位,从而抵达我们想要的地方来执行代码。那么这里就可以将eip改成gettimeofday,然后在之后添加多个gettimeofday来滑到one_gadget来执行代码。
8.所以现在就可以编写exp了
(1)前置内容:
1 | #注释头 |
这里由于相加关系,levels=system_addr + one_gadget_offset - libc_system_offset,肯定超过999,所以关卡数一定是1000关。
(2)开始循环答题,直至到达最后一关执行栈溢出:
1 | #注释头 |
(3)最后一关执行栈溢出,利用gettimeofday滑至one_gadegt从而getshell。
1 | #注释头 |
▲以下是测试[rbp+var_110]的数据:
main函数中的rbp: 00007FFD3A854900
hint函数中的rbp: 00007FFD3A8548C0
go函数中的rbp: 00007FFD3A8548C0
▲vsyscall用法:
vsyscall直接进行syscall,并没有利用栈空间,所以在处理栈溢出,但是由于PIE没有别的地址可以用时,而栈上又有某个有用的地址的时候,可以通过vsyscall构造一个rop链来ret,每次ret都会消耗掉一个地址,将rsp下拉一个单位,这样就可以逐渐去贴近想要的那个地址,最后成功ret到相应的位置。
▲vdso的特点:
(1)vdso的地址随机化的,且其中的指令可以任意执行,不需要从入口开始。
(2)相比于栈和其他的ASLR,vdso的随机化非常的弱,对于32的系统来说,有1/256的概率命中。
(3)不同的内核随机程度不同:
A.较旧版本:0xf76d9000
-0xf77ce000
B.较新版本:0xf7ed0000
-0xf7fd0000
C.其它版本:
可以编译以下文件之后用脚本查看:
1 | //注释头 |
查看脚本:
#注释头
1 | #!/usr/bin/python |
▲vdso的用法:与vsystem类似,泄露出地址后相当于有了syscall。另外32位条件下有__kernel_rt_sigreturn,可以打SROP。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157