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

Last updated