ROP
1. 概述
1.1. 程序流劫持(Control Flow Hijack)
通过程序流劫持(如栈溢出,格式化字符串攻击和堆溢出),攻击者可以控制PC指针从而执行目标代码。
1.2. 系统防御
为了应对程序流劫持,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。
1.3. ROP的概念
ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术,可以用来绕过现代操作系统的各种通用防御。
2. 准备工作
2.1. 设置coredump
可以防止出现程序在GDB调试环境下的内存地址与实际运行环境下内存地址不同的情况。
1 | # 开启coredump |
2.2. 解除安全措施的方法
- GCC编译选项:
-fno-stack-protector,用于关闭栈保护 - GCC编译选项:
-z execstack,用于关闭DEP - shell指令:
echo 0 > /proc/sys/kernel/randomize_va_space,用于关闭ASLR,设置为2为启用ASLR - GCC编译选项:
-no-pie,用于关闭PIE(程序基址版本的ASLR)
2.3. 将程序绑定到端口:SOCAT
socat TCP4-LISTEN:10001,fork EXEC:./level1,SOCAT可以将目标程序作为一个服务绑定到服务器的某个端口上- xinetd
2.4. 32位库
如果要在64位环境下调试32位程序,需要安装32位相关的库函数:
1 | sudo dpkg --add-architecture i386 |
2.5. shellcode
2.5.1. 32位
2.5.1.1. 21字节系统调用
1 | # shellcode最后的功能相当于execve ("/bin/sh") |
2.5.2. 64位
syscall
3. 工具
3.1. gadgets工具
- objdump(可用于寻找简单gadgets):kali Linux自带
- ROPEME: https://github.com/packz/ropeme
- Ropper: https://github.com/sashs/Ropper
- ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master
- rp++: https://github.com/0vercl0k/rp
3.1.1. ROPgadget
Kali Linux自带该款工具:
- ROPgadget –binary libc.so.6 –only “pop|ret” | grep rdi
- ROPgadget –binary libc.so.6 –string ‘/bin/sh’
3.2. pwntools
python库,可以极大的简化pwn的工作量。
- 寻找bin文件中的字符串:next(ELF(‘libc.so.6’).search(‘/bin/sh’))
- 获取bin文件中函数地址(该地址为展开后函数实际地址,一般用于计算函数偏移):ELF(‘libc.so.6’).symbols[‘system’]
- 寻找bin文件中plt表和got表中对应函数的地址(plt利用方式为
call/jmp addr_plt,got利用方式为call/jmp [addr_got]):ELF(‘libc.so.6’).plt[‘system’];ELF(‘libc.so.6’).got[‘system’] - 内存泄露:可用于获取函数地址,无法获取字符串
- 可以直接让GDB附加到程序上
- 可以直接编译汇编代码(需要指定平台类型如i386,
context.arch = "i386") - 可以通过
context.log_level = "debug"来输出debug日志
3.3. EDB
EDB调试器,Linux下的GUI调试器,对标OD。
3.4. objdump
- 查看可执行文件中的plt表:
objdump -d -j .plt level2 - 查看可执行文件中的got表:
objdump -R level2
3.5. GDB
print system:获取函数地址find 0xb7e393f0, +2200000, "/bin/sh":找到字符串(0xb7e393f0是通过print指令确定的__libc_start_main函数地址)
3.6. 其它
- checksec:检查程序的被保护情况
- ldd:可以查看目标程序调用的so库在哪里
- readelf:可以查看elf文件各个段的属性
4. 思路总结
4.1. 利用步骤
- 通过
checksec等手段检查保护情况:系统ASLR、POE、Canary、DEP - 判断漏洞函数,如gets、scanf、read等(注意:gets函数读取输入以换行符结束,read函数则指定了读取长度)
- 计算目标变量的在堆栈中距离ebp的偏移
- 分析是否已经载入了可以利用的函数,如system,execve等
- 分析是否有字符串/bin/sh,如果没有的话可以利用gets、read等函数写入.bss段(注意:gets函数读取输入以换行符结束,read函数则指定了读取长度)
4.2. 方案表
表格内容空白代表有或者无均无影响。
| 系统ASLR | PIE | Canary | DEP | system函数 | /bin/sh字符串 | gets等写入函数 | 其它条件 | 手法 |
|---|---|---|---|---|---|---|---|---|
| No | No | 程序中存在system(‘/bin/sh’)调用 | 返回到system(‘/bin/sh’)调用 | |||||
| No | No | 存在 | 程序中存在pop_pop_pop_pop_int的gadgets和/bin/sh字符串 | 利用gasgets进入系统调用 | ||||
| No | No | 导入 | 存在 | 返回到plt中的system函数,以/bin/sh字符串为参数 | ||||
| No | No | 未导入 | 存在 | 导入 | 程序中存在pop_ret的gadgets | 将/bin/sh字符串写入.bss段,利用pop_ret平衡堆栈,返回到system函数,以/bin/sh字符串为参数 | ||
| 不开启 | No | No | 利用库中的JMP ESP指令,返回栈中执行 | |||||
| No | No | No | 利用程序本体中的JMP ESP指令,返回栈中执行 | |||||
| 开启 | No | No | 开启 | 存在 | 存在 | 拥有目标程序so库 | 通过plt或got中的write函数打印so库中某些函数地址,通过偏移计算出system函数和/bin/sh字符串在内存中的地址 |
|
| No | No | No | 无目标服务器so库 | 通过read函数将/bin/sh字符串写入bss段(用于保存全局变量,地址固定,并且可以读可写) |
4.3. x64位的注意事项
- 内存地址范围由32位变成了64位,但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常
- 函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上
5. 通用gadgets
因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。默认gcc还会有如下自动编译进去的函数可以用来查找gadgets。
1 | _init |
5.1. __libc_csu_init
一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作。
1 | # encoding:utf-8 |
5.2. _dl_runtime_resolve
通过这个gadget可以控制六个64位参数寄存器的值,当我们使用参数比较多的函数的时候(比如mmap和mprotect)就可以派上用场了。
5.3. 一个Tips
另外,通过控制PC跳转到某些经过稍微偏移过的地址(会改变程序原来的汇编代码)会得到意想不到的效果。
6. 参考资料
- 蒸米的《一步一步学ROP》系列文章
- 《Linux PWN从入门到熟练》系列文章
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 凌虚的博客!