When you're exploiting software (legally hopefully ;) there are some common problems you might encounter. In this post I'm going to focus on three specific problems. I'll assume you're already familiar with basic buffer overflows and have tried to write one before. Oh and even if you were successful in writing the exploit, maybe you encountered some annoyances that are addressed in this posts. Let's go!
My exploit only works under gdb?
A common question people ask is why their exploit works when running the target program in gdb, but why no longer works when the program is started normally. There's actually another variation of this question: people wonder why they didn't obtain elevated privileges when executing the exploit under gdb. I'll first explain the elevated privileges problem and then we'll address the original problem.
No elevated privileges
When you are exploiting a suid program (e.g., for local privilege escalation) your exploit may work under gdb, yet you don't obtain any new privileges. First and for all, a "suid program" is a program that a normal user can execute, but runs under root privileges (to be precise it runs as the user that owns the program). Such programs are marked with an "s" suid bit. For example, the passwd utility is a suid program:
root@bt:~# ls -l /usr/bin/passwdThis makes perfect sense, as passwd has to be able to update /etc/passwd and /etc/shadow and this requires root privileges. As a side note this means that if we can exploit passwd we can elevate our privileges to those of root. To get back to our original problem: if we exploit a suid program under gdb we don't obtain elevated privileges. What's happening? Before we answer this question, one should first realize that this is actually wanted behavior! Otherwise we could simply open the suid binary in gdb and overwrite the current code using
-rwsr-xr-x 1 root root 37140 2011-02-14 17:11 /usr/bin/passwd
set *(unsigned int)(address) = valueThis way one could directly inject shellcode without exploiting anything. So being able to debug a suid binary as a less privileged user shouldn't be possible. Yet you seem to be debugging the suid binary anyway?! Well, when launching the targeted suid program using gdb no elevated privileges will be granted to the program. You can then debug the program, though exploiting it won't result in elevated privileges (since it was not given elevated privileges).
Different stack addresses
Another problem is that the stack addresses of variables, fields, pointers, etc. will change when the targeted program is debugged using gdb. Let's use the following program to investigate these stack differences:
When directly executing the program I got the values "env=0xbfffddbc arg=0xbfffddb4 esp=0xbfffdcfc" but when running it under gdb I got "env=0xbfffdd8c arg=0xbfffdd84 esp=0xbfffdccc". We notice that all the addresses have changed! Why did this happen? Well there's a reason the program also prints the environment variables :) Looking at the output we can see that our program was given different environment variables under gdb. These environment variables are saved on the stack. And if different environment variables are given the space required to save them will also be different. Because of these different space requirements, the stack addresses of variables saved on the stack will change. Looking at the stack layout in the simplified illustration below we see that this influence nearly all stack addresses under in a program:
When directly executing the program I got the values "env=0xbfffddbc arg=0xbfffddb4 esp=0xbfffdcfc" but when running it under gdb I got "env=0xbfffdd8c arg=0xbfffdd84 esp=0xbfffdccc". We notice that all the addresses have changed! Why did this happen? Well there's a reason the program also prints the environment variables :) Looking at the output we can see that our program was given different environment variables under gdb. These environment variables are saved on the stack. And if different environment variables are given the space required to save them will also be different. Because of these different space requirements, the stack addresses of variables saved on the stack will change. Looking at the stack layout in the simplified illustration below we see that this influence nearly all stack addresses under in a program:
lower addresses
esp
[argv]
[envp]
higher addresses
Remember that the stack grows towards lower addresses (in the illustration above it grows upwards).
One way to solve this is using the command "env -i ./program". This runs the program using an empty environment. However, when launching gdb using "env -i gdb ./program" and running the program, we notice that gdb still added some environment variables. Damn you gdb! One possible way to deal with this is to include these variables when directly executing the program using something like
env -i COLUMNS=97 PWD=/root LINES=29 SHLVL=0 /root/a.out
Note that gdb uses the full path to start the program, and that this path is given to the program in argv[0]. So we must also use the full path when directly running the program (since the arguments are also saved on the stack). Although the addresses are now the same, this is annoying to do manually all the time. Our approach also breaks a few bash-specific tricks because the SHELL variable is cleared (can be fixed by setting SHELL=/bin/bash). An easier solution is to use this script written by hellman. Directly running the program or debugging now becomes:
./r.sh ./a.out
./r.sh gdb ./a.out
Both runs will have the same stack addresses. Perfect!
Padding in structures, stack, etc.
This is really more of a remark. When given the source code of a program you know the general layout of structures and function stacks. However, you cannot predict actual offsets (i.e., the precise location of fields). The reason is that most compiles will add padding. This is done so that fields are 2-byte or 4-byte aligned (or anything else that your compiler deems appropriate). The introduced padding can be seemingly random. So while you can use the source code to quickly detect vulnerabilities, you should still disassemble the compiled binary to calculate the offsets.
A common question is then "why does my debugger add X amount of padding bytes?" A frequent answer would be for performance, which is hardware/processor dependent. The answer can change for different versions of the compiler as well. There's just no general answer here. Also, another thing the compiler can do is change the order of variables on the stack, say placing a function pointer before a buffer even though it's not declared like that in the source code. (This way overflowing the buffer won't affect the function pointer, and used in combination with stack canaries this is used to decrease the potential impact of buffer overflows.)
Placing a Suid Script or Suid Shell as Backdoor
Alright. Say you've successfully exploited a vulnerability. Then it can be convenient to create a backdoor to easily obtain elevated privileges at a later point in time. Two seemingly easy strategies would be to either create a suid script or copy the /bin/sh executable and making it suid. Unfortunately the first strategy is not possible on Linux, and the second strategy needs some special attention. The first strategy fails because even if the script file is marked with suid, the kernel doesn't grant elevated privileges when starting scripts. Let's confirm this behavior in detail by inspecting the linux kernel. Essentially we need to learn how the kernel starts script files. Remember that scripts are treated as real executables and can be started using something like
So let's assume that a script is being started using the execve system call. This function is implemented in the do_execve_common function in the linux kernel. Essentially it checks for errors, fills in a so-called binary structure parameter, and then calls search_binary_handler [1]. The real work is being done in this last function, which consists of scanning a list of registered binary formats until a match is found, and then calling that handler. Scripts are detected by checking if the file starts with "#!". The handler for scripts is located in binfmt_script.c in the function load_script. In this handler you don't explicitly see something like "don't grant suid to script files". In fact you see no mention of the suid bit at all. But that's the point, suid is never granted in the script handler. On the other hand, if we look at the handler for ELF linux executable, we notice that suid is explicitly set using SET_UID and SET_GUID. [2] The reasons scripts are not run as suid is because it's too easy to write insecure suid scripts.
Padding in structures, stack, etc.
This is really more of a remark. When given the source code of a program you know the general layout of structures and function stacks. However, you cannot predict actual offsets (i.e., the precise location of fields). The reason is that most compiles will add padding. This is done so that fields are 2-byte or 4-byte aligned (or anything else that your compiler deems appropriate). The introduced padding can be seemingly random. So while you can use the source code to quickly detect vulnerabilities, you should still disassemble the compiled binary to calculate the offsets.
A common question is then "why does my debugger add X amount of padding bytes?" A frequent answer would be for performance, which is hardware/processor dependent. The answer can change for different versions of the compiler as well. There's just no general answer here. Also, another thing the compiler can do is change the order of variables on the stack, say placing a function pointer before a buffer even though it's not declared like that in the source code. (This way overflowing the buffer won't affect the function pointer, and used in combination with stack canaries this is used to decrease the potential impact of buffer overflows.)
Placing a Suid Script or Suid Shell as Backdoor
Alright. Say you've successfully exploited a vulnerability. Then it can be convenient to create a backdoor to easily obtain elevated privileges at a later point in time. Two seemingly easy strategies would be to either create a suid script or copy the /bin/sh executable and making it suid. Unfortunately the first strategy is not possible on Linux, and the second strategy needs some special attention. The first strategy fails because even if the script file is marked with suid, the kernel doesn't grant elevated privileges when starting scripts. Let's confirm this behavior in detail by inspecting the linux kernel. Essentially we need to learn how the kernel starts script files. Remember that scripts are treated as real executables and can be started using something like
execve("/myscripts/somescript.sh", argv, envp);where we assume somescript.sh starts with "#!" as usual.
So let's assume that a script is being started using the execve system call. This function is implemented in the do_execve_common function in the linux kernel. Essentially it checks for errors, fills in a so-called binary structure parameter, and then calls search_binary_handler [1]. The real work is being done in this last function, which consists of scanning a list of registered binary formats until a match is found, and then calling that handler. Scripts are detected by checking if the file starts with "#!". The handler for scripts is located in binfmt_script.c in the function load_script. In this handler you don't explicitly see something like "don't grant suid to script files". In fact you see no mention of the suid bit at all. But that's the point, suid is never granted in the script handler. On the other hand, if we look at the handler for ELF linux executable, we notice that suid is explicitly set using SET_UID and SET_GUID. [2] The reasons scripts are not run as suid is because it's too easy to write insecure suid scripts.
Now to address the second problem. First, on my machine /bin/sh is a symlink to /bin/bash, so the remaining discussion will be specific to bash. Anyway, as mentioned copying /bin/sh to something like hiddenshell and making it suid can be problematic: You'll notice that starting your copy called hiddenshell won't grant you a suid shell. This is because bash automatically drops its privileges when it's being run as suid (another security mechanism to prevent executing scripts as suid). Looking at the source code of bash confirms this:
We see one interesting global variable in the if test: privileged_mode. Looking further into the code one learns that this flag is set when the "-p" parameter is given. So starting your suid shell using the "-p" parameter won't drop privileges! That solves our problem. To create a backdoor we copy /bin/sh and make it suid. The backdoor shell must then be started with the "-p" parameter.
But even though we solved the original problem, a new question arose. That new question is: Why does calling "system(command)" work in a suid binary. That is, when a normal suid binary calls the system function, that supplied command is also executed as being a suid program. Remember that system(command) will fork the process and then use execve to run "/bin/sh -c command". If bash always drops privileges, the command shouldn't be executed as suid! Let's first look at the code of the system() function in the glibc library:
What's different from our situation? It may seem pretty silly, but the difference is the name of the executable. Yes, try renaming hiddenshell to sh and then execute it again. Now you will get a suid shell even without supplying the "-p" parameter. Apparently my bash installation doesn't drop privileges when it's started using the "sh" command, probably to preserve backwards compatibility. Interestingly this is not behavior defined in the original source code of bash. No, it's a patch added by several linux distributions. See for example the changelog of bash for ubuntu (search for "drop suid"). It's implemented by updating the if test to:
if (running_setuid && privileged_mode == 0 && act_like_sh == 0)There, that concludes our very detailed discussion of suid scripts and suid shells!
disable_priv_mode ();
[1] Playing with binary formats, marzo, 1998
[2] I'm actually not happy with this explanation at all. Unfortunately I don't understand the linux kernel well enough to give a really decent explanation. If you know more about this please comment!