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 A
th argument to equal \x00
and the B
th 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 C
th 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 C
th 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 :)