This is a write-up for the task readflag
from zer0pts CTF 2022.
The task provides an docker instance, which contains a executable readflag
. What this executable does is very simple. It contains the flag as a static string, and prints it out with a little touch. Prior to printing, the flag is xor'd with /dev/urandom
. However, we can't just run cat readflag | strings
because we don't have the read permissions on the flag.
As it's well known, reading a file with open/openat()
and executing the file with execve/execveat()
require different permissions, yet shares a common point. Both of them allows the contents of a file to be loaded onto the memory. In this case, we can examine readflag
's /proc/<pid>/mem
entry to take a look at the flag in .rodata
.
However, there is a setuid
flag on this executable, which prevents these kinds of memory inspection. In order to inspect memory in any ways, this setuid
has to be dropped. There are many ways to accomplish this, but the easiest way of doing this would be running readflag
under the debugger. With that said, the problem is now there is nothing like gdb
or even gcc
in this docker. To make matters worse, the docker is running with --network=none
which makes installing new things a real pain. So, the best bet here is to craft a compact binary which does the ptrace()
job, and move it via echo
ing it into the console.
The binary itself is simple. It's a simple debugger, which forks itself and diverges into a debugger and debuggee. The debuggee just calls ptrace(PTRACE_TRACEME, 0, NULL, NULL)
and execve()
the target binary, /readflag
. The debugger waits for it, and then examines the address which the flag belongs with ptrace(PTRACE_PEEKDATA, pid, ...)
. Because /readflag
is a Position-Independent Executable, I just stopped at the 5th openat()
call by utilizing ptrace(PTRACE_SYSCALL, ...)
and examine the system call number by ptrace(PTRACE_GETREGS,...)
. This resolves the address problem with the least effort, as the flag string and "/dev/urandom"
must be somewhat near.
However, this plan didn't work out. I was not able to read the contents of the memory with ptrace()
. The story is, I cannot examine the memory contents with PEEKTEXT or PEEKDATA with the perms of readflag
. So, I decided go around it. From the prior runs, I could confirm that getting the value of the registers were working fine. Therefore, I examined the value of rdx
just after the instruction movzx edx, byte ptr [rax]
, which loads the value of flag onto edx
. In order to choose this particular instruction, I just used a fuzzy and blunt method of examining the least significant byte of rip
, circumventing the need of full address resolution.
Then, I just compiled the program and compressed it with gzip
. After moving the compressed binary through the terminal and unzipping it, I was able to get a bunch of string. Refining the flag out of it was a piece of a cake.
Please refer to the attached files for the actual exploit code.