Pwnable.kr: ‘input’ Walkthrough

While being one the most straightforward challenges yet, pwnable’s “input” challenge took me a while because there was some C/Linux stuff that I either didn’t know, or that I forgot (sockets, aghgh).  😬  No matter, now’s a great time to (re-)learn it!  Here’s our hint:

Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)

If we ssh into the server, and cat out the input.c file that we’ll be running, we see:

input2@ubuntu:~$ cat input.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");

    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");

    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
    if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");

     // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");

    // file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n");

    // network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons( atoi(argv['C']) );
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
    return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");

    // here's your flag
    system("/bin/cat flag");
    return 0;
}

Okay, so there’s 5 different sections that we need to get past in order for our flag to be printed up.  I fumbled around a little bit with how to approach this.  I originally started with a python file, and then realized that I’m way better at C than I am at python.

So, I made a directory under /tmp/ and made a C file there called input_args.c

input2@ubuntu:~$ mkdir /tmp/jl2
input2@ubuntu:~$ cd /tmp/jl2
input2@ubuntu:/tmp/jl2$ vi input_args.c

Phase 1:  argv

Phase 1 asks for a bunch of arguments to the input program, as follows:

int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");

    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");

The first if statement is expecting 100 arguments to the program.  The second if statement expects the Ath argument to equal \x00 and the Bth argument to equal \x20\x0a\x0d.

Note that the chars A and B are equivalent to 65 and 66 in the ASCII table.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(){
   int i;

    /* Stage 1 is looking for 100 arguments (we need one extra space at the end)*/
    char *args[101] = {};

    for (i=0; i<101; i++) {
        args[i] = "A"; // fill up 100 arguments with a filler char
    }

    args['A'] = "\x00";
    args['B'] = "\x20\x0a\x0d";
    args[100] = NULL;
    execve("/home/input2/input", args, NULL);
}

execve at the end runs the input program, and we get “Stage 1 clear!” like we were hoping for.  Also, I just dumped all the #includes from the original file in here from the get-go.

Phase 2:  stdio

Next up, stdio.

// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");

The read call gives us a clue.  The function signature for read is as follows:

ssize_t read(int fd, void *buf, size_t count);

The first argument is fd.  And input.c is going to be reading from fd=0 and fd=2.  As it turns out, these file descriptors map to STDIO and STDERR, respectively.  Can we use pipes to send the messages?  Probably.

Pipe

The pipe section of this challenge was definitely a re-learning area for me.  This guide on creating pipes in C was very useful.

First, I defined two pipes (int arrays of length 2), one for STDIN and one for STDERR, and also created a process ID variable:

pid_t childpid;
int pipe_stdin[2];
int pipe_stderr[2];

A pipe has two ends, one for reading and one for writing, which is why the arrays have a length of 2.  Each end holds a file descriptor, one for reading and one for writing.  Calling pipe on each pipe creates the reading and writing ends of the pipe.  We can just pass in pipe_stdin because it’s equivalent to &fd_stdin[0].

// call pipe() on both pipes
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
  perror("oh no\n");
  exit(1);
};

After that, we need to call fork() to spawn a new process.  This gets assigned to childpid, which we then check for an error condition.  Some more reading on fork here and here.

// fork the process
if((childpid = fork()) < 0)
{
    perror("fork failed, oop");
    exit(1);
}

Now we can use childpid to determine which process is currently active.

From creating pipes in C again:

If the parent wants to receive data from the child, it should close fd1, and the child should close fd0.

If the parent wants to send data to the child, it should close fd0, and the child should close fd1.

Since descriptors are shared between the parent and child, we should always be sure to close the end of pipe we aren’t concerned with.

We can have the child process write the expected values (\x00\x0a\x00\xff and \x00\x0a\x02\xff) into the two pipes, which means first we need to close the reading end of the pipe.

The else case, which represents the parent process, needs to close the writing end of the pipe, somehow hook up the pipes to STDIN and STDERR, and then close the reading end (and then call our input file).  Dup2 will duplicate a file descriptor for us.

// child process can close input side of pipe and write expected values
if(childpid == 0)
{
    /* Child process closes up input side of pipe */
    close(pipe_stdin[0]);
    close(pipe_stderr[0]);

    write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
    write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);
    return 0;
}
else
{
 /* parent process can close up output side of pipe, connect it to stdin and stderr,
    and then close the input side and call/home/input2/input */
    close(pipe_stdin[1]);
    close(pipe_stderr[1]);

    dup2(pipe_stdin[0],0);
    dup2(pipe_stderr[0],2);

    close(pipe_stdin[0]);
    close(pipe_stderr[0]);
    execve("/home/input2/input", args, NULL);
}

Et voila, Stage 2 cleared!

Stage 3:  env

In stage 3, we have to set an environment variable (deadbeef) to the value cafebabe.

// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");

This is pretty straightforward.  We can call setenv (see?  straightforward) and pass in our variables, then create an environ variable that we then pass into our execve call later.

setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);
extern char** environ;
[..rest of code...]
execve("/home/input2/input", args,environ);

Stage 3 cleared!

Stage 4:  file

This stage looks for a file, opens it with reading privileges, reads from it, compares it to a value, and then (hopefully) lets us continue on our way:

// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

This stage is relatively easy because we can just do the inverse of what the input code is doing.  By that I mean:  create/open a file with write privileges, write the value to it, close it.  For reference, I included the signature of fwrite.

//size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
FILE* fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

Stage 4 cleared!  On to the final boss!

Stage 5:  network

Here’s the original input code for this section:

// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
    printf("socket error, tell admin\n");
    return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
    printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
    printf("accept error, tell admin\n");
    return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

