格式化字符串学习

多学点 没坏处

格式化字符串函数介绍

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分

  • 格式化字符串函数
  • 格式化字符串
  • 后续参数,可选

img

格式化字符串函数

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

  • 输入
    • 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,输出一个双字节
  • 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),栈上的布局由高地址到低地址依次如下

1
2
3
4
5
some value
3.14
123456
addr of "red"
addr of format string: Color %s...

注:这里我们假设 3.14 上面的值为某个未知的值。

在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况

  • 当前字符不是 %,直接输出到相应标准输出。
  • 当前字符是 %, 继续读取下一个字符
    • 如果没有字符,报错
    • 如果下一个字符是 %, 输出 %
    • 否则根据相应的字符,获取相应的参数,对其进行解析并输出

那么假设,此时我们在编写程序时候,写成了下面的样子

1
printf("Color %s, Number %d, Float %4.2f");

此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为

  1. 解析其地址对应的字符串
  2. 解析其内容对应的整形值
  3. 解析其内容对应的浮点值

对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。

这基本就是格式化字符串漏洞的基本原理了。

泄露内存

利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容。一般会有如下几种操作

  • 泄露栈内存
    • 获取某个变量的值
    • 获取某个变量对应地址的内存
  • 泄露任意地址内存
    • 利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
    • 盲打,dump 整个程序,获取有用信息。

泄露栈内存

给定如下程序

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}

然后,我们简单编译一下

1
gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c

然而并没有遇到warning。。。

1
2
3
4
5
┌──(root㉿kali)-[/home/kali/Desktop/ctfwiki]
└─# ./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
fffe7eb0.f7f44420.0804918d

GDB 来调试一下

此时,程序等待我们的输入,这时我们输入 %08x.%08x.%08x,然后敲击回车,是程序继续运行,可以看出程序首先断在了第一次调用 printf 函数的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[-------------------------------------code-------------------------------------]
0xf7e09f0b: xchg ax,ax
0xf7e09f0d: xchg ax,ax
0xf7e09f0f: nop
=> 0xf7e09f10 <__printf>: call 0xf7efb169 <__x86.get_pc_thunk.ax>
0xf7e09f15 <__printf+5>: add eax,0x1970eb
0xf7e09f1a <__printf+10>: sub esp,0xc
0xf7e09f1d <__printf+13>: lea edx,[esp+0x14]
0xf7e09f21 <__printf+17>: push 0x0
No argument
────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ esp 0xffffd3fc —▸ 0x80491da (main+100) ◂— add esp, 0x20
01:0004│ 0xffffd400 —▸ 0x804a00b ◂— '%08x.%08x.%08x.%s\n'
02:0008│ 0xffffd404 ◂— 0x1
03:000c│ 0xffffd408 ◂— 0x22222222 ('""""')
04:0010│ 0xffffd40c ◂— 0xffffffff
05:0014│ 0xffffd410 —▸ 0xffffd420 ◂— '%08x.%08x.%08x'
06:0018│ 0xffffd414 —▸ 0xffffd420 ◂— '%08x.%08x.%08x'
07:001c│ 0xffffd418 —▸ 0xf7fc3420 —▸ 0x80482b8 ◂— 'GLIBC_2.0'