win64
Solving a 64-bit ret2win.
This is going to be the first time dealing with a 64-bit binary. In the ret2win case, this is not much different than the 32-bit version. In future challenges, we will see that 64-bit binaries become increasingly more complex than 32-bit ones, so it's essential to understand the differences between them.
Remember that 64-bit binaries pass parameters via the registers. The return pointer and base pointer are still stored on the stack for later use, just like 32-bit.
This binary has identical source code to win32, but is compiled for 64-bit.
Checking Security
As always, we check the security of the binary:
$ checksec win64
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segmentsWe see no protections on the binary, so ret2win is probably a good solution. Here is where we also notice that the binary is 64-bit, meaning that we have to treat the binary accordingly.
GDB Disassembly
Unsurprisingly, checking the functions list yields us the same result as win32:
We're going straight to read_in because we know the contents of win and main aren't relevant to us.
We see that this is formatted very similar to the win32 binary, so it shouldn't be that scary.
The same "assembly dance" is happening in this binary. Let's go over this process:
We know that
mainperforms acall read_into get inside this function.calldoes two things: (1) puts the return pointer, the instruction aftercall, onto the stack; and (2) jumps to the address of the called function.push rbppushes the old base pointer onto the stack. This is done so that the base pointer can be restored later.mov rbp, rspsets the base pointer to the current stack pointer. This is done so that the base pointer can be used as a reference to the stack.sub rsp, 0x30allocates0x30bytes on the stack for local variables.
This means that after the assembly dance, our stack should look like this:
Reviewing the assembly, we notice that there is a call to puts@plt and gets@plt. puts just prints out the "Can you figure out how to win here?" to the screen. Let's dive deeper into gets() and how it might be different for this architecture.
Inputting in 64-bit
Since this is 64-bit, parameters are passed via the register. We know that gets() takes one argument: the address where our input is stored. We proved the last binary that we are writing to the stack.
If we check the last data that was passed to rdi before gets is called, this is where we write. We find that this line is the last update to rdi:
rdi is loaded with the address of rbp-0x30, meaning this is where we are writing.
Getting the Offset
Getting the offset is the same in 64-bit as 32-bit. Let's put a breakpoint right before the call so we can inspect the stack:
Find the address of the return pointer by checking the instruction after the call to read_in:
Our return pointer is 0x401205.
Let's check what's on the stack:
We see that the return pointer is there:
Checking rdi shows us where we will start writing:
Let's see how many bytes that we need to write to reach here:
This makes sense. We were told that we are writing at rbp-0x30, which is 48 bytes from the base pointer, plus we need to add 8 bytes for the old base pointer that was pushed on the stack, totaling 56 bytes.
Let's craft our exploit:
Running this produces the following output:
Hmmm. This isn't working. We know this because we reached EOF (end of file) before we got to the interactive shell. Something's not quite right with the payload.
The movaps Problem
movaps ProblemWe can modify our payload to run a gdb instance on the binary using our payload to ensure that it executes properly.
gdb.debug takes the secondary argument of gdbscript which is the list of commands you want to run automatically. This allows for rapid debugging by consistently jumping to the same spot in memory. In our case, we set gdbcmds to:
This is going to set a breakpoint right before the gets() call (which we did manually) and then continue (because a breakpoint is automatically set at _start()).
If we reach the end of read_in, we notice, based on the execution flow, that it intends to go to win():
Using ni, we see that the program successfully makes it to win(). Our payload successfully takes us to the right place! If we continue execution to let it print the flag, we see that it segfaults:
We notice that it stops on the movaps instruction inside of do_system:
From the call trace, we see that this is called from win() calling system(), as expected:
This is known as the movaps fault. This happens because movaps expects the stack to be 16-byte aligned. However, we diverted execution away from the standard execution flow, so there's no guarantee that the stack is aligned. Furthermore, we are writing 56 bytes to the stack, which is not a multiple of 16. This means that the stack is not aligned and movaps will segfault.
How can we fix this? We can add 8 bytes to the payload, which will make it 64 bytes (which is a multiple of 16). The standard solution for this is to divert to another return, which will effectively add 8 bytes to the payload and not affect the rest of the execution. Let's find another ret to divert to. It doesn't matter which you pick, I randomly chose the one inside deregister_tm_clones:
The address of this return is 0x401110. Let's add this to our payload:
You'll notice that I label my variables based on what they are. f variables are functions, g variables are gadgets. More on what gadgets are when we get to ROP.
Running this:
cat flag.txt is called! Let's run this against the remote server:
And we have our flag!
Last updated