My solution for task wiki
- 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 Edit
-> Segments
-> Rebase Program
and typing an address to be used as base - 0x555555554000
.
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.
main (0x0000555555554A40)
It does some initialization which I was not analysing. It calls function which I have called real_main
.
real_main (0000555555554CB7)
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 : command_USER
, command_PASS
and command_LIST
.
read_line (0x0000555555554C00)
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.
command_LIST (0x0000555555554BA5)
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 'a'
.
Of course, contents of these files are not known to me.
command_USER (0x0000555555554DA1)
command_PASS (0x0000555555554C5E)
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.
Indeed, function 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 - system("cat flag.txt");
First idea - leak some memory to obtain address of .text
section.
- it is not possible to leak the memory.
Second idea - overwrite only x last bytes of the return address to return to system("cat flag.txt")
- 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 RET
instructions.
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 .text
section.
I displayed stack content using command:
Stack contains addresses of functions: command_PASS
, command_LIST
, command_USER
and some other places in .text
section.
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 -
RDI
- second argument -
RSI
- 3rd argument -
RDX
- 4th argument -
RCX
- 5th argument -
R8
- 6th argument -
R9
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 RET
, RDI
contains a pointer to password buffer.
The idea is to call jump again to command_PASS
function. 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:
And that’s everything, there is no more code.
Accordingly, the syscalls incluced in vsyscall area are: gettimeofday
, time
, getcpu
.
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 threemov
instructions.
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.
Argument 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 0xffffffffff600000
, 0xffffffffff600400
, 0xffffffffff600800
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:
References:
- http://toh.necst.it/hack.lu/2015/exploitable/StackStuff/
- http://lwn.net/Articles/446528/
- https://gist.github.com/kholia/5807150
- https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-3.html
Writing exploit
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 0x0000555555554C5E
address, RBP
is pushed:
Later RBX
is pushed:
And next, RSP
is substracted by 0x88 bytes
The addres of the password buffer is equal to the place of actual RSP
register:
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 RET
in 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 command_PASS
.
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 command_PASS
.
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 RDI
.
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:
CTF{NoLeaksFromThisPipe}
Summing up
Below is a visualization of the exploit. I hope that this helps when some parts are less understandable: