pwnable.tw->seethefile
前言
SUCTF里做了一道简单的文件题,想趁热打铁多了解一些文件结构的知识,于是时隔多年再次杀回pwnable.tw,不过遗憾的是这个题还是没能自己做出来,不过依然有一些trick可以借鉴,因此写篇writeup提醒自己
程序逻辑
程序也比较简单,用户输入文件名,打开之后每次读取0x18F字节,输出到屏幕上,关闭文件,退出。这里的exit前会让用户输入自己的姓名,name位于.bss段的0x0804B260处,而文件句柄fp位于.bss段的0x0804B280处。这里的scanf并没有限制输入的长度,因此存在溢出漏洞。
数据构造
Libc基址
根据前面学到的知识,文件可以通过覆写vtable来修改一些函数指针指向我们想要执行的函数。因此,可以通过伪造FILE结构来执行shell,首先是libc基址的寻找,也正是在这里我直接卡住放弃的。看了p4nda师傅的wp得知文件执行的时候Linux会将进程的虚拟地址空间存储在/proc/
FILE结构体
FILE结构体比较复杂,构造的时候要注意一些关键的验证条件要满足,这里给出CTF All In One的libio.h中的实现(glibc2.23)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
71struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _blksize;
int _flags2;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
_IO_off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
1 | struct _IO_jump_t |
这里伪造结构体的时候有一些关键的点需要注意:
- IO_FILE结构体偏移0x34的地方即_chain字段指向的也是一个FILE结构,我们可以使用_IO_2_1_stderr_,_IO_2_1_stderr_,_IO_2_1_stdout_或者,也可以使用自己的FILE结构体地址(不过实测发现stderr不可以,猜测里面用到了read和write但是没有setbuf(stderr),即这个流没打开,不过正常应该这仨都是自动打开的才是Orz)
- 位于0x48偏移的_lock必须指向一个为NULL的空间,因此我们可以用\x00填充name,在这里填name的地址
- _vtable_offset要为0,其位于0x46处且只占一个字节
- 其余部分是0或者0xffffffff按照未溢出前正常fp结构体的分布来写
- vatable的前两个地址为NULL
- close函数覆写为system,函数执行的时候的参数为fp,故可以将fp的开头改为/bin/sh\x00,即可让fclose(fp)变为syetm(fake_fp),fake_fp->/bin/sh\x00
综上所述最终的payload结构如下:
exp.py
1 | #coding=utf-8 |