0%

格式化字符串漏洞

何为格式化字符串漏洞

1
2
3
4
5
6
7
#include<stdio.h>
int main(){
` char buf[16];
gets(buf,0x400);
printf(buf);
return 0;
}

形如上述代码的程序一般具有格式化字符串漏洞,试想如果往buf里输入%p等格式化字符串时会发生什么

那么细心的师傅们可能就会说了:你这后面也没跟第二个参数啊,这能运行吗?

答案是肯定的,并且打印出的地址就是栈上的地址,这就是格式化字符串最简单的用法:泄露栈地址

那么还有什么别的用途吗,且听我继续分析

格式化字符串函数介绍

在开始之前,我们先来介绍一下格式化字符串函数

格式化字符串函数

常见的有格式化字符串函数有

  • 输入
    • scanf
  • 输出
函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 。。。

格式化字符串

其基本格式如下

1
%[parameter][flags][field width][.precision][length]type

每一种 pattern 的含义请具体参考维基百科的格式化字符串 。以下几个 pattern 中的对应选择需要重点关注

  • parameter
    • n$,获取格式化字符串中的指定参数
  • flag
  • field width
    • 输出的最小宽度
  • precision
    • 输出的最大长度
  • length,输出的长度
    • hh,输出一个字节
    • h,输出一个双字节
    • ll, 输出一个八字节
  • 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。

参数

即对应要输出的变量

如何利用

那么基础打牢,开始实践吧

泄露内存

上文已经介绍过用%p泄露出栈的地址,那么如何泄露栈变量的值和对应字符串呢

要做到这些只需要将%p换成%x和%s即可

我们可以用%k$s这种形式的格式化字符串来获取偏移量为k的地址的数据,所以只要知道指定内存地址相对于参数位置的偏移就可以打印任意地址的数据了

覆盖内存

这里需要用到一个非常特别的字符串%n

1
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

这使得我们可以通过精妙的构造来向指定地址写入任意的值

可以使用pwntools中fmtstr中的神器fmtstr_payload

使用方法入下

1
fmtstr_payload(offset,{原始地址:目标地址})

关于偏移

这里主要说一下64位与32位程序的差异,64位程序是用寄存器传参,所以在计算偏移的时候要把寄存器数量考虑上,而32位程序就没这个顾虑了

大小数字覆盖问题

覆盖小数字

针对要写入的值小于偏移的值的情况,我们可以换一种方法来构造

我们可以将格式化字符串放在我们构造的数据的中间,将地址放在后面

1
payload = 'aa%8$naa' + p32(a_addr)

就像这样

覆盖大数字

我们需要用到一下两个标志

1
2
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。

我们可以将一个大数字进行分解,就像这样

1
2
3
4
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12

然后通过如下这种方法构造payload进行覆盖

1
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'

参考文章:

ctf-wiki