Pwn学习记录——基本ROP-2
题目来源 CTF Wiki
ret2libc
ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。
关于got和plt的介绍:介绍got plt以及libc.so
ret2libc1
首先检查程序的安全保护
程序只开启了栈不可执行保护,下面丢到IDA里面看一下程序源代码
可以看到在执行 gets 函数的时候出现了栈溢出,Alt+T
搜索发现跟ret2text一样具有system函数,但是不再是可以直接执行的system(”/bin/sh“)
了
要使用system函数,需要将程序跳转到plt处,找到地址为0x08048460
所以需要我们手动将参数 /bin/sh
传入进去,使用ROPgadget搜索
1 | ROPgadget --binary ret2libc1 --string '/bin/sh' |
正好存在,那么我们就可以控制程序直接返回到system函数,然后参数为/bin/sh
获得shell
下面我们构造payload,经分析后无效数据填充的长度跟之前一样,还是0x70,然后在返回地址处覆盖system的地址
1 | from pwn import * |
这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以’bbbb’ 作为虚假的地址,其后的数据才对应为函数的参数。
注意:这里只有在plt表中调用system函数时才需要填充数据,如果使用的是程序中的call调用system函数则不需要填充数据
ret2libc2
checksec和丢到IDA里面查看后可以发现,程序跟ret2libc1基本一样,只是搜索不到字符串/bin/sh
那么该怎么办呢?只能我们手动将字符串输入了。所以我们需要控制程序跳转两次,一次跳转到gets函数读取字符串(参数)/bin/sh
到内存中,另一次跳转到system函数执行获取系统shell。
那么问题来了,gets读取的数据要保存到哪里呢。我们要寻找一块可读可写的buffer区,通常会寻找.bss段,在IDA里面查看可看到在.bss段里面存在一个buf2数组,地址是0x0804A080
我们检查一下这一块数据是否可读可写
接下来我们就需要考虑payload的构造了,先来搞清楚溢出覆盖后栈的情况
为了控制esp指针在gets函数执行完毕后可以向上移动(esp+4),在gets的返回地址填入pop ; ret
指令,这样在执行system函数的时候,esp指针指向无效数据’aaaa’,buf2作为system的参数传入
使用ROPgadget工具找到可用的gadget
这里我选择pop ebx ; ret
作为利用的gadget,当然pop ebp ; ret
也可以,但是一般不建议对esp、ebp的值进行修改(为了避免程序寻址错误)。
gets函数和system函数的地址可以在IDA中查找 plt 表
1 | from pwn import * |
-------分割线------
这里还有另外一种更简洁的覆盖方式,但是要在理解上面payload构造方法的基础上才能明白。
这是第二种方法的栈覆盖结构,具体原理跟上面一样,但是省去了寻找pop ; ret
这种gadget的麻烦。构造的payload如下
1 | from pwn import * |
可以看到,同样也可以获得shell
ret2libc3
题目分析
同样,先检查程序,依旧是只开启了栈不可执行的32位程序,跟之前的一样,但是IDA查看会发现,这次system函数和/bin/sh都不存在了。
那么我们该如何得到system函数的地址呢?利用libc.so动态链接库。
在libc动态链接库中的函数之间的相对偏移是固定的,我们可以通过got表,泄露出某个函数的地址,然后再到libc中查找system的地址进行计算。这里有一个libc的查找工具可以使用:
这个工具使用的数据库是在github上niklasb大佬整理维护的,传送门
查看IDA中的程序源代码
我们可以看到puts函数可以用来泄露输出已经执行过的函数的地址,这我们可以输出puts函数在got表中的地址,当然我们也可以选择输出 __libc_start_main
函数的got表地址,这个函数基本存在于所有的程序当中,它是程序最初被执行的地方。
利用思路
- 执行程序泄露 __libc_start_main 地址,并且返回地址设置为 _start() 或者 main(),以便于下面再次执行程序
- 选择 libc 版本
- 通过接收puts函数输出的地址,计算libc中的地址与真实地址的偏移量,从而获得 system 地址与 /bin/sh 的地址
- 再次执行源程序
- 利用计算得到的libc中的system函数地址和/bin/sh的地址,触发栈溢出执行 system(‘/bin/sh’)
payload编写
在第一次栈溢出用puts函数的地址覆盖函数返回地址时,puts函数的返回地址可以设置为_start()或main()函数地址。
返回地址为_start()函数
1 | from pwn import * |
这里我们泄露的是__libc_start_main
函数的地址,若要泄露puts函数的地址,大同小异,稍微修改一下exp脚本即可
脚本在执行的时候,可能会检测到多个可用的libc,需要进行选择,我们选择其中一个即可
返回地址为main()函数
先将_start()换成main(),payload填充字符的偏移量不变,运行脚本会发现程序并不能正确获取到shell,添加GDB调试之后发现溢出多了8个字节的数据
所以我们对偏移量减少8个字节即112-8=104就可以成功溢出获得shell
1 | from pwn import * |
_start()和main()的区别
简单地说,main()函数是用户代码的入口,是对用户而言的;而_start()函数是系统代码的入口,是程序真正的入口。
我们可以看下本题的_start()函数内容,其包含main()和__libc_start_main()函数的调用,也就是说,它才是程序真正的入口:
为啥会多出8个字符?
主要是因为我们两次设置不同返回地址函数的区别,即 _start() 和 main() 的区别, _start() 为程序的真正的入口、执行一定代码后才会进入main()执行,而当我们覆盖返回地址为main()函数地址时,实际上并不为程序原本的入口,但计算出的偏移量是基于程序入口计算的,因此payload的偏移量实际会偏小;至于为何刚刚好是8个字节,这个得深入调试分析 _start() 到 main() 之间的逻辑才能搞清楚,一般都是通过调试得知偏移差多少就可以做出题了。
在CTF-Wiki中的官方exp用的是main()作为返回地址,而且第二次payload直接就使用了104作为偏移,这令我百思不得其解。感谢 Mi1k7ea 大佬提供的思路为我解决了这个困惑~~
练习:train.cs.nctu.edu.tw: ret2libc
拿到题目先检查程序保护
老样子,32位程序,只开启了NX保护。丢到IDA里
可以看到,程序存在scanf溢出漏洞,并且程序运行的时候还打印出了/bin/sh
和puts
的地址,不禁微微窃喜
多次运行程序发现,字符串/bin/sh
的地址是不变的,一直是0x0804a02c
题目给了我们libc文件,那么我们就可以参考ret2libc3的思路,通过puts函数泄露libc中的地址,从而计算得到system的地址,与已知字符串/bin/sh的地址进行拼接便可溢出执行shell
如若是remote方式执行脚本,我们可以令libc为下载的libc.so.6文件,本地执行的话需要我们找到在本地与程序相链接的libc文件,使用lld
命令进行查看
这个程序在本地链接的libc的路径为/lib32/libc.so.6
**注意:**使用本地提供的libc文件就不能使用LibcSearcher这个工具进行查找,这里我们使用的是pwntools的ELF库
根据EBP和ESP的地址我们可以计算出偏移量:158-(120+1c)=0x20
exp如下:
1 | from pwn import * |