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
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 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:
first, is to attach the current process to the process we wish to ptrace
ptrace(PTRACE_ATTACH, <PID>, NULL, NULL);
second, we perform operations to the attached process such as reading and writing to it
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:
which we'll be using to read data (the flag) from parent .
to confirm this, I wrote this small program to confirm the hypothesis:
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:
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.
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.
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:
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:
ββββΌ [β ]$ seccomp-tools dump "./red40 loop"
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000029 if (A == socket) goto 0009
0006: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0009
0007: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0010: 0x06 0x00 0x00 0x00000000 return KILL
ββββΌ [β ]$ ./red40 loop
What would you like to do with your RED40?
1. Appreciate RED40
2. Gamble RED40
3. Warn RED40
4. Consume RED40
5. STEAL RED40
6. Exit (no RED40 for you)
>
main.c
void appreciate()
{
printf("You are now appreciating your %d RED40!\n", RED40);
}
main.c
void gamble()
{
puts("ITS TIME TO GAMBLE RED40!!!");
int r;
char again[2];
while (1)
{
puts("Are you ready to gamble??? (\"99.9999999% of gamblers quit right before winning big...\" - Royce du Pont) (Y)");
printf("\n> ");
read(0, again, 2);
again[1] = '\0';
if (strncmp(again, "Y\0", sizeof(again)))
break;
r = rand() % (40 + 1);
if (r == 40)
{
puts("YOU WON!!! (KEEP GAMBLING FOR MORE RED40)");
RED40 = getppid();
break;
}
RED40--;
printf("\nGambling... YOU LOST. You have %d RED40 remaining\n", RED40);
puts("I'm a winner see my prize, you're a loser who sits and cries");
}
}
main.c
void warn_get()
{
int c, i = 0;
char red[0x20];
memset(red, 0x0, 0x20);
printf("How will you warn the RED40?\n>\n");
while ((c = fgetc(stdin)) != '\n' && c != EOF && i < 12)
red[i++] = c;
printf(red);
printf("\nDO YOU HAVE ANYTHING ELSE TO SAY TO THE RED40?????\n> ");
gets(red);
if (WARN)
{
puts("YOU HAVE BEEN CAUGHT!!! NO MORE WARNING RED40!!!");
exit(0);
}
else
WARN = 1;
}