The most common issue we encounter while exploiting buffer overflow is that our payload works fine within gdb but fails miserably on actual vulnerable executable and we are left with no clues regarding what went wrong. Within gdb we have all the comfort of debugging the program and checking the register/memory contents at each step. But we cannot do the same on the executable without gdb for e.g. when we are trying to elevate our privilege using buffer overflow. It may create lot of frustration when we have no clue how to sort out the issue. Wouldn’t it be great if we can attach some “debugging” functionality to the executable without gdb and know the reason for the failure. In this blog, we will look into some of the possible reasons of failure outside gdb and how to create our payload with almost 100% success rate.
Who is the culprit?
Most of the times exploits fails due to environment variables. In the techniques like ret2libc, we are dependent on the environment variables to get the argument (e.g /bin/sh). To execute any program, it is first loaded to the main memory and there it occupies its own memory space (e.g. stack, heap). Stuffs like local variables and buffers resides on the stack. Interestingly, environment variables are also loaded onto the stack. Any changes in the environment variable will disturb the entire stack and possibly the address where local variables and buffers (which is important for writing payloads) are loaded. The key for successful working exploit is to make both the environments (within gdb and outside gdb) as close as possible. This can be achieved by following few approaches like
- Remove extra environment variables added by gdb. When the program is loaded using gdb some extra environment variables like LINES and COLUMNS get added.
Get rid of them from gdb using set exec-wrapper env -u LINES -u COLUMNS
- Remove all the environment variables from the shell before executing program or starting gdb using env -i sh
- Use full path of the executable both inside and outside gdb. e.g gdb /tmp/vuln and /tmp/vuln
- Run gdb and executable from the same location. This will eliminate problems due to environment variables like PWD, OLDPWD
- Instead of adding new environment variables, modify existing variables. For e.g. modify the existing environment variable USER=someuser to USER=/bin/sh. Adding a new environment variable may disturb the stack which can case issues later.
- While modifying the existing environment variable, try to keep its length same/close to the previous one. For e.g. if you are modifying environment variable “USER=somelinuxuser“, make new environment variable as “USER=/tmp/file1234“. Note that value of both the environment variables has 13 characters.
These tricks will increase the probability of successful exploit but what if we still get segmentation fault or the result is not what we expected? Now here comes the interesting part. 😉
Adding real-time debugging capability
Most of the times our attempt to exploit buffer overflow using ret2libc fails outside gdb because though we are pointing to “/bin/sh” in gdb but when the executable is run directly, the same memory location points to somewhere else. The biggest challenge is how we can adjust our payload to point to the particular memory location during run time. For this we will be exploiting buffer overflow to add some debugging capability which will eventually help as to achieve our objective.
We will use ret2libc technique to call “printf“. The advantage of using printf in ret2libc technique is that it is easy to use and even gives us the quick confirmation whether we are able to successfully exploit buffer overflow. Passing the memory address as an argument to the printf function will give the content of that memory address. For debugging purpose, we can pass the memory address of the argument (e.g. /bin/sh) to printf function to “verify” if the address points to the same string at run-time. The output will print some part of environment variable from the stack which may not point to our desired location (i.e. /bin/sh) but will in close proximity of it. Then use “env” to see the environment variables and adjust the payload accordingly by modifying the memory address till we end up in printing the desired string (e.g /bin/sh) using our payload. Below will be the structure of the payload for debugging purpose.
padding = "A"*n #padding till EBP
printf_addr = "\xaa\xaa\xaa\xaa" #address of 'printf' in libc
exit_addr = "\xbb\xbb\xbb\xbb" #address of 'exit' in libc
env_addr = "\xdd\xdd\xdd\xdd" #address of string '/bin/sh' from environment variable
payload = padding + printf_addr + exit_addr + env_addr
Few trial and errors will be required to get the exact address of the argument. Once we get that address, we can use it to get the shell by calling “system” using ret2libc technique. These neat little tricks will be of immense use when we can use ret2libc to exploit buffer overflow vulnerability and they will guarantee almost 100% success. This will eliminate the uncertainties of environment variables and will add powerful feature to debug our program and look into the memory content at run-time.
I hope this article was useful. If you have any questions or suggestions please leave you comments. Subscribe to the mailing list to get updates for my future articles. Feel free to check my other articles on buffer overflow series.
Happy Learning 🙂
The author is a security enthusiast with interest in web application security, cloud-native application development and Kubernetes.