- Event: Google Capture The Flag 2017 (Quals)
- Category: pwn
- Points: 243
- Solves: ~30
We’re given one file which can be downloaded here.
Task description -
Challenge running at wiki.ctfcompetition.com:1337.
Reverse engineering the program
For this task I’ve used the following set of tools:
- Ida Pro - for disasembling and decompiling
- gdb with pwndbg plugin for debugging
- pwntools - an exploit development library written in Python
The first step I’ve done was to check the architecture of the binary (to know which version of IDA Pro should I run)
a@x:~/Desktop/wiki$ file wiki wiki: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=b0bf486a495913bb2702825c5b41d5823f16b9ac, stripped
I’ve set up IDA Pro to show the same addresses when binary is loaded in the memory when PIE is disabled (for example when you run binary in gdb, or disable ASLR in your system). This can be done by chosing
Rebase Program and typing an address to be used as base -
Below I present my interpretation of reverse engineered binary. My C code is not 100% the same as original binary. I simplified it to avoid distracting readers with irrelevant details.
It does some initialization which I was not analysing. It calls function which I have called
It takes 1 argument. Debugging revealed that it is an array of pointers to 3 functions which are present in the binary.
I named these functions accordingly :
This function does exactly what the name states. It reads user input until
'\n' or up to length specified in an argument.
Moreover, it returns the number of bytes read.
Sends list of files in folder
./db to the client.
To execute this function it’s enough to type
LIST after connecting to a running program:
a@x:~$ nc 192.168.43.252 1337 LIST xmlset_roodkcableoj28840ybtide Fortimanager_Access 1MB@tMaN
I have also created folder
db with files named like above and filled them with many bytes of value
Of course, contents of these files are not known to me.
I created a file
flag.txt and placed fake flag inside.
Hunting for vulnerability
I didn’t find vulnerability when reverse engineering the first time. I started to think where vulnerability can be located.
Security protections of the binary can be listed using checksec command:
pwndbg> checksec [*] '/home/a/Desktop/wiki/wiki' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
Binary is protected with PIE which makes that
.text section is placed at different address on every execution of the program (note that when you run binary in gdb, it disables PIE and also ASLR).
The program use neither heap buffers nor prints user input. It only reads user input to stack buffers and what’s more, canaries are not present on the stack. It is reasonable to expect that buffer overflow vulnerability is present.
command_PASS is prone to buffer overflow. It reads 4096 bytes to
password which is only 128 bytes long.
Exploiting the vulnerability
It is not necessary to create a shellcode using ROP technique. To capture the flag it is enough to jump into code that is a part of
command_PASS function -
First idea - leak some memory to obtain address of
- it is not possible to leak the memory.
Second idea - overwrite only x last bytes of the return address to return to
- it is not possible because the length of vulnerable user input must be divisible by 8. All variables (including return address) on the stack are aligned to 8 bytes on x86_64 architecture.
Third idea - I remember the vsyscall table - a deprecated method used to accelerate system calls execution. This table is located at the same address during every run, and it contains
Later, during returning from
command_PASS I displayed stack content. Stack at higher addresses was keeping pointers to various functions and other executable places in
I displayed stack content using command:
Stack contains addresses of functions:
command_USER and some other places in
I could fill the place on the stack between return address (including it) and chosen function (not including it) by
RET instructions from vsyscall table. That way I could jump to chosen address from this stack area.
Next, I viewed
command_PASS in assembly view and realized something that can be the last part of the puzzle.
As a brief digression here, I would like to mention that Linux on x86_64 architecture follows the System V ABI standard. Function calling convention uses the following registers:
- first argument -
- second argument -
- 3rd argument -
- 4th argument -
- 5th argument -
- 6th argument -
If there are more arguments than 6, they are passed on the stack.
Returning back to the function
command_PASS, you can notice that during
RDI contains a pointer to password buffer.
The idea is to call jump again to
RDI contains data which we know and now this will be the first argument for this function.
The vsyscall area
Once upon a time, vsyscall area was created to speed up program execution. People came to a conclusion that it is not necessary to enter the kernel mode during some syscalls like
gettimeofday. It can be implemented in user mode. Therefore, an executable area was created, implementing some of the funcionality normally executed in kernel mode via traditional syscall. For this functionality, when process executed syscall, it was was jumping to the vsyscall area instead of going into kernel mode.
Unfortunately, vsyscall area was located at the same address during every run of the program. This proved to be a bad decision from security point of view. Exploit authors had list of several gadgets available even when PIE was enabled.
After some period of time, kernel developers realized that this is wrong, and they started to think how the problem can be minimized. vsyscall could not be removed due to risk of breaking backward compatibility. Instead, they modified vsyscall area in the following way:
- code was replaced by instructions:
mov rax, [syscall_number]; syscall; ret
probably you can see on your system, by attaching to random process:
there is also:
And that’s everything, there is no more code.
Accordingly, the syscalls incluced in vsyscall area are:
Position of vsyscall area is still not randomized, but number of usefull gadgets is now reduced.
- One can’t simply jump to
RET(process segfaults). You can only jump to one of three
Vsyscall area is not normal memory region, but it is emulated by the kernel. It is filled by trap instructions. When process executes this part of memory, kernel is notified of a page fault.
Later, kernel hits function emulate_vsyscall.
address is an address where process jumped in vsyscall area.
Next, function addr_to_syscall_nr validates whether this address is permitted. Address must be one of
I would like to mention that currently Linux supports vDSO area. It has the same functionality as vsyscall but the difference is that the localization of it is randomized.
If you are interested in how vsyscall was looking at the beginning, you can look at snipped taken on Ubuntu 11.04 x86_64:
Looking at assembly code of
command_PASS, we can compute the offset of return address starting from user buffer.
During call, before jumping to function, return address is placed on the stack. At
RBP is pushed:
RBX is pushed:
RSP is substracted by 0x88 bytes
The addres of the password buffer is equal to the place of actual
Below is a picture visualizing stack layout.
So you need to send 0x88+0x8+0x8 = 152 bytes before data which starts to overwrite return address.
Another thing that we need to know is the size of area between return address and
command_PASS. When you look at diagram showing stack on
command_PASS, you can see that return address is placed at position:
command_PASS is placed here:
First number in a line shows position on stack (in hexadecimal). It is necessary to fill stack fields from 00 to 23. It is 24 8B values.
For computing offset of return address you can also use pwntools.util.cyclic. This is easier method but you need to run debugger one more time :(
I needed to chose one syscall from available syscalls as filler between return address and address to
At the beginning I have used
gettime which saves UNIX time in memory pointed by
RDI. When I was running exploit on my system, I was providing current time as password in second call to
It worked on my host, but unfortunately was not working on the CTF server. I was thinking that UNIX time is the same on all PCs but now I realized that it is not true. Later I changed syscall to
gettimeofday. It saves a
struct timeval representing current UTC time, in a memory pointed to by the first argument. I wrote a script which checks the value of memory that is pointed by the
RDI register after this operation.
I disabled ASLR on my system to disable ASLR and PIE, to make debugging easier. Without this the script below would not work.
I ran the binary and set it to listen on port 1337 on my host:
The following script was used:
I run this script several times and it gives output similar to this:
The value of
3 in above output changes between 0 to 7 at every execution.
It means that the syscall
gettimeofday modified first 4 bytes pointed by
Now I can send password which is equal to this buffer, first byte is different but I can assume that is equal to 0 and run exploit several times.
Algorithm comparing passwords stops at null byte, so we can send 8
"\x00" as password.
Now, we can modify this script to do the job mentioned above:
After running it for the second time, it prints my fake flag:
After modyfying line
r=remote(... to connect to google address, it gives us real flag which is:
Below is a visualization of the exploit. I hope that this helps when some parts are less understandable: