0x0. Introduction
A buffer overflow (bof), or buffer overrun, is an anomaly whereby a program, while writing data to a buffer, overruns the buffer's boundary and overwrites adjacent memory locations (Wikipedia).Well, let's talk about this very old Software Vulnerability and let's see if this still counts.
Since the very first article of Aleph One (Elias Levy) in 1996, " Smashing The Stack For Fun And Profit ", 27 years have been passed. The answer to the question "Is this vulnerability still active?" is a big: YES!
But Indeed, nowadays, with modern Software Development Environments & languages (.Net, J2EE, RoR, etc) it is not so easy to perform such attack, but... the more generic applications that was created in languages that are vulnerable to buffer overflow (such as C or C++) still exist, and they are many: Web servers, Operating Systems, RDBMSs and generally, almost everything our "safe" applications we put to run on...
In this series of articles I will try to explain to newcomers how such attack can be performed in modern environments and Operating Systems.
Since this specific attacks has to cope with memory architecture, OSs architecture and specific compilers implementations there are several assumption we must take.
We will discuss such assumption on every architecture we choose.
NOTE: The examples that you will see here make heavy use of memory addresses. It is very unlike to see the same memory addresses in your systems in case you try to test my examples by your own. This is not a bug, but a feature!
Let's start with Linux...
0x1. Linux 64bit on 64bit application
Let's create our demo application in C language to complete the test.
The source code is this:
#include <stdio.h>
#include <string.h>
int checkProductKey(char *userKey) {
char key[12];
strcpy(key, userKey);
int n = (strcmp(userKey, "123-456") == 0);
return n;
}
int main(int argc, char* argv[]) {
char key[255];
if (argc != 2) {
printf("Enter product key >");
scanf("%s",key);
}
else
strcpy(key, argv[1]);
int iAllow = checkProductKey(key);
if (!iAllow) {
printf("Wrong key!\n");
return -1;
}
printf("Welcome to the DEMO SA Application.\n");
printf("(c) 2023 all rights reserved.\n");
return 0;
}
This very simple demo program will get a product key as input and if the product key is correct will continue to the main flow, otherwise it produces an error and exits.
The input can be given from command line argument or (if no arguments provided) it will ask the user to enter it.
In order to be able to make a successful attack, we make some assumptions by using specific compiler flags.
I compile the program as follows:
Let's explain the flags:
- -m64: create an executable in 64bit architecture (to be honest I could omit this in the specific system since it is used as a default).
- -g: to produce special metadata for the debugger (that we will use later)
- -fno-stack-protector: do not perform memory stack checks (protections).
- -z execstack: allow to execute code on stack segment (in memory).
- -o demo: name the final executable demo .
The specific example has been implemented in KALI Linux 2022.4 :
To this, in kali just enter the command as root:
In any time you want to rollback this ASLR check, just enter this:
Btw, if you want to check, what is the current state of ASLR, enter this:
- 0: means OFF
- 2: means ON
In the following image we can see a normal execution of my small demo program.
As you can see there are two ( at least !!) main vulnerabilities in the program: Is where the vulnerable strcpy function is used and let's see this in practice:
As you can see, this is the way to test if our program is vulnerable to a buffer overflow attack: We give a very large string and then we check the program's response. If we get a 'segmentation fault' then it is possible to have a bof (buffer overflow) vulnerability...
0x1.0x1. The ROP approach
According to Wikipedia: Return-oriented
programming (ROP) is a computer security exploit technique that allows
an attacker to execute code in the presence of security defenses such as
executable space protection and code signing .
Our main goal here is to take advantage of a program's vulnerability in
order to redirect the program's flow to where we like (and where we can,
of course)...
Let's examine the program's behavior using the gdb debugger.
We put a breakpoint at line 7, immediately after the strcpy , inside the function checkProductKey .
We run the application inside the debugger ( just enter
dbg ./demo
) by passing a normal string, in order to check where the RET address is located (more about this below) is...Let's discuss some "things" here...
The image above is divided into 2 consoles
- The left console is the program disassembly code that I get after setting the breakpoint at line 7
break 7
and enterdisassemble /s main
. The "/s" parameter instructs the debugger to produce the assembly code along with the corresponding C source code (thanks to the -g flag on the compilation phase). - The right console is the actual console we work on testing the program.
- Note also one very important thing: the memory addresses (defined in blue color) are the same in both images. This is very tricky thing in real situation because these addresses may not be always the same every time we run out program. This is because of ASLR, and this why we disable it, above, otherwise we may end by hunting... ghosts, believe me!
As you can see in GREEN BOXES we run the program in debugger by passing ten "A"s as argument:
run AAAAAAAAAA
This is stored in the stack as the function checkProductKey is called since it is passed as argument to the function.
We can see the stack in memory by entering the
x/24xg $rsp
command in the debugger. This command instructs the debugger to show
in hexadecimal format "x", the next 24 memory positions in giant ("g")
8-bytes format. The $rsp is the well known Stack Pointer (the old ESP on 32bit systems) where in our case points the variables that passed as arguments inside our function.
The "AAAAAAAAAA" are represented here in hexadecimal format ("x") by the number "41" that the ASCII hexadecimal representation of the letter "A".
Also, note that the 10 "A"s are stored in the memory location in reverse order , since the little endian architecture .
Thus, the actual string that is kept on stack (on memory) is the '41.41.41.41.41.41.41.41.41.41.00' (I put the "." just to make it more readable).
Note that the "00" is the Null Character that denotes the string termination. Remember this "00" because plays an important role (as a barrier) in exploit development in general, and specifically in shellcode (or bytecode) creation (more on this in part II). The main point to know here is this: The reading (or sometimes the execution) of a series of memory locations is stopped when the system meets a 00 byte.
Focus now on RED BOXES and remember where we stand: We are inside the function checkProductKey .
When the function ends, the program's functionality must return to the place that was called. To be more specific: must return to the address of the caller.
In order for the system to remember this address, it stores it in a specific memory location inside the buffer of the current function. We call this address: the RET (RETurn) address . It is one of the most important address of the buffer overflow attacks and is considered as the "holly grail" of any buffer overflow exploitation.
As you can see in the red box on the right console, the RET address is stored at the end of the buffer, after the address of our input string "AAAAAAAAAA" and some other addresses (as the Base Pointer, some environment variables, etc, that they are important... but not so important for now).
As you can imagine if we enter a very big input string, greater than what the system has booked (in our case is 12 - because of the
char key[12];
at line 5) then we can overwrite all addresses that follows this variable, including the RET address . This is very important and very tricky! and this is why:
Suppose that we entered this "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" as input.
Our buffer can hold only the first 12 characters (or bytes in i386 architecture) including the string terminator "00". So what about the remaining characters? they will overwrite the neighboring memory addresses... RET address including.
So the RET address will be filled with "A"s thus "AAAAAAAA" or "4141414141414141".
Note that I choose 8 bytes to denote the address in 64bit systems, on purpose.
In general, you should know that in 64bit systems (as opposed to the 32bit systems) we have:
- General purpose registers have been expanded to 64-bit. So we now have RAX, RBX, RCX, RDX, RSI, and RDI.
- Instruction pointer, base pointer, and stack pointer have also been expanded to 64-bit as RIP, RBP, and RSP respectively.
- Additional registers have been provided: R8 to R15.
- Pointers are 8-bytes wide.
- Push/pop on the stack are 8-bytes wide.
- Maximum canonical address size of 0x00007FFFFFFFFFFF (more on this later).
$RIP = RET address
is executed. Thus, the functionality of the program will return to the
caller, since the RET address keeps the caller address, and to be more
specific it keeps the address of the next instruction of the one that
call the function. So, in our above theoretical example the RET address will be filled with "4141414141414141", that means that the program, at the end of the checkProductKey function will try to go to the address 0x4141414141414141.
But such address does not exist in our program's context, so we get the well know error:
segmentation fault
By having in mind the above knowledge let's try to exploit our program.
By examining the $rsp structure in our initial example (in the image above) we can see the we need 3 x 8 bytes in order to meet the RET address.
Let's prove this with the following example:
As you can see we enter 24 x"A" = "AAAAAAAAAAAAAAAAAAAAAAAA" and the 8 "B"s "BBBBBBBB" overwrite the RET address.
The program crashed...
What we would like to do actually is to we overwrite $RIP with an invalid address. But, in fact we don’t control $RIP at all. We have control only on the RET as you already see.
For your info you must know this:
The maximum address size we can handle in 64bit architecture is 0x00007FFFFFFFFFFF. What we did, is that we overwriting $RIP (via RET) with the non-canonical address of 0x4242424242424242 which causes the processor to raise an exception.
[If you have problems understanding the situation we are, you can read more about 64bit address architecture here .]
So the goal was to find the offset with which, to overwrite RET and consequently $RIP with a canonical address .
For this reason I use a cyclic patter (say "AAAAAABBBB" etc) and try it until I end up with a needed address (and yes, I know that there are other methods published that calculate the required offset differently, but this one I presented here also works for me ).
So, I will go to replace "BBBBBBBB" with an existing address of the existing source code ( code segment ) in order to bypass the product-key checks.
This address is the following:
This is where all the checks about the product-key has been passed, and this is what I call ROP (see the paragraph title).
I need to replace "B"s with this address: 0x0000555555555261
But I have to pass its value as bytes... not string! How to do this?
There are several ways to pass this string as "bytes" in the debugger:
I will choose the quicker one... take a look here:
Let's examine a little the attack "string" : Here I use a bit python v.2 (v.3 also works if I put the print string in "(" and ")" ) command :
python2 -c 'print "1"*24 + "\x55\x55\x55\x55\x52\x61"[::-1]'
Instead of writing an explanation in text here I will show the image of the result when I execute it in command line: As you can see, I pass the above results, as a command line argument to the dbg using the
run $( <SYSTEM_COMMAND> )
notation. This is an easy way to pass the command line string as bytes into a program.
The program crashes, but at the end, I made a succefull redirection as you can see the 'Welcome' message (in the green box above). Thus, I bypass the checking mechanism.
Note also, the way that I pass the goal address, in reverse order : the
[::-1]
python notation just put the hexadecimal string in reverse. Thus, the " 55.55.55.55.52.61 " will be put in the string arguments as " 61.52.55.55.55.55 " (remember the little endian architecture I mention above).
Important note: the " 555555555261 " is put in the memory in reverse order per pair : the " 55.55.55.55.52.61 " will be put in memory as " 61.52.55.55.55.55 ".
And in general, any memory address we will read, is a series of pairs - or a series of 1 byte (8bits, from 0 to 255 - aka 2^8=256 possible different values)
"Historically, the byte was the number of bits used to encode a single character of text in a computer and for this reason it is the smallest addressable unit of memory in many computer architectures." (wikipedia)
The same applies to i386 64-bit Windows 10 and i386 64-bit Kali Linux .
In addition, the '\x' notation indicates that I am talking to the program not in decimal but in hexadecimal.
I do this because the debugger displays memory addresses in hexadecimal notation.
If you wander why the debugger prefer the hexadecimal notation, I would said this:
The decimal representation of the 555555555261 haxedcimal address is this number: 93824992236129 . Well, it looks more than a telephone number, huh?
So... it is not so easy the human to cope with so big length numbers. The hexadecimal notation is more compaq and censequently managable. This is why has been adopted by (almost) all debuggers in the world to represent addresses.
So far I have performed a "goto" (remember in BASIC the goto statement?) or a JUMP (assembly-wise) by using the bebugger.
What I need now is to do the same in the final executable from the command line in a console, NOT by using the dbg .
Well, the answer looks fair enough: I just run this from command line:
As you can see I just open the Welcome screen without entering any product-key and I just created my first exploit of buffer overflow...
Note that the results in the image above it is not always so obvious, especially when we mess with memory addresses and the stack directly. Several times what I see in the debugger is not the exactly the same of what I see when I execute the program directly from command line and this is very common when I have to refer addresses in the stack (and not on the code segment as I did here).
We see such example in the Part II of this series of articles, when I will show how to put a "command shell" in the stack and how to execute it. In addition in Part II we will see what is a a bytecode, how we will create or find some ready-made byte-codes and how we test them. We will see is such cases that the results may be not so deterministic as we would expected...
Happy reversing
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.