漏洞实验2—ROP技术基础
实验2:ROP技术基础
1. 实验条件
Linux虚拟机(Kali)
源文件(rop1.c)
1
2
3
4
5
6
7
8
9
void vuln(){
char buf[128];
read(0,buf,256);
}
int main(){
vuln();
write(1,"hello rop\n",10);
}
2. 实验原理
点击查看
ROP (Return Oriented Programming),主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。可以将gadget形象的比喻成搭积木的过程,将散落在各地址的所需操作和数据收集起来,通过拼接已达到shellcode的效果。
ROP攻击需要满足的条件:
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets以及相应gadgets的地址。
3. 实验要求
- 针对实验一,通过gdb调试rop1,确定shellcode的地址;此外,通过rop1.py的调试脚本确定shellcode地址;最终拿到shell权限。相关详细分析过程写入报告,并比较两种方法的特点。
- 针对实验二的32位环境和64位环境,通过调试分析,完成实际rop2.py和rop3.py(预留有空白和错误之处),最终拿到shell权限。相关详细分析过程写入报告
4. 实验步骤
4.1 程序流的劫持
关闭ALSR地址空间随机化。其开启会导致同一应用多次执行所使用的内存完全不同。
1 | sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" |
对rop1.c
进行编译,并关闭栈保护和NX保护。
1 | gcc rop1.c -o rop1 -m32 -fno-stack-protector -z execstack |
-fno-stack-protector
为关闭Canary保护,确保栈溢出可执行,-z execstack
为打开栈的可执行权限,-no-pie
关闭编译器地址随机化,-g
是加入调试信息,让编译后的二进制文件启用调试,-o
表示输出源文件的名称。
出现以下报错:
通过查询发现其原因:缺少相应的32位库文件导致的。解决方法为运行以下命令后,再次在64位平台上编译32位应用程序。
1 | sudo apt-get install gcc-multilib |
之后再次编译,出现以下报错:
解决办法为在rop1.c
加入头文件#include<unistd.h>
,再次编译即可通过。可能还会如上图出现warning的报错,其实不是error可以直接无视,不影响实验。
安装checksec
,用其进行确认机制是否关闭。可以看到栈保护和NX保护已经关闭,另外如下图也可以了解该可执行文件为32位小端存储。另外checksec使用的参数可能因版本不同而不同,老版本的文件引用直接写文件名即可。
1 | apt install checksec |
安装pwntools模块,其集成了pwn中所需要的使用的功能模块。因为使用Kail搭建的环境,如果需要安装python2版本的pwntools,需要先安装pip2
。
1 | pip install pwntools |
运行gdb,用其pwndbg插件对可执行文件rop1进行分析。disass vuln
为查看vuln函数的汇编指令。当然也可以拖入ida对其进行静态分析。
1 | gdb rop1 |
通过分析可以大概画出栈的结构。
如图可看出,payload中除了shellcode外要填充足够长度以覆盖返回地址。buf数组的地址为ebp-0x88
,即距离ebp有0x88
字节,ebp距离返回地址又0x4
字节(32位),要覆盖返回地址前需先填充0x8c
个字节。
之后,需要将返回地址覆盖为执行'/bin/sh'
shellcode的地址,从而获取shell。因此接下来需要寻找或创建执行'/bin/sh'
shellcode的存储地址,并将其地址写入payload中。编写python脚本。
1 | # coding=utf-8 |
- 脚本中利用pwn中的shellcraft模块生成shellcode,其中shellcraft.sh()为生成执行的
/bin/sh
的shellcode,再利用asm()把shellcode转换成机器码。 ljust(0x8c,'a')
表示向shellcode左对齐后,向后填充字符a
直到达到0x8c长度。p32()
会将数字转换成字符,如p32(0xdeadbeef)->'\xef\xbe\xad\xde'
。
然而在终端进行gdb调试程序,查看内存得到的地址和真正执行程序的地址是不一样的,gdb的调试环境会影响buf变量在内存中的地址。因此提供了两种获取真实地址的方法。
方法一:开启core dump(用命令可开启)
1
gdb rop1 core
此时pwndbg会运行到返回地址结束位置,因返回地址找不到而中断。即为esp = buf + 0x8c + 0x4的位置,因此buf的真正地址为esp-0x90。
当然也可以
i r
获取当前esp寄存器为0xffffd130,做运算后也可得0xffffd0a0
。之后改正脚本中buf的地址运行,即可获取shell。
方法二:在脚本中进行gdb调试获取buf数组真实地址
首先,在python脚本中pause()阻塞停止函数。
然后运行脚本,如下图返回了pid进程号,并且程序发生阻塞。
再打开一个终端,
gdb
进入pwndbg,并输入attach 382891捕获该进程,返回之前程序阻塞的终端输入任意键继续,再pwndbg中可n继续执行程序。当运行到返回地址处找不到
0xdeadbeef
处,出现报错。此时可以看到ecx寄存器地址上的内容正是shellcode,与core方法中的地址也符合。之后同core方法一样,更改脚本0xdeadbeef为正确的地址,运行即可获取shell。
4.2 ROP绕过NX保护
在实验一中将shellcode通过不安全的read函数写入栈中,并利用返回地址找到并运行该段内容。而在实验一的基础上开启NX保护,意味着栈空间内容不再具有可执行的权限,但是程序中用到了libc
库中的read和printf函数,libc.so
中保存了大量执行函数,因此可以考虑调用其中的内容拼凑成system('/bin/sh')
来获取shell。
实验二将会分别分析32位和64位程序。因为32位和64位存在参数传递上的区别,32位使用栈帧来作为传递参数的保存位置,64位使用rdi,rsi,rdx,rcx,r8,r9寄存器保存第1-6个参数,rax作为返回值。32位用ebp作为栈帧指针,64位没有栈帧的指针,64位取消了这个设定,从而rbp作为通用寄存器使用。
4.2.1 32位
编译rop1.c
文件,只关闭了Canary栈保护,并通过checksec验证。
1 | gcc rop1.c -o rop2 -m32 -fno-stack-protector |
首先,获取system函数地址,因为关闭了ASLR,system函数在内存中的地址不会发生改变。
1 | gdb rop2 |
之后,在pwndbg中vmmap查看程序堆栈结构,并在libc.so
的栈地址范围内寻找字符串/bin/sh
。
1 | vmmap |
1 | find 0xf7dc2000,0xf7fae000,"/bin/sh" |
通过上述过程可以得到:vuln函数返回地址应该写入system函数地址0x7e07160
,system函数参数”/bin/sh”地址应写入0xf7f51924
。
最后,编写python脚本。
1 | from pwn import * |
其中,'a' * 0x8c
同实验1一样是为填充buf申请的内存空间和ebp返回地址。p32(sys_addr)
为vuln返回地址去找system函数地址,p32(0xdeadbeef)
为system函数返回地址,填一个无效地址即可,因为获取shell后没有其他操作了,p32(binsh_addr)
为system的参数地址,去找/bin/sh
拼凑。
4.2.2 64位
编译rop1.c
文件,只开启Canary栈保护,并通过checksec验证。
1 | gcc rop1.c -g -o rop3 -m64 -fno-stack-protector |
因为参数不会向32位一样放在栈上,需要寻找类似于pop rdi;ret
的gadget,将参数从栈中弹出到rdi寄存器后,返回到返回地址处继续执行。至于为什么要找尚未所说样式的gadget,见参考文章。
该实验将在栈中事先压入参数'/bin/sh'
地址和system地址。首先同32位实验一样,找到系统中system函数地址以及'/bin/sh'
存储地址,分别为0x7ffff7e35e10
和0x7ffff7f7569b
,可以看出与32位系统中的地址值不同。
借助ROPgadgets工具查找gadget,在libc.so中查找可用的gadgets。先确定rop3使用的共享库列表。
ROPgadgets查找结果如下,其中0x277d5
是相对于libc.so的偏移。其指令查找对rdi寄存器操作的pop或ret指令。
1 | ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"|grep rdi |
用之前vmmap获取的libc.so首地址(通过上文vmmap得到的首地址)加上这个偏移,就得到了最终的地址。需要注意的是64位系统编译出的buf地址也发生了改变,需要再次查看。
之后编写python脚本。
1 | from pwn import * |
其中,'a' * 0x88
是为填充buf申请的0x80内存空间和ebp返回地址,因为是64位所以返回地址是0x8大小。p64(pr_addr)
为vuln返回地址去找pop rdi;ret
指令的地址,p64(binsh_addr)
为弹入rdi中的字符串地址,p64(sys_addr)
为ret返回地址,system函数返回地址p64(deadbeef)
,因无后续操作填写无效地址。最后运行脚本获取shell。
5. 实验总结
这个实验欠了得有两周,整个实验做下来是非常坐牢的,个人感觉pwn确实是很难入门,需要大量的知识储备、对寄存器,堆栈空间的想象能力以及面对陌生的工具集和指令。很多地方跟着老师ppt可以很简单的做下来,但是大部分地方不知所以然,需要去查资料、看视频来学基础知识,扣每个细节。