The basic threat makes some functions pass (return success) even if they fail — like even if the password that is provided is wrong. In this article, we will discuss how a poorly written application involving function calls could get hacked, how gdb
can be invoked in batch mode, and how to write gdb
scripts and use them to automate the hack. We also look at counter measures for developers to safeguard their applications.
Basic concepts
Usually, a program is divided into a set of functions, each doing a specific task. Once a function is invoked, it has (in general) two characteristics:
- It returns a value — the status of the function call; whether it passed or failed. The caller can take action based on this return value.
- Side-effects — the effects of the function on the global state of the program (like updating a database or a global variable, etc).
Not all functions have side-effects, but almost all functions return a value. The return value normally decides the flow of the program after that call. What if a hacker manages to change the return value of a targeted function? This causes application logic (more precisely, the flow) to be changed to the hacker’s advantage — we don’t want this!
Consider an application that has a function named authenticate
. This function takes an input string (password
), and returns 0
if the password is good — or returns 1
otherwise. Hackers can gain access to the application by changing the return value of authenticate
.
Victim application: An example
For this article, we have a test application (hackMe.c
) shown below, where the authenticate
function is called by main
to check if the application should continue execution or not. The application exits if the input string passed to it is not equal to PASS
. The program command-line argument is passed to main
, and subsequently to the authenticate
function.
#include<stdio.h> /* * Returns 0 on success, and * Returns 1 on failure */ int authenticate (char *test) { if(strcmp(test,"PASS") == 0 ) return 0; /* success*/ else return 1; /* fail */ } /* USAGE: . /hackMe FAIL */ int main(int argc, char *argv[]) { int retVal = -1; if (argc< 2) { printf ("\n USAGE: %s <PASS|FAIL>", argv[0]); exit (-2); } /* skipping any checks on argv to keep it simple*/ retVal = authenticate(argv[1]); /* WE WILL USE GDB TO ALTER RETURN VALUE OF THIS CALL JUST BEFORE COPYING THE RETURN VALUE TO VARIABLE retVal */ if( retVal == 0) printf("\n Authenticated ...program continuing...\n"); else { printf("\n Wrong Input, exiting...\n"); exit (retVal); } /* . . Rest of the program . . */ }
Compile and link the program (without any debug information!) with the following command:
gcc hackMe.c -o hackMe
Analysing the victim binary
Here are the results of invoking this program with different command-line arguments:
[raman@Linux32 article]$ ./hackMe PASS # [R1] Authenticated ...program continuing... [raman@Linux32 article]$ ./hackMe FAIL # [R2] Wrong Input, exiting... [raman@Linux32 article]$ ./hackMe 123 # [R3] Wrong Input, exiting... [raman@Linux32 article]$
The program continues execution only if the input string supplied as a command-line argument matches some criteria (in this case, a fixed string, PASS
). But what exactly causes it to fail in scenario R2 and scenario R3? Let’s make use of nm
— this command lists all the symbols in a binary.
[raman@Linux32 article]$ nm hackMe | grep " T " 08048390 T authenticate 080484d4 T _fini 08048278 T _init 0804847c T __libc_csu_fini 0804844c T __libc_csu_init 080483c2 T main 080482e0 T _start [raman@Linux32 article]$
There are two functions of interest: main
and authenticate
. All the rest are provided by GCC. The developer didn’t take enough care while naming — authenticate
seems to be the weak function! It’s a guess, but let’s dig deeper.
Running the victim application in gdb: Mounting the attack manually
Let’s apply the mechanism discussed in the basic concepts, and see if this application can be compromised! The session below shows the steps performed with gdb
(I used v6.8):
[raman@Linux32 article]$ gdb ./hackMe (#1) GNU gdb Red Hat Linux (5.3post-0.20021129.18rh) Copyright 2003 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux-gnu"... (gdb) br authenticate (#2) Breakpoint 1 at 0x8048396 (gdb) r FAIL ---------------------- > Program should fail(#3) Starting program: /home/rdeep/article/hackMe FAIL Breakpoint 1, 0x08048396 in authenticate () (#4) (gdb) stepi 0x08048399 in authenticate () (gdb) step Single stepping until exit from function authenticate, which has no line number information. 0x0804840e in main () (gdb) p $eax (#5) $1 = 1 (gdb) set $eax=0 -------------------------- > But we hacked! (#6) (gdb) p $eax $2 = 0 (gdb) c (#7) Continuing. Authenticated ...program continuing... ---- > And, reflected here!
Before explaining what is happening in this session, let’s recall some gdb
commands:
stepi
— Step one assembly instruction at a time.step
— Step program until it reaches a different source line; when the program is compiled without any debug information, this command stops just before coming out of the current function.set
— Set the value of a variable or register, etc.$eax
, in this command, refers to the processor register EAX.- To pass small (like
int
) return types, GCC uses the EAX processor register on the Intel platform.
Armed with this information, let’s find out what is happening in the above session. I loaded the (release mode) program in gdb
(#1), and inserted a breakpoint in the authenticate
function (#2) — the one we need to manipulate. Then the program is started, and the command r
(#3) provides a command-line argument, FAIL
. Since this is a wrong password, the program should end with a failure message. However, we intervene (with the help of the great gdb
!) and alter the program flow.
As soon as the program is run, it hits the breakpoint in the authenticate function (#4). We want control back with us as soon as gdb
has finished executing authenticate
and the program flow is just back into main
. This is achieved by two GDB commands: stepi
and step
. By the time step
completes, we are back in main
and the return value of authenticate
is available in the EAX processor register.
As you can see, the return value was 1
(#5); we changed this to 0
(#6), and then we continued the program with the command c
(#7) — and all this happened before copying the return value into the variable retVal
. So, here is the result; we changed the normal execution flow of the program. The program that was supposed to fail has passed!
This approach is simple because:
- No coding is involved.
- Performance hit is minimal.
- It can be used against any function in the application, provided the function side-effects do not alter the overall program flow.
- No debug information is required in binary; it works on release-mode binaries, as long as they are un-stripped.
Automating the attack: GDB scripts at work
GDB can be invoked in batch mode, wherein it reads inputs from a file (called a GDB script) instead of reading interactively. We will be looking into two gdb
options, -batch
and -x
. The –x
switch takes the name of a file from where gdb
reads commands. Please refer to man gdb
for more details.
Let us write a script with GDB commands in it, to automate the steps we performed in the previous section. We can name our script hackScript.gdb
, and here are its contents:
file ./hackMe br authenticate r FAIL printf "n ----> Breakpoint HIT...n" stepi step printf "nn ----> I AM OUT OF THE TARGET FUNCTION, BACK IN MAIN: RETURN VALUE HERE(BEFORE HACK) IS %d", $eax printf "nn ----> SETTING THIS VALUE TO 0n" set $eax=0 printf "nn ----> EAX AFTER HACK IS %d", $eax c
This script loads the binary, and then inserts a breakpoint with the command br
. Let us use the GDB command printf
(similar to LIBC printf
) to print logging information. Given below is the output to run this script in gdb
:
[raman@Linux32 article]$ gdb -x hackScript.gdb -batch Breakpoint 1 at 0x8048396 Breakpoint 1, 0x08048396 in authenticate () ----> Breakpoint HIT... 0x08048399 in authenticate () Single stepping until exit from function authenticate, which has no line number information. 0x0804840e in main () ----> I AM OUT OF THE TARGET FUNCTION, BACK IN MAIN: RETURN VALUE HERE(BEFORE HACK) IS 1 ----> SETTING THIS VALUE TO 0 ----> EAX AFTER HACK IS 0 Authenticated ...program continuing...
Counter measures
- Obfuscation — this technique involves changing the function names to some hard-to-guess names. This can be achieved by just using the
#define
construct in C. - Use function side-effects — don’t just make decisions on the return value, use an additional criterion.
- Use a challenge-response mechanism.
- Strip the binary so that it’s difficult to determine symbols (functions, variables, etc.) and their address in the binary.
- Include fake code before exiting the application, so that it’s hard to detect the location where the program exits.
We learned how GDB can be used to hack an application if the hacker has sufficient information about the key functions. Although key functions can be obfuscated, some level of R&D with the application can undermine the security of the application.
The application used in this article was built in release mode, though un-stripped. The vulnerability can be reduced by stripping (with the strip
command) before shipping. However, even stripping the binary may not be very effective, as some smart minds have an UNSTRIP program. Stripping a binary may also result in debugging challenges in the field.
What next?
I hope you find this article useful for educational purposes. The idea discussed can be extended (in a positive manner) to build a logging mechanism for a particular application, in order to debug the behaviour of the application — and, in some cases, alter it!
In the next article, we will try to make better use of this technique by digging a little deeper into the glibc
interface layer, and explore more threats that our application will have to face in the real world!
References
1. GDB man page
2. UNSTRIP
Hi Raman Deep,
I’m a beginner at gdb. Tried your hack-steps mentioned above in “Running the victim application in gdb: Mounting the attack manually”. But when I gave the “c” command to continue, it didn’t pass i.e the code exited with a return value of “01”. Also I saw that the EAX register on my machine had some random 5 digit value each time I tried debugging, instead of a 1 as yours.
Can you tell me what should be the problem? Whether its with the EAX register or any other way that you can suggest to have this issue sorted?
Thanks!
In practice, the target would likely be running as a SUID binary with higher permissions, preventing the above from working.