fd - pwnable.kr

As I’m exploring cybersecurity during my semester break, this is my first write-up on solving a cybersecurity challenge. I tried to solve the first challange in pwnable.kr: fd

Below is my thought process from when I solved it.

Setup

The challenge provides SSH creds to connect to a server:

ssh [email protected] -p2222

Let’s SSH into it.

Inspection

Running ls after SSHing in reveals three files: fd, fd.c, and flag. Clearly we’re expected to read what’s inside that flag file.

Let’s just try to cat it.

It says: cat: flag: Permission denied. Obviously it can’t be this easy.

But wait, we also got the source file fd.c, and I know a little bit of C, so let’s see what it has:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
        if(argc<2){
                printf("pass argv[1] a number\n");
                return 0;
        }
        int fd = atoi( argv[1] ) - 0x1234;
        int len = 0;
        len = read(fd, buf, 32);
        if(!strcmp("LETMEWIN\n", buf)){
                printf("good job :)\n");
                setregid(getegid(), getegid());
                system("/bin/cat flag");
                exit(0);
        }
        printf("learn about Linux file IO\n");
        return 0;

}

Back to first semester

Hmmm. Thanks to my first-semester C programming class, this looks kind of familiar. It’s taking some CLI arg and doing some math on it.

In C, the argv[] array is the array of CLI args passed to the program like ./program arg1 arg2, etc. So int fd = atoi(argv[1]) - 0x1234 means: convert the CLI arg at index 1 (index 0 is always the program name) to an integer (quick Google revealed atoi converts ASCII to integer).

And then we’re using read(). Another quick Google search revealed that read() is a system call that takes three inputs:

  1. fd: file descriptor of a file to read (hmm, the title of the challenge makes sense now; more on this later)
  2. buf: pointer to memory where the read data will be stored
  3. count: maximum number of bytes to read

So it looks like it’s trying to read something and put the content in the buf variable. But I still didn’t know what it was trying to read.

I remembered from my C class that an fd (file descriptor) is a positive integer ID assigned to an open file so the OS can keep track of which files are opened (potentially being read/written). But… I still didn’t know what file it’s trying to read. So yeah, let’s keep going.

After reading, it uses strcmp to compare whatever was read with "LETMEWIN\n". If it matches, it prints “good job :)” and then cats the flag.

But it does something in between that I didn’t really understand at first: setregid. That’s not something I’d seen yet, so I googled it.

I went down a rabbit hole here, but after a few minutes of Googling and ChatGPTing what setregid is, I realized it’s basically there so I can cat the flag (remember we couldn’t do it because we didn’t have the permission). I was chasing the wrong thing.

Let’s focus on real challenge

Now at this point I know it’s reading something and somehow that “something” has to contain LETMEWIN. But how? What file?

My first instinct was to create a file with LETMEWIN myself and somehow do the math and guess the fd to it… but I didn’t even have permission to create a file there, so that idea died instantly.

Eureka

After a while of messing around and Googling, I realized how dumb I’d been the whole time by ignoring the message the program prints on failure: “learn about Linux file IO”.

Because when you Google Linux I/O + file descriptors, something very important comes up: 0, 1, and 2 are special.

  • 0 is stdin
  • 1 is stdout
  • 2 is stderr

THE fd 0 IS stdin.

Which means if I do the math to make fd = 0, I can literally just type LETMEWIN into the console and get the flag.

QUICK MATH

We want fd to be 0, so our input argv[1] should be 0x1234.

So what’s decimal for hex 0x1234? Calculator says it’s 4660.

Moment of truth

Then I called the program with 4660 as the CLI arg like:

./fd 4660

…but nothing happens.

I’m thinking it’s taking a lot of time. After waiting there for like a minute like a dumbo, I realized it’s waiting for me to type LETMEWIN because it’s reading from stdin (fd 0).

Then I typed LETMEWIN and it did let me finally winnnnn.