This Post is about different issues and challenging moments I encountered during my learning experience with Buffer Overflows and wished I knew before. This is not a guide how BOFs are working or how to learn this topic.
Watch these two videos from LiveOverflow, these will really save you some time if your exploit wont work and you don`t know why.
In this post I will often link to LiveOverflows videos, this is not because I want to promote his content, (besides his content is every minute of time worth!) no, it is because I watched his series Binary Exploitation very long time after I started with BOFs and I wish I would only have seen these videos much sooner, as that would have saved me many hours of struggling and guessing!
If you start with BOFs you just want to put your shell code in the stack and point to it, to get this working without any gadgets or extra pointers you need two things
- A binary where ASLF is turned off
- A System where ASLF is turnend off
Otherwise you stack address will always change and you will not be able to jump to a desired address.
1. to check the binary use one of the following tools
checksec <binary> #or rabin2 -I <binary>
2. make sure ASLF is turned off or you cant find a static memory address (outside of debugger)
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
This won’t survive a reboot, so you’ll have to configure this in sysctl. Add a file /etc/sysctl.d/01-disable-aslr.conf containing:
kernel.randomize_va_space = 0
32bit vs. 64bit
Beside the 4bit vs. 8bit difference in the addresses there are some other differences, mainly how the architectures handles the function parameters
- 32bit stores them on the stack
- 64bit stores the first x in registers and then on the stack
check out LiveOverflows Video about the differences.
if you cant debug 32bit binaries check out my blog post about this issue.
GDB vs. no GDB vs. GDB.debug vs. GDB.attach
Addresses are different between starting the program with gdb and outside of gdb and also between gdb.debug and gdb.attach because of defined starting parameters like env and pathes that are stored on the stack. The savest way would be using the gdb.attach medthod from pwntools if you need the really exact stack address.
example start gdb from pwntools with payload as parameter
target = gdb.debug(['./buffer-overflow',payload])
Cat the Shell
if the shell is executed but closed immediately we can use
cat for reflecting STDIN to the shell, see how this works in one of LiveOverflows YouTube Videos.
(python3 exploit.py; cat) | ./buffer_overflow
GDB cant Disassemble a Function
if you get something like: „No function contains specified address.“ this is because gdb cant find a entry address for the function, likely because it is never called or its dynamic linked via plt, but you can still force gdb to disassemble it, you need to pass the start and end address or start + offset
disass 0x00402200, +16
More Infos about this on StackOverflow
NOPs vs. Null bytes
- Use NOPs (0x90) as a „placeholder“ and for let the pointer address slide to your payload (so called NOP slide)
- Use Null Bytes to close / terminate a payload or pointer address, but you cant use null bytes in a argument or when strcpy is called.
- some functions like strcpy or gets will stop copying the bytes when they get null bytes or 0x0A bytes
- you can remove 0 byte operations with push and pop https://security.stackexchange.com/questions/91969/removing-null-bytes-from-shell-code
- if possible place the shellcode behind the pointer, because then you make sure that the shellcode dont corrupt itself during runtime
Test your Pointer / Call
You can use b“\xcc“ (a debugger breakpoint) to test if the code is executed, if you receive a SIGTRAP then the program hit the code
Python2 vs. Python3
There is a difference between how python2 and pyhton3 sending bytes https://reverseengineering.stackexchange.com/questions/13928/managing-inputs-for-payload-injection
you can send raw bytes in pyhton 3 with the following import
python3 -c 'import sys;sys.stdout.buffer.write(b"A" * 12 + b"\x6a\x3b\x58\")
Calculating vs. Trying
As i started with BOF I always just „tried and error“ to find the correct offset or amount which I need to overflow, that is ok and sometimes the only possible way to figure out the correct padding but sometimes its also worth to try to find and understand the BOF Vuln. with infos from Ghidra
for example: if you see in the code something like this
char local_18 ; read(0,local_18,0x100);
go ahead and use https://www.calculator.net/hex-calculator.html to calculate the amount of chars to override the stack
0x100 = 256 bytes (reads) – 40 bytes (buffer) = 216 (payload)
40 bytes to reach the stack 216 bytes for your payload
or you can use ltrace to check the exact number of bytes a challenge binary is attempting to read.
Payloads and Errors
Try different Payloads! Some work and some may not work even if they reach complete and correct the stack. I spend hours to figure out why some basic shell code does not work and other works. So if you can confirm that the payload is in the stack and is executed but the program crashes, just try another with a different size.
If you receive erros like „Illegal instruction“ or SIGILL try different offsets / places for you shellcode, maybe you are on a wrong stack position and the shell instructions destroy the further instrunction by itself, then you just need to store the shellcode on another position in the stack (watch the mentioned videos from LiveOverflow)
There are many pitfalls with Buffer Overflows, this post was an extract of my notes which is maybe more up to date in the future: Buffer Overflow Cheat Sheet