Appendix A. A Security Attack Example: The Stack Overrun


Appendix A. A Security Attack Example: The Stack Overrun

Stack overruns are a good example of the kind of vulnerability that attackers often enthusiastically exploit. A famous example from the history of the Internet of such an attack was the Morris Internet Worm. This is just one of many interesting types of attacks that must be considered in secure programming. This particular type of attack is much more difficult to mount in a managed runtime environment, such as .NET or Java, but has been exploited in many large C or C++ programs, including SQL Server and IIS. Studying this simple example may help you get a feel for the resourcefulness and state of mind of your potential adversaries.

The Win32ProjectBufferOverflow example described in this appendix demonstrates the concept behind the stack-overrun attack. [1] This technique is a simple exploit that is designed to write code onto the parameter area of the stack in a way that overlaps the function return address, causing execution to jump into the attacker's code rather than return normally. In the following code listing, you can see that the attacker's code is represented by the AttackerInsertedCode function. If you run this program, you will see that the AttackerInsertedCode function does get executed, even though there is no code in the source listing that actually calls this function.

[1] Note that the /GS option can be used in Visual C/C++ programs to detect buffer overruns, effectively preventing most vulnerabilities to the stack-overrun attack.

To keep things simple and compact, this example has both the code being attacked and the code that performs the attack all in one small program. In fact, they are both in the same source file, which is convenient for our purposes of demonstration, but not all that realistic. In a real-world example, the target code and the attack code would be in separate programs, typically communicating via a socket rather than a direct function call. However, precisely the same concept would still be in effect in both the realistic scenario and the example given here. Another point to remember when trying out this example program is that if you recompile it with a different version of the compiler, or if you change any of the source code and then recompile, you will probably have to modify the data that is used to overwrite the stack, since both code and stack layout may have changed. This project has not turned on stack runtime checks, and if you turn this compiler option on, it will not work properly.

 // Win32ProjectBufferOverflow.cpp ... #define BUFLENGTH 16 //the following code was written by a sloppy programmer void  VulnerableFunction  (char * str) {    char buf[BUFLENGTH];  strcpy(buf, str); //danger here!  printf("VulnerableFunction called.\n\n"); } //the following code was written by a nasty programmer void  AttackerInsertedCode  () {  printf("Nyahahaha... AttackerInsertedCode called.\n");  while (true); //never return } void main() {    //call the vulnerable function in a safe way    printf("Call VulnerableFunction in a safe way.\n");    VulnerableFunction("hello");    //call the vulnerable function in a nasty way    printf("Call VulnerableFunction in nasty way.\n");  char bufNasty[25];  //make sure there are no intervening nulls    for (int i=0; i<20; i++)       bufNasty[i] = 0x01;    //overwrite return address with AttackerInsertedCode  int * pRetAddres = (int *)&(bufNasty[20]);   *pRetAddres = 0x004119A0;  //final null terminator    bufNasty[24] = (char)0x00; //actually redundant    //do the dirty deed  VulnerableFunction(bufNasty);  } 

The key weakness that is exploited in this example is the sloppy workmanship in the function named VulnerableFunction . In particular, the call to strcpy is not done in a safe manner. The strcpy function copies the buffer pointed to by the second parameter, including its terminating null, to the buffer pointed at by the first parameter. But this copying is done without any buffer-length checking. Within VulnerableFunction , the local buffer buf is allocated on the current stack frame with a capacity of 16 bytes. If you call VulnerableFunction with a parameter that points to a zero- terminated buffer that is no more than 16 bytes, it will work normally. If you pass in a pointer to a buffer that has more than 16 bytes, the additional bytes are written into the next available bytes on the stack, which contain, among other things, the return address used upon returning from the function to resume at the next instruction within the calling function.

If you run the program, you will see the following output. Note that the first time that VulnerableFunction is called, it works normally. This is because it is called safely, passing a buffer containing fewer than 16 bytes. But the second call to VulnerableFunction is a different story. The buffer passed in contains 25 bytes. Normally, this would just be a bug that causes a random amount of arbitrary data to overwrite the stack, which would cause an access violation (or perhaps have no deleterious effect). But in this example, the data being written over the stack is not a random length, and the data being written is specially crafted to have a very specific effect. Rather than producing an access violation, the AttackerInsertedCode is executed, as evidenced by the appearance of the output Nyahahaha... being displayed on the console.

 Call VulnerableFunction in a safe way. VulnerableFunction called. Call VulnerableFunction in nasty way. VulnerableFunction called.  Nyahahaha  ... AttackerInsertedCode called. 

So, how do you figure out exactly how long your stack overwriting buffer should be, and how do you figure out exactly what data should be placed into this buffer to ensure that your own nasty code gets executed in place of a normal, orderly function return? To sort this out, you have to get into the debugger and set breakpoints, view the disassembly of the code, study a few of the CPU registers, and view the contents of memory containing the stack. Then, you can locate the address that contains the four bytes containing the return address for VulnerableFunction . You can also determine the address of your own AttackerInsertedCode function.

Let's see how to determine the address of the AttackerInsertedCode function. Set a breakpoint on the AttackerInsertedCode function, and run the program under the debugger. Then, click on the Disassembly tab and note the address of this function. This is shown in Figure A-1, where you can see that this address is 004119A0 in this example.

Figure A-1. Determining the address of the AttackerInsertedCode function.

graphics/afig01.gif

Next, let's see how to determine where the return address is stored for returning from the call to the VulnerableFunction function. This is a bit tricky, but here is the basic approach to use. First, set a breakpoint on the function to be called (Figure A-2), and then run the program under the debugger. When you hit the breakpoint, switch the source code view to show the disassembly of the function call.

Figure A-2. Setting the breakpoint on the VulnerableFunction.

graphics/afig02.gif

From this information, you can determine the return address that you would like to switch to ( pRetAddres = 0x004119A0 ) and also exactly where in the parameter stack you need to place this new return address ( bufNasty[20] ).



.NET Security and Cryptography
.NET Security and Cryptography
ISBN: 013100851X
EAN: 2147483647
Year: 2003
Pages: 126

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net