RedHat 2017-pwn1
1.常规 checksec查看一下,发现开启了NX,IDA打开程序找漏洞,变量V1的首地址为bp-28h,即变量在栈上。而之后还有__isoc99_scanf不限制长度的函数,所以输入会导致栈溢出。这样就可以寻找system和”bin/sh”来getshell了。
1 | #注释头 |
2.首先ctrl+s看看.got.plt中有没有system函数,这里有。找到system函数后,再寻找”/bin/sh”,但是找不到,所以考虑从__isoc99_scanf来读取”/bin/sh”来写入到内存进程中。
3.接下来考虑字符串”/bin/sh”应该放到哪里,因为可能会有ASLR(地址随机化)的影响,所以最好找个可以固定的内存地址来存放数据。ctrl+s查看内存页后可以看到有个0x0804a030开始的可读可写的大于8字节的地址,且该地址不受ASLR影响,所以可以考虑把字符串读到这里。(可以看到有R和W权限,但我也不知道怎么看该地址受不受到ASLR的影响,可以按照以前的做法,这里可以将该地址修改为某个extern段的地址,因为延迟绑定之后,这个段中的内容就基本没用了,这里选择这个段上的某个地址一样可以getshell,我选的是0x0804A050。)
4.既然程序读取用的是__isoc99_scanf,那么参数”%s”也得找到,容易找到位于0x08048629。
5.先编写rop链测试一下:
1 | #注释头 |
这里”A”*0x34有点不一样,我们可以看到在该函数中声明的局部变量v1距离栈底有0x28,那么main函数的返回地址应该是0x28+0x04=0x2c才对。但是实际上,由于程序最开始的动态链接,是从start开始初始化main函数栈的,所以经过start函数会给main函数栈上压入两个全局偏移量。通过调试也可以看到,输入AAAA,位于FFFDF568,加上0x28应该等于FFFDF590,但是这里却不是ebp,得再加上两个0x04才是ebp的位置。这是因为在程序运行起来的延迟绑定的关系,压入栈的是全局偏移。不过不用管,没啥用,这里直接再加上两个0x04就好了,通过调试也可以调试出来。而且查汇编代码,发现寻址方式是通过esp寻址,也就是[esp+18h],FFFDF550+0x18=FFFDF568,也就是我们输入的地方。
6.程序运行到这没什么问题,但是接着运行下去从由于我们覆盖的是main函数的返回地址,让main返回地址返回到scanf中,执行的是return命令。而再次进入到scanf函数中之后,执行:io.sendline(“/bin/sh”)。发现binsh并没有被读入到binsh_addr中,这是因为scanf读取输入时的汇编操作如下:假设为scanf(“%s”,&v1);
1 | #注释头 |
栈的分布如下:
1 | #注释头 |
而我们直接return scanf的栈分布如下:
1 | #注释头 |
scanf在执行过程中,由于我们没有将scanf的返回地址压入栈中,所以第一个读取的是esp+2,将我们需要输入的binsh的地址当作了格式化参数%s来读取,发生错误。之后scanf也没办法正常返回
8.所以我们用main函数的return来调用scanf时,需要给栈布置一个scanf的返回地址,否则scanf执行过程中会读取参数发生错误,不能正常读取和返回。
9.那么第一次的shellcode顺序应该是‘A’*0x34 + scanf_addr + scanf_returnaddr + format_s + binsh_addr。
1 | #注释头 |
之后大多有两种解决方案:
▲第一种:将scanf返回到main,再次执行栈溢出:
也就是将scanf的返回地址设置为main函数的地址,scanf出来之后,回到mian中之后,第二次的shellcode应该是’A’0x2c +system_addr + system_ret_addr + binsh_addr。这里的system_addr和上述的scanf中是一样的,都是为了防止函数读取参数发生错误从而无法正常执行。但是这里的system_ret_addr可以随便填,因为我们并不需要返回system,进入到system之后执行binsh就能getshell了。而’A’2c是因为栈的状态发生了改变,所以需要重新计算一下。因为再次进入main函数构造出来的Main函数栈应该是0x40,而不是之前0x48这么大了,没有经过start函数初始化main函数栈,不存在压入的全局偏移,系统只是将这次的main函数当作一个普通的函数来构造栈。
所以这一次我们输入的内容距离栈底就确实只有0x28这么远了,那么计算一下0x28+0x04=0x2c,所以这一次的padding就是0x2c。
1 | #注释头 |
▲第二种:将scanf的返回地址拿来做文章,通过rop将esp直接下拉两个0x04到达我们输入的system,然后在从之后的地方读取binsh字符串,一次payload直接搞定:
通过汇编代码可以看到,调用scanf时的栈状态应该跟下图一样:
所以我们scanf函数返回时esp应该还是指向的format参数地址才对,那么为了将esp下拉两个0x04,到达输入的system函数地址,就需要两个Pop操作,这里通过ROPgadget可以查出来,或者直接从init什么的初始化段中找万能gadget,同样存在多个Pop操作。那么这样的化就只有一次payload,所以总的payload就应该是:
1 | #注释头 |
▲这里再给出第三种方案,也比较容易理解
这个方案是基于第一种的,覆盖scanf返回地址为start函数,这样main函数栈又重新初始化,相当重新执行一次程序,那么第二次的shellcode的padding字符个数还是0x34个A,之后就常规覆盖eip跳转system函数getshell了。但是这里直接写start函数的首地址会出错,因为这里的start首地址为0x08048420,末尾为20,转化成字符串就是空格。而读入我们输入的又是scanf,scanf不支持空格录入,所以遇到空格就会发生截断,导致读不进去。而这里又是因为大端序,如果发生0x08048420,那么先发送的字符是0x20,也就是空格,那么就直接截断,之后所有数据都读不了了。所以这里如果需要传入start函数,则将start函数下拉两个字节,传入0x08048422。看汇编代码:
start函数体的第一条汇编指令是xor ebp,ebp。异或操作,就是将ebp清理好初始化而已,啥用也没有,所以可以直接跳过,到pop esi就行。具体代码就是将第一种方案的种第一段shellcode的main_addr改成start_addr+0x02,然后偏移都是0x34就行。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157