Welcome back! Now that we’ve conquered Level 2 and gained the password for Level 3 it’s time to keep pressing.
Introduction
Same thing from before. Log into the lab server as Narnia3 and look at the source code for narnia3.c
.
/*
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv){
int ifd, ofd;
char ofile[16] = "/dev/null";
char ifile[32];
char buf[32];
if(argc != 2){
printf("usage, %s file, will send contents of file 2 /dev/null\n",argv[0]);
exit(-1);
}
/* open files */
strcpy(ifile, argv[1]);
if((ofd = open(ofile,O_RDWR)) < 0 ){
printf("error opening %s\n", ofile);
exit(-1);
}
if((ifd = open(ifile, O_RDONLY)) < 0 ){
printf("error opening %s\n", ifile);
exit(-1);
}
/* copy from file1 to file2 */
read(ifd, buf, sizeof(buf)-1);
write(ofd,buf, sizeof(buf)-1);
printf("copied contents of %s to a safer place... (%s)\n",ifile,ofile);
/* close 'em */
close(ifd);
close(ofd);
exit(1);
}
Static Code Analysis
I won’t go over the already known preprocessor directives. For information regarding them check out the last post. I will however, go over the new ones we see here.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
The first preprocessor directive #include <sys/types.h>
contains some of the more common data types that are used in the system. Although these are meant to be used within the kernel, most of the system data types are accessible also to user code. Check out more information on this header file here. Or, look over the header file yourself with this command: less /usr/include/x86_64-linux-gnu/sys/types.h
Next, we have #include <sys/stat.h>
and this header file defines the structure of the data that is returned by the stat(), fstat(), and lstat() functions. Check out more information on this header file here. The header file is located here: /usr/include/x86_64-linux-gnu/sys/stat.h
The #include <fcntl.h>
directive shall define the following requests and arguments for use by the functions fcntl() and open(). This is mostly used to retrieve or modify open file flags given a file descriptor. Check out more information on this header file here. The header file is located here: /usr/include/fcntl.h
Lastly, we have #include <unistd.h>
and this directive provides access to the Portable Operating System Interface (POSIX) operating system API. Check out more information on this header file here. The header file is located here: /usr/include/unistd.h
As we can clearly see from the preprocessor directives, this is a much more robust program. We still have the same main() function declaration which tells us that we will need to supply an argument. We also have more variables declared than prior levels.
int ifd, ofd;
char ofile[16] = "/dev/null";
char ifile[32];
char buf[32];
First we have two integers declare, ifd and ofd. Then we have three arrays, ofile[16], ifile[32], and buf[32]. Immediately we see an issue here with the variable ofile[16] only having 16 bytes in value and the other two arrays having 32. Ok, getting an idea here. Let’s continue.
The next line is an if-statement telling us if the command-line argument, argc is not equal to 2 then print usage, %s file, will send contents of file 2 /dev/null\n
. Then an exit(-1)
? What?! We know that exit(0)
is a successful program termination and exit(1)
is an indication of an unsuccessful program termination. Looking ahead we see that the other if-statements both return the same exit(-1)
while the end of the main() exits with a exit(1)
. Let’s keep moving forward.
if(argc != 2){
printf("usage, %s file, will send contents of file 2 /dev/null\n",argv[0]);
exit(-1);
}
Next is a function called strcpy() which is actually kind of famous. Check out the man
page for strcpy() with the command man strcpy
and press Shift+gg
.
If the destination string of a strcpy() is not large enough, then anything might happen. Overflowing fixed-length string buffers is a favorite cracker technique for taking complete control of the machine. Any time a program reads or copies data into a buffer, the program first needs to check that there’s enough space. This may be unnecessary if you can show that overflow is impossible, but be careful: programs can get changed over time, in ways that may make the impossible possible.
Alright, so this function is known to cause buffer overflows and we can add this to our arsenal of possibilities. Looking at the function it accepts two arguments, ifile
and argv[1]
and we know from the variable declaration that ifile[32]
has a value of 32 bytes and argv[1]
is a pointer to the first command line argument supplied.
/* open files */
strcpy(ifile, argv[1]);
Then we have another if-statement that states if the variable ofd
equals the open() function with the variable ofile
filed and O_RDWR
flag set is less than 0 then print error opening %s\n
. The O_RDWR
is the File Access Mode
flag for the open() function. This is necessary to read and write to the file. Read more about it here.
if((ofd = open(ofile,O_RDWR)) < 0 ){
printf("error opening %s\n", ofile);
exit(-1);
}
The next if-statement is similar however, the open() function is opening the ifile
variable and the File Access Mode
flag is set to O_RDONLY
which is Read-Only and being compared to the ifd
variable.
if((ifd = open(ifile, O_RDONLY)) < 0 ){
printf("error opening %s\n", ifile);
exit(-1);
}
So, from what we can tell we will be opening two files and the first command-line argument will be copied into the ifile
variable.
Next we have the heavy lifting of the program. First is the read() function taking three arguments, the ifd
variable, the buf
variable, and then sizeof(buf)-1
. To better understand this we need to look at the read() function, man read
.
READ(2)
NAME
read - read from a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
DESCRIPTION
read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
So the last portion of the function sizeof(buf)-1
is the count bytes. So the function will read the file up to the size of buf
which is 32 minus 1 making is 31. The same goes for the write() function as well. Then finally the files are closed and the program exits. Easy enough.
/* copy from file1 to file2 */
read(ifd, buf, sizeof(buf)-1);
write(ofd,buf, sizeof(buf)-1);
printf("copied contents of %s to a safer place... (%s)\n",ifile,ofile);
/* close 'em */
close(ifd);
close(ofd);
exit(1);
}
Exploit Staging
With our static code analysis finished it’s time to start working on our exploitation of this program. We know the exploit will be a buffer overflow. What else do we know? We know that strcpy() will take everything from argv[1]
and copy to ifile[32]
. However, the buffer for ifile[32]
is only 32 bytes. If we supply an argument for argv[1]
that was longer than 32 bytes then we would have our buffer overflow condition. Ok, now we’re getting somewhere. We need a file that is larger than 32 bytes.
To make our file and to ensure that we are able to read and write to it I am going to create my file in the /tmp/
directory using python
. The file, including the /tmp/
need to equal 32 charcters.
python -c 'print(len("/tmp/" + "A"*27))'
mkdir -p $(python -c 'print ("/tmp/" + "A"*27 + "/tmp")')
cd $(python -c 'print ("/tmp/" + "A"*27 + "/tmp")')
This creates a directory in /tmp/
named AAAAAAAAAAAAAAAAAAAAAAAAAAA
which contains another /tmp/
directory. Now we can test our exploit theory by passing our current working directory to the narnia3
binary.
/narnia/narnia3 `pwd`/abc
We receive an error which tells us that our argument is too large however, the error did not provide the entire working directory. Lets examine and disassemble the binary to find out what is getting overrun. To do this we can use gdb
like before in the previous Narnia write ups or we can use objdump
.
A little bit about objdump
. If you have not used objdump
before then I would suggest playing around with this great tool. objdump
is a great command-line tool that can be used for a wide variety of things. Such as, listing headers for an executable, disassembling an executable and even display debug information. This doesn’t do objdump
justice and there are many more use cases. For more information check out the man
page here or type man objdump
. Now back to exploiting the narnia3
binary.
To disassemble the binary with objdump
we will be issuing the -d
flag. Futhermore, we will set our disassembly flavor to into with the -M
flag followed by intel
and then the path to the binary.
objdump -d -M intel /narnia/narnia3
Running the above command will fill the screen with the entire binary disassembled and might be a bit overwhelming. We will be focusing on the <main>
function.
Using the above screenshot as a reference because your memory addresses may be different. The first two operations are used to build the stack frame and you will typically see these at the beginning of every function.
804850b: 55 push ebp
804850c: 89 e5 mov ebp,esp
Then we have the stack pointer esp
with the value 0x58
which is the hex value 88. This is the space needed for our variables. Then we have the mov
operand to move a pointer to ebp-0x18
the hex value 24, then ebp-0x14
the hex value 20, then ebp-0x10
hex value 16, the ebp-0xc
the hex value 12, and lastly ebp-0x8
the hex value of 8 but this operand isn’t a mov
but rather a cmp
. The cmp
operand is used to compare the first source operand with the second source operand. Which all add up to 80 leaving room for local variables.
Exploit Development
So what do we know? We know that ifile[32]
is copied at 0x0804855a
. We also know from the source code that ifile
is copied from argv[1]
which is then copied into ofile
. So, in order for us to overflow ofile
the value must exceed 32 characters. Then we have 16 characters for ofile
which contains “/dev/null”. Let’s provide the executable an input file which contains or references the password and an output file which we control. To solve this, I created a folder within /tmp
called AAAAAAAAAAAAAAAAAAAAAAAAAAA
. The total length of the following string is 32 bytes. Therefore, anything that follows the above path will overwrite the output file. To solve that, I created an output file within /tmp
named output
. I also created an input file named output
within a folder named tmp which nests under /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAA/tmp
. If we symbolically link this file to the password file, we can use it as the input file and the output file will be re-written to /tmp/output
which is a file we can view.
If you missed out on Narnia Level 2 click the button on the left to check it out. If you’re all caught up and want to see more, check out the Narnia Level 3 by clicking the button on the right.