---
ingested: true
ingestedAt: 2026-05-16
---
标题: 格式化字符串漏洞原理介绍
链接: https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-intro/
提取方式: WebFetch
内容:
# 原理介绍
首先,对格式化字符串漏洞的原理进行简单介绍。
## 格式化字符串函数介绍
格式化字符串函数可以接受可变数量的参数,并将**第一个参数作为格式化字符串,根据其来解析之后的参数** 。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来**输出信息,调试程序,或者处理字符串** 。一般来说,格式化字符串在利用的时候主要分为三个部分
- 格式化字符串函数
- 格式化字符串
- 后续参数,**可选**
这里我们给出一个简单的例子,其实相信大多数人都接触过 printf 函数之类的。之后我们再一个一个进行介绍。
![[Ztc81wQevr]]
> **图片描述**:这张图片展示了一个典型的 printf 格式化字符串函数的使用示例。上方显示输入代码:`printf("Color %s, Number %d, Float %4.2f", "red", 123456, 3.14);`,其中包含了三个格式化占位符:`%s`(字符串)、`%d`(整数)和 `%4.2f`(浮点数)。通过箭头我们可以清楚地看到,每个占位符都对应着后面的一个参数:`"red"` 对应 `%s`,`123456` 对应 `%d`,`3.14` 对应 `%4.2f`。下方显示输出结果:`Color red, Number 123456, Float 3.14`,完美地展示了格式化字符串函数如何将占位符替换为实际参数值的过程。
### 格式化字符串函数
常见的有格式化字符串函数有
- 输入
- 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
```
每一种 pattern 的含义请具体参考维基百科的[格式化字符串](https://zh.wikipedia.org/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2) 。以下几个 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'表明我们会输出一个字符串参数。
我们再继续以上面的为例子进行介绍
![[Ztc81wQevr]]
> **图片描述**:再次展示同一个 printf 示例图,因为这对理解后面的漏洞原理非常重要。这个简单的示例清楚地展示了格式化字符串函数的工作机制,当我们后面介绍漏洞原理时,这个正常使用的例子将作为对比,帮助我们理解当参数缺失时会发生什么问题。
对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下
```
some value
3.14
123456
addr of "red"
addr of format string: Color %s...
```
**注:这里我们假设 3.14 上面的值为某个未知的值。**
在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况
- 当前字符不是 %,直接输出到相应标准输出。
- 当前字符是 %, 继续读取下一个字符
- 如果没有字符,报错
- 如果下一个字符是 %, 输出 %
- 否则根据相应的字符,获取相应的参数,对其进行解析并输出
那么假设,此时我们在编写程序时候,写成了下面的样子
```
printf("Color %s, Number %d, Float %4.2f");
```
此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为
1. 解析其地址对应的字符串
2. 解析其内容对应的整形值
3. 解析其内容对应的浮点值
对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。
这基本就是格式化字符串漏洞的基本原理了。
## 参考阅读
- [https://zh.wikipedia.org/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2](https://zh.wikipedia.org/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2)