Ooooooh boy, sockets.

I went through this section step by step, investigating each function and variable to make sense of things.  Throughout the process, I found this networking and socket programming guide to be incredibly useful.

Sockets

Sockets are internal endpoints for sending or receiving data within a node on a computer network.  Calling socket() creates an endpoint for this communication, and returns a descriptor, which we’re assigning to sd.

sd = socket(AF_INET, SOCK_STREAM, 0);

What is AF_INET, though?  It’s an address family used to designate what types of addresses that the socket can communicate with.  In our case, AF_INET means we can communicate with IPv4 addresses.

SOCK_STREAM means a “socket type that provides sequenced, reliable, two-way, connection-based byte streams with an OOB data transmission mechanism.”

And what about that 0?  The last argument to socket() indicates the protocol.  An argument of 0 indicates that we don’t want to specify a protocol.

Up next, we set up some more socket-related variables in our saddr_in struct.

saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );

Setting sin_family to AF_INET is the same IPv4 address family discussed earlier.  Setting sin_addr.s_addr to INADDR_ANY doesn’t mean bind to a random IP address, but instead, bind the socket to all available interfaces.

Lastly, we’re setting the port number to the Cth argument that we passed into our function, which means we’ll need to alter one of the input args we created in Stage 1.  The htons() function converts the integer from host byte order to network byte order, which I think means it handles endianness issues for us.

Lastly, the code calls bind() and then will listen for messages through that connection.  Once again, I referred to this guide for help–there’s a handy diagram that shows the steps for the server and the client.  We need to follow the steps for the client.

What that means for us

So, we need to step a similar socket struct, connect, and then write to it.  You can see that the socket() call is almost verbatim (I stole the error checking from the original file as well).  We set sin_family to AF_INET, we set the sin_addr.s_addr to localhost (127.0.0.1) and the sin_port to the Cth arg from stage 1.

We call connect, check for errors, and then write our deadbeef message.  All of this code is in the child process block that we created earlier in the pipe section.

// Stage 1 addition:  
args['C'] = "5001";

// Stage 5
sleep(5);
int sd, cd;
struct sockaddr_in saddr;
sd = socket(AF_INET, SOCK_STREAM, 0);

if(sd == -1){
   printf("socket error, tell admin\n");
   return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if(connect(sd, (struct sockaddr *)&saddr, sizeof(saddr))<0)
{
   printf("\n Error : Connect Failed \n");
   return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
 close(sd);

The whole enchilada

Here’s the final code, all together:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(){
   int i;

  /* Stage 1: argv */
  char *args[101] = {};

  for (i=0; i<101; i++) {
      args[i] = "A"; // fill up 100 arguments with a filler char
  }

  args['A'] = "\x00";
  args['B'] = "\x20\x0a\x0d";
  args['C'] = "5001";

  args[100] = NULL;

  /* Stage 3: env */
  setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);
  extern char** environ;

  /* Stage 4: file */
  FILE* fp = fopen("\x0a", "w");
  //size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
  fwrite("\x00\x00\x00\x00", 4, 1, fp);
  fclose(fp);

  /* Stage 2:  stdio */
  pid_t childpid;
  int pipe_stdin[2];
  int pipe_stderr[2];

  // call pipe on both of them
  if (pipe(pipe_stdin) < 0  || pipe(pipe_stderr) < 0) {
    perror("oh no\n");
    exit(1);
  };

  // fork the process
  if((childpid = fork()) < 0)
  {
    perror("fork, oop");
    exit(1);
  }

  // child process can close input side of pipe and write expected values
  if(childpid == 0)
  {
    /* Child process closes up input side of pipe */
    close(pipe_stdin[0]);
    close(pipe_stderr[0]);

    write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
    write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);

    /* Stage 5:  network */
    sleep(5);
    int sd, cd;
    struct sockaddr_in saddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);

    if(sd == -1){
      printf("socket error, tell admin\n");
      return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    saddr.sin_port = htons(atoi(args['C']));

    if(connect(sd, (struct sockaddr *)&saddr, sizeof(saddr))<0)
    {
      printf("\n Error : Connect Failed \n");
      return 1;
    }

    write(sd, "\xde\xad\xbe\xef", 4);
    close(sd);

    return 0;
  }
  else
  {
  /* parent process can close up output side of pipe, connect it to stdin and stderr,
     and then close the input side and call/home/input2/input */
    close(pipe_stdin[1]);
    close(pipe_stderr[1]);

    dup2(pipe_stdin[0],0);
    dup2(pipe_stderr[0],2);

    close(pipe_stdin[0]);
    close(pipe_stderr[0]);
    execve("/home/input2/input", args, environ);
  }
}

We’re not quiiiiite done yet.  If we run the program, it can’t find flag because it’s not in our directory, and we don’t have permissions to move it.  However, we can create a symbolic link by using ln -sf:

input2@ubuntu:/tmp/jl2$ ln -sf /home/input2/flag flag
input2@ubuntu:/tmp/jl2$ ls -la
total 1972
drwxrwxr-x     2 input2 input2    4096 Jun  9 19:58 .
drwxrwx-wt 54398 root   root   1986560 Jun  9 19:56 ..
-rw-rw-r--     1 input2 input2       4 Jun  9 19:55 ?
lrwxrwxrwx     1 input2 input2      17 Jun  9 19:58 flag -> /home/input2/flag
-rwxrwxr-x     1 input2 input2   13776 Jun  9 19:52 input_args
-rwxrwxr-x     1 input2 input2    3386 Jun  9 19:51 input_args.c

Yay!

And here it is!

input2@ubuntu:/tmp/jl2$ ./input_args
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
Mommy! I learned how to pass various input in Linux :)