浙江2020省赛pwn1
据学长说,那一次的ctf出题人一句“我就没打算让pwn有解”让全场所有pwner直呼内行,pwn1看似简单实则在比赛过程中能出比赛也快结束了。
分析elf确定大概思路
IDA打开分析elf文件
main()
函数
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
别的可能会有些许差异,但是它调用了prctl
函数我们就要有所警惕了,就要想它应该开了沙箱保护。用seccomp-tools
查elf
的沙箱保护,发现该elf禁用了execve
系统调用。
这个保护的开启就相当于断绝了我们调用system
的绝大部分的命令,诸如/bin/sh
或者是cat flag
这类的。对于这个我们也是有应对措施的,那就是orw
(open
-read
-write
的缩写)。这一题很明显就是要我们用这个方法去获取flag
了。
寻找漏洞点
确定好思路之后进welcome()
函数寻找漏洞点。
很明显存在格式化字符串漏洞和栈溢出漏洞,而plt表段存在_stack_chk_fail
函数,那就是开了canary
保护,那么我们肯定是先利用格式化字符串任意读的特性泄露canary
,再栈溢出。
canary偏移确定
首先是看字符串相对于格式化字符串第一个参数的偏移。因为buf
刚好在welcome
函数的缓冲区顶部,因此很容易可以猜测buf
距离第一个参数差了6*8个字节,因为它们中间隔了5个寄存器。然后buf
到栈底又有0xb0
个字节,而canary就在rsp-8的位置上。综上所述,canary应该在printf除了格式化字符串参数以外的第(6x8+0xb0-8)/8=27
,所以第一次的payload
我们就给%27$p
,看看输出的是不是canary
有如下特性:
①随机性,每次运行差别都会很大
②最低位字节永远为\0
多次测试发现均符合上面两个特性,因此canary就被泄露出来了。
我们接收的是数字组成的字节,因此我们会接收到16个16进制的数,然后字节转成数值后用p64转为字节就可以利用canary
了。
1 | payload=b'%27$p' |
这就是第一步的payload
栈溢出泄露libc
题目中存在输出函数puts
。
这个用一般的payload就可
1 | payload=b'a'*(buf_size-8)+canary+b'a'*8+P64(pop_rdi)+p64(puts_got)+p64(puts)+p64(welcome) |
泄露完成之后记得重新执行welcome
实现二次溢出执行一些libc的函数,因为我们不需要system
,只需要orw
,在已有的函数中就缺一个open
函数。找到偏移得到open的真实地址。因为我只能本地复盘,又没有加载它给的libc,因此偏移只能是我自己本地调试是多少就是多少了。
open函数的调用
open函数只需要两个就能完成,一个是"flag"
或者是"flag.txt"
,另一个参数就是"r"
或者是0。而只有栈是我们可读可写可以随意控制的,因此我们还需要泄露栈地址。泄露栈地址与泄露canary思路是一样的,可以用格式化字符串的任意写和C字符串的判定方式来泄露存储在栈中的rbp
,这里已经有了格式化字符串漏洞,因此我们可以在payload1
加上一个格式化字符来多泄露一个rbp
。
gdb
是个好东西,用它来调试一下。因为我们的rop
链是执行到welcome
的那个地方才是栈底,所以我们泄露那边的rsp
那边的rbp
刚好是我们新的welcome
函数的栈底,讲的通俗点上面就是canary
了。
所以我们的payload和对应的接受输出的措施就可以改成
1 | payload=b'%27$p %31$p' |
然后我们就可以在下一次的payload
里面输入flag
字符串并且根据地址去引用它。首先还是老样子把canary
泄露出来,然后open
函数去打开flag
文件。这里我的做法是把flag
输出到了后面,但其实可以在前面就把flag
字符串放上去,那样偏移还是固定的,而放在后面的话还得根据rop
链的长度判断地址。但是思路是这么个思路,可以想一下万一缓冲区没给够,还得输出在后面的时候该用什么措施应对。这里有一个很好用的东西就是ljust
固定rop
链大小然后再去写。
这里我给了0xb0
的长度去写rop链,先一步步来。
1 | payload=payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)*2+p64(pop_rdi)+p64(rbp+0xb8)+p64(op) |
这一部分输入之后gdb
调试看看能不能成功打开flag
文件。
可以看到已经成功把rdi
的值变成了flag
字符串了,后面一步也是直接执行了open
函数。接着调试发现open
返回值为3,说明该文件描述符为3,等一下read
里面的fd
参数就应该给3了。因为这里有三个参数,前两个寄存器的gadget很容易找得到,第三个rdx相关的gadget是死活找不到。这样的话有三种应对措施
①ret2csu,这个方法套一下模板和容易就可以执行了这个read。
②去libc中找到rdx相关的gadget,这是官方放出的wp的思路。
③我另辟蹊径,开辟出第三种方法,这个是一个小技巧,可以记一下:在调用strcmp
函数的时候,rdx的值会变成两个字符串中第一个不同字符的第二个字符串对应位置的ascii
值。举个栗子,如果我调用strcmp(“aaa”,”abc”)的话,结束的时候rdx的值为’b’。
这里我用了第三种方法。那么接下来的payload就是这样:
1 | payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(op) |
继续运行下去可以发现read成功地把我本地的flag文件读到了栈上面
最后一步应该很简单,就是把那个地方的字符串puts出来,因为在调试器里面我们能看到这个flag,但是打远程的时候我们肯定要输出出来才能看到这串flag。
1 | 、payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(op) |
怎么说呢?难是很难,但是确实收获很多,如果想试试这题的师傅们可以直接联系我qq找我要。
exp
1 | from pwn import* |