2.6 Code Injection
When the return address is overwritten as the result of a software flaw, it seldom points to valid instructions. Consequently, transferring control to this address typically causes an exception and results in a corrupted stack. However, it is possible for an attacker to create a specially crafted string that contains a pointer to some malicious code, which the attacker also provides. When the subroutine returns, control is then transferred to this code. The malicious code runs with the permissions that the vulnerable program has when the subroutine returns. This is why programs running with root or other elevated privileges are normally targeted. The malicious code can perform any function that can otherwise be programmed, but often will simply open a remote shell on the compromised machine. For this reason the injected, malicious code is referred to as shellcode.
The pièce de résistance of any good exploit is the malicious argument. A malicious argument must have several characteristics.
- It must be accepted by the vulnerable program as legitimate input.
- The argument, along with other controllable inputs, must result in execution of the vulnerable code path.
- The argument must not cause the program to terminate abnormally before control is passed to the shellcode.
The get password program shown in Figure 2–9 can also be exploited to execute arbitrary code. This time, the program was compiled for Red Hat Linux 9.0 using GCC. An exploit can be injected into the program via a binary data file (as shown in Figure 2–24) from a file using redirection as follows:
%" "./BufferOverflow < exploit.bin
The binary data file cannot contain any newline or null characters until the last byte because the exploit relies on the string function gets(). The gets() function interprets a null character as a string termination character and reads data until a newline character or EOF condition is encountered.
000 31 32 33 34 35 36 37 38-39 30 31 32 33 34 35 36 "1234567890123456" 010 37 38 39 30 31 32 33 34-35 36 37 38 E0 F9 FF BF "789012345678a· +" 020 31 C0 A3 FF F9 FF BF B0-0B BB 03 FA FF BF B9 FB "1+ú · +≠+· +≠v" 030 F9 FF BF 8B 15 FF F9 FF-BF CD 80 FF F9 FF BF 31 "· +ï§ · +-Ç · +1" 040 31 31 31 2F 75 73 72 2F-62 69 6E 2F 63 61 6C 0A "111/usr/bin/cal "
Figure 2–24. Contents of binary file exploit.bin containing shellcode
Figure 2–25. Program stack overwritten by binary exploit
When the exploit code is injected into the get password program, the program stack is overwritten as shown in Figure 2–25, and the exploit works as follows.
- The first 16 bytes of binary data (line 1) fill the allocated storage space for the password. Even though the program only allocated 12 bytes for the password, the version of the gcc compiler that was used to compile the program allocates stack data in multiples of 16 bytes.
- The next 12 bytes of binary data (line 2) fill the extra storage space that was created by the compiler to keep the stack aligned on a 16-byte boundary. Only 12 bytes are allocated by the compiler because the stack already contained a 4-byte return address when the function was called.
- The return address has been overwritten (line 3) to resume program execution (line 4) when the program executes the return in the function IsPasswordOkay(). This results in the execution of code contained on the stack (lines 4–10).
- Create a zero value and use it to NULL terminate the argument list (lines 4 and 5). This is necessary because an argument to a system call made by this exploit must contain a list of character pointers terminated by a NULL pointer. Because the exploit cannot contain null characters until the last byte, the NULL pointer must be set by the exploit code.
- The system call is set to 0xb, which equates to the execve() system call in Linux (line 6).
- The three arguments for the execve() function call are set up (lines 7–9). The data for these arguments is located in lines 12–13.
- The execve() system call is executed, which results in the execution of the Linux calender program (line 10), as shown in Figure 2–26.
Reverse engineering of the code can be used to determine the exact offset from the buffer to the return address in the stack frame, which leads to the location of the injected shellcode. However, it is possible to soften these requirements [Aleph One 96]. For example, the location of the return address can be approximated by repeating the return address several times in the approximate region of the return address. Assuming a 32-bit architecture, the return address is normally 4-byte aligned. Even if the return address is offset, there are only 4
Figure 2–26. Execution result of the code injection exploit
possibilities to test. The location of the shellcode can also be approximated by prefacing the shellcode with nop instructions.7 The exploit need only jump somewhere in the field of nop instructions to execute the shellcode.
Most real-world stack smashing attacks behave in this fashion; that is, overwriting the return address to transfer control to injected code. Exploits that simply change the return address to jump to a new location in the code are less common, partly because these vulnerabilities are harder to find (it depends on finding program logic that can be bypassed) and less useful to an attacker (only allowing access to one program as opposed to running arbitrary code).