0x00 前言
这是一道很有学习价值的题目。通过这道题目,我学到了DynELF模块的用法、一些ROP技巧等。
感谢大佬们开发的各种工具、解锁的各种姿势,愿我也能站在巨人的肩膀上。
0x01 分析
1 | Arch: amd64-64-little |
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
sub_40063D
函数就相当于一个read函数,不过必须读够200个字节且每次读一个字节,这里存在明显的栈溢出。v1
距离rbp
只有0x40个字节,但是却读入200个字节。
- 由于程序里面并没有后门函数且题目并没有给我们libc,这就要求我们想办法获取目标系统的libc。
关于远程获取libc,pwntools在早期版本就提供了一个解决方案——DynELF类。DynELFl的官方文档见此:
其具体的原理可以参阅文档和源码。简单地说,DynELF通过程序漏洞泄露出任意地址内容,结合ELF文件的结构特征获取对应版本文件并计算比对出目标符号在内存中的地址。
- 所以总体思路就是先通过
DynELF模块
拿到system
函数的地址,然后想办法执行system("/bin/sh");
。
0x02 通过DynELF模块泄露出system函数的地址
DynELF模块需要我们提供一个
leak方法
,而且需要程序里面有能够调用的输出函数,如write、puts、printf
。其实DynELF模块就相当于循环得出任意地址里的内容的一个工具。每次由这个模块自动传入一个地址,然后多次调用
leak(addr)
去泄露这个地址里面的内容,通过泄露的内容与ELF文件的结构特征比对,从而获得对应的libc
版本,进而能获得我们的目标函数(我们搜索的函数)所在地址。
构造传统的payload
基于这个思想,我们先来写一个通过溢出,让程序通过
puts函数
输出指定地址里的内容的payload。由于
puts函数: int puts(const char *s)
只有一个参数,这个参数就是我们所要输出的内容所在的地址,64位程序的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,第七个及以后的参数才保存在栈中
。所以puts函数
的参数需要保存到RDI
中。所以我们需要先通过
ROPgadget
找到pop rdi;ret
的地址。如果不太清楚为什么这么做的话,可以看我的另一篇博客,里面有很详细的讲解及调试。
具体命令为:
ROPgadget --binary pwn100 --only 'pop|ret' | grep rdi
。得到结果0x0000000000400763 : pop rdi ; ret
。有了
pop rdi ; ret
以后,我们就可以任选一个想输出其内容的地址来构造我们的payload了。
1 | from pwn import * |
由传统payload转成leak方法
- 理解了这个payload的原理以后,我们就可以去构造我们需要的
leak方法
了。
小难点1
前面我们也提到过了, 我们的
leak方法
必须能够多次被调用。能被多次调用也就意味着我们的栈空间要跟第一次调用的时候基本保持一致,如果不一致,最先导致的一个问题就是参数传递问题,可能在传递和调用过程中,栈空间就被消耗完了。
所以这里我们把调用完
puts
以后的返回地址修改成start
,这个函数在IDA里面能够看到。
这是一段编译器添加的代码,用于初始化程序的运行环境后,执行完相应的代码后会跳转到程序的入口函数
main函数
去运行程序代码。
- 也就是说,我们通过每次返回到
start函数
就能实现每次相当于重新加载程序的效果。
小难点2
- 由于这道题目我们能够利用的
puts
函数的输出长度是不受控制的,它遇到\x00
才会停止输出。所以我们需要自行添加上\x00
。
leak方法
- 克服了几个小困难以后我们得到
leak方法
。
1 | # -*- coding: UTF-8 -*- |
- 仔细想一想,我们可以发现,这个
leak方法
其实就是一个模板, 我们只需要提供溢出payload然后稍作改动就行,大体代码不变。
泄露system函数地址
- 我们有了
leak方法
以后还需要实例化一个DynELF对象,用这个对象去泄露我们的system函数地址
。
1 | d = DynELF(leak, elf = elf) |
这样我们就能够拿到
system函数地址了
。所以泄露
system函数
的完整代码为:
1 | # -*- coding: UTF-8 -*- |
- 运行可得
system_addr
。
- 从运行结果来看,我们也能明白
DynELF模块
大致的工作原理。
0x03 ROP
获得/bin/sh
有了
system函数
以后,还需要有它的参数/bin/sh
。我们可以找到一个具有读写权限的地址,写入
/bin/sh
。通过
vmmap
命令或者在IDA里面找bss段
等都可以找到合适的地址。
我们发现
0x601000
到0x602000
之间的地址都具有读写权限。我们任选一个地址
binsh_addr = 0x601500
用于写入/bin/sh
。我们需要构造ROP链来实现写入
/bin/sh
。由于我们的
read函数: read(int fd, void *buf, size_t count)
,有三个参数,所以我们采用的gadgets为:gadget1(0x40075A)
:
1 | .text 000000000040075A pop rbx |
gadget2(0x400740)
:
1 | .text 0000000000400740 loc_400740: |
- 这是
__libc_csu_init函数的通用gadget
。通过这两段gadget,我们能发现这么一个关系。
1 | r13 == rdx == arg3 |
- 所以得到。
1 | .text 000000000040075A pop rbx 必须为0 |
1 | .text 0000000000400740 loc_400740: |
- 从而我们的ROP链为:
1 | payload='A'*(0x40 + 8) |
上方payload中有一处
payload+='\x00'*56
, 这里相当于计算了一下RSP
与RIP
的偏移。由于我们的
gadget2
中在调用完call qword ptr [r12+rbx*8]
后,RSP
是指向ret_addr
的,然后我们会执行这一系列指令。
1 | .text:0000000000400756 add rsp, 8 |
首先
add指令
导致rsp += 8
, 然后有6个pop
,每次pop
都会导致rsp += 8
,也就是rsp
一共加了(1+6)*8 = 56
个字节。所以我们要在距离调用完
call qword ptr [r12+rbx*8]
后的56
个字节的位置放置我们真正的ret_addr
。
get shell
- 剩下的就是另一个ROP链了:
1 | payload = "A"*(0x40 + 8) |
- 所以我们完整的exp为:
1 | # -*- coding: UTF-8 -*- |
0x04 参考链接
- 感谢各位师傅。