UMassCTF

PTRACE

circle-info

Team: HCS

Rank: 66 / 417

Challenge
Category

red40

Binary Exploitation

red40

Description

I heard you like RED40

Binary Analysis

We're given attachments as follows:

└──╼ [β˜…]$ tree .
.
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ entrypoint.sh
β”œβ”€β”€ libc
β”‚Β Β  β”œβ”€β”€ ld-linux-x86-64.so.2
β”‚Β Β  └── libc.so.6
β”œβ”€β”€ main.c
β”œβ”€β”€ Makefile
β”œβ”€β”€ nsjail.cfg
β”œβ”€β”€ parent
β”œβ”€β”€ parent.c
β”œβ”€β”€ red40
└── run.sh

└──╼ [β˜…]$ pwn checksec parent red40 
[*] '/home/halcyon/git/CTFs/2024/UMass/red40/parent'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./libc'
[*] '/home/halcyon/git/CTFs/2024/UMass/red40/red40'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./libc'

notice we have 2 binary here, looking at the entrypoint:

we see that parent is the interface we're interacting with.

looking at its main function, it will try to run fork() seemingly to randomize the PID of the forked process, and then finally running the forker() function.

forker() then will create a new process using the red40 binary and allocate the flag inside of parent's heap memory

in red40 there's a seccomp filter that is being applied:

we can also dump the filter using seccomp-tools and it shows the same result:

upon running red40 we're greeted with a few options:

the first option simply prints the RED40 global variable

the second option is a literal gamble lol

however if we won the gample we will receive the parent's PID and can leak it using the aforemention appreciate() function.

the third option, enables a format string and buffer overflow vulnerability:

I never used the fourth option and so I will skip it as it is lack of relevancy.

the fifth option enables a LFI once, allowing us to read files from the machine but with some limitation like, only 3 / are allowed among others.

Exploitation

circle-info

my exploit is not the easiest to do, but I'm glad I gone through this path because in the end I'm touching a new subject and learning more

more on the cheesy solution at the end.

Threading Shenanigans

in a threaded environment such as this, it would be pain to debug. from this link:

we can set the breakpoint before the call to fork(), and run set follow-fork-mode child in GDB which is part of the thread that will eventually spawn the new process. this will enables us to examine the new process memory (i.e. red40) and debug our payload.

Parent Process Memory Leak

since our flag is located inside of the parent's process memory, we obviously need to leak the parent's address heap memory to read. this can be done by:

first leaking the parent's PID by gambling:

and then leveraging LFI to read its memory mapping:

but then now what?

Understanding PTRACE

the flag is in the parent's memory but we're interacting with the child's memory, and there's no way for the child to examine or read from another process memory... right?

and so I did a bit of googling using that exact keyword, and found this:

the example code given in that link somewhat seems very similar to the challenge we're facing. so what exactly is PTRACE?

reading from the man page:

The ptrace() system call provides a means by which one process(the "tracer") may observe and control the execution of another process (the "tracee"), and examine and change the tracee's memory and registers. It is primarily used to implement breakpoint debugging and system call tracing.

so its a system call to examine another process memory and execution, exactly what gdb and other debugger uses. and this is exactly what we wanted

but me myself is still quite blind in terms of how to use the syscall properly, so to google I return once more and found this article that demonstrate how to use PTRACE:

I highly recommend you to read it to fully understand but to summarise, there's 3 stages to PTRACE:

  1. first, is to attach the current process to the process we wish to ptrace ptrace(PTRACE_ATTACH, <PID>, NULL, NULL);

  2. second, we perform operations to the attached process such as reading and writing to it

  3. once we're done, we detach from it ptrace(PTRACE_DETACH, <PID>, NULL, NULL);

there's numbers of operations that can be done, the full list of them can be seen from the man page linked above, and for the literal macro values can be seen here:

other than the PTRACE_ATTACH operation:

we're also interested in PTRACE_PEEKDATA:

which we'll be using to read data (the flag) from parent .

to confirm this, I wrote this small program to confirm the hypothesis:

circle-info

Here's I used sleep() to wait for the attach completes within the kernel side, I tried using wait(NULL) just as the aforementioned blog demonstrates but it doesn't work here nor in the ROP payload later below.

while running parent , I run it and giving it parent's PID and its heap base

and as you can see, we're able to read another process memory and leak the flag.

with this mind lets develop the exploit for it, we still have bof and format string that we haven't abused yet.

Implementing PTRACE ROP

to do this, we need to be able to control a lot of register to pass the PTRACE arguments, however the red40 binary itself doesn't have an abundance of gadgets.

libc in the other hand has practically all of the gadget we need, and thus a leak to libc address will enable us to ROP through the gadget to control the register. we will exploit the format string to leak a libc address.

the binary also have PIE enabled, and thus we also need to leak a writeable address for which it will store the data from parent that wanted to be read. I chose the stack as it is easy to leak from the same format string.

our ROP payload will be split into three section:

  1. Initating PTRACE

    the sleep to wait is mandatory or else, the subsequent PTRACE operations will returns an error, I can't say for sure how long the duration will be.

  2. Reading flag from parent and storing it in red40 calling PTRACE with PTRACE_PEEKDATA only read 8 bytes (in x64) from the starting memory of which is in RAX. I'm not sure if there's anyway to read more than 8 bytes, but since the we have no length limitation on our ROP payload, because the bof is triggered by gets() , I didn't bother to look it up. and so I wrote this function to read 8 bytes and mov the return value to the writeable address in the current process.

  3. Writing leaked flag to stdout next we just need to write the flag to stdout, I think this is pretty self explanatory, just call the write syscall

I write this writeup long after the competition had already ended, and didn't take the screenshot of running it againts remote so you have just take my word and it works lol :p

Reading Parent memory without PTRACE

there's another way to read to read parent's memory process without requiring to PTRACE, I found this solution in the TCP1P server which if you joined the discord, you can open the writeup here:

in summary:

  • open("/proc/<ppid>/mem", O_RDONLY)

  • lseek(mem_fd, flag_addr, SEEK_SET)

  • read(mem_fd, buf, 0x30)

  • write(1, buf, 0x30)

Cheesy Solution

the cheese to solution to this which I didn't know why I didn't think of this lol, is to just basically read the parent' binary. since the flag is hardcoded, we can read the string at a certain offset using lseek(), we can figure out where the binary is located from the dockerfile that they provided.

Another awesome PTRACE challenge

Another thing I wanted to note is that the writeup linked below helped me a lot in debugging and understanding while also motivates me to do the exploit the PTRACE way:

the challenge in that writeup is harder and definitely more interesting as it uses PTRACE to patch a process in real time.

Below is the full exploit script:

circle-check

Last updated