0x00 基础知识
触发格式化字符串漏洞的函数有限,主要就是printf,sprintf.fprintf等print家族函数,就用printf为例来记录一些相关的基础知识。
printf()函数的一般形式是printf("format",输出项)
,其中format是格式化参数,看一下它的结构吧:%[标志][输出最小宽度][.精度][长度]类型
,其中跟格式化字符串漏洞有关系的主要有以下几点:
1、输出最小宽度:用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。
2、类型:
- %c:输出字符,配上%n可用于向指定地址写数据。
- %d:输出十进制整数,配上%n可用于向指定地址写数据。
- %x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
- %p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
- %s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
- %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
- %n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据
以上是需要关注的重点
0x01 漏洞的触发
printf()函数正常情况下的使用应该类似像这样:
1 | char str[100]; |
但是要是一不小心,可能会变成这样:
1 | char str[100]; |
那么,因为我们输入的参数是可控的,如果我们输入的参数中结合format,那么我们就能进行相应的内存读取和写入
先把ASLR关了:
1 | root@mask-virtual-machine:/home/mask/format_pwn# echo 0 > /proc/sys/kernel/randomize_va_space |
第一个程序format.c
程序源码:
1 | #include <stdio.h> |
然后编译(关闭了栈保护)
1 | gcc -fno-stack-protector -o format format.c |
运行:
1 | mask@mask-virtual-machine:~/format_pwn$ ./format |
运行正常,现在修改一下format参数,多加一个%x:
printf(“%s %d %d %d %x\n”,buf,a,b,c);
再编译运行:
1 | mask@mask-virtual-machine:~/format_pwn$ gcc -fno-stack-protector -o format format.c |
编译有警告,但是依然通过了,输出结果:
1 | mask@mask-virtual-machine:~/format_pwn$ ./format |
我们知道,这个%x的格式化参数后面并没有与之对应的输出项,但是依然可以输出数据,那么这个输出的数据在哪?它是什么?我们现在开始对这个程序进行调试分析:
1 | Guessed arguments: |
这是运行printf时的堆栈布局,可以看到b7fbb000
即为四个输出项的后一个栈中的数据,那么我们再加一个%x,是不是就能输出再下一个0xb7fb9244
为了验证我们的想法对不对,再次修改程序
1 | printf("%s %d %d %d %x %x \n",buf,a,b,c); |
然后编译运行:
1 | mask@mask-virtual-machine:~/format_pwn$ gcc -fno-stack-protector -o format format.c |
那么只要我们能够控制输入的format参数,那么我们就能一直读取内存数据
读完了,我们来看看怎样通过%n写数据
第二个程序format2.c
源码:
1 | #include <stdio.h> |
编译运行:
1 | mask@mask-virtual-machine:~/format_pwn$ ./format2 |
这样就实现了改写数据
0x02 实例分析
例子是2016CCTF的pwn3,正好利用了格式化字符串漏洞写数据
先分析源码:
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
发现有三个主要的功能函数,put_file
,show_dir
,get_file
然后还有登录函数ask_username
和ask_password
跟进查看:
1 | char *__cdecl ask_username(char *dest) |
把你输入的用户名字符串中的每个字符的ascii码+1
1 | int __cdecl ask_password(char *s1) |
等于”sysbadmin”就输出“welcome”并返回继续,不然就终止程序,那么用户名是“sysbdmin
”字符串中每个字母的ascii码减1,即rxraclhm
;
然后运行一下程序:
1 | mask@mask-virtual-machine:~/format_pwn/CCTF$ ./pwn3 |
用ida分别查看那三个功能函数的伪代码,可以发现,get_file函数中存在格式化字符串漏洞:
1 | int get_file() |
那么我们就可以利用这个漏洞以“/bin/sh
”为参数调用system,也就实现了getshell
思路是:
- 先读取
puts@got
的内容,得到puts的地址,之后通过lib中偏移量固定的方式算出system的地址 - 将system地址写到puts@got里
- 让程序去执行puts(‘/bin/sh’), 这时实际是执行system(‘/bin/sh’)
下面就开始调试分析:
1 | mask@mask-virtual-machine:~/format_pwn/CCTF$ gdb ./pwn3 |
开了NX
第一步,我们需要找到system
的函数地址,这里我们需要通过格式化漏洞泄露出libc的地址然后去查询对应的版本
首先通过IDA可以知道printf的地址:
1 | .text:08048895 lea eax, [ebp+dest] |
那么我们下断点 :
1 | gdb-peda$ b *0x0804889E |
运行:
1 | [----------------------------------registers-----------------------------------] |
然后stack 100查看栈上的内容,可以发现:
1 | gdb-peda$ stack 100 |
可以看到在栈上第91个位置存在libc_start_main+247,即libc_start_main_ret的地址,那么构造printf("%91$p")
即可以将该地址的值泄露出来:
1 | mask@mask-virtual-machine:~/format_pwn/CCTF$ ./pwn3 |
验证了确实和调试时栈上显示的一样,那么
__libc_start_main
的地址为:0xb7e21637-f7(16进制的247) = 0xb7e21540
这里使用一个工具libc-database,非常方便查找libc版本
1 | mask@mask-virtual-machine:~/libc-database$ ./find __libc_start_main 0xb7e21540 |
然后非常喜欢这个dump功能:
1 | mask@mask-virtual-machine:~/libc-database$ ./find __libc_start_main 0xb7e21540 |
很赞!!
那么system_addr
= __libc_start_main_ret
-0x18637
+offset_system
= 0xB7E43DA0
验证一下:
1 | gdb-peda$ print system |
然后就是要吧system_addr写到put地址处
1 | mask@mask-virtual-machine:~/format_pwn/CCTF$ objdump -R pwn3 |
1 | pwn3: 文件格式 elf32-i386 |
1 | DYNAMIC RELOCATION RECORDS |
然后开始编写exp:
1 | #!/usr/bin/env python |
1 | elf = ELF('pwn3') |
运行:
1 | mask@mask-virtual-machine:~/format_pwn/CCTF$ python exp.py |