Home > Articles > Programming > C/C++

  • Print
  • + Share This
This chapter is from the book

2.3. String Vulnerabilities and Exploits

Previous sections described common errors in manipulating strings in C or C++. These errors become dangerous when code operates on untrusted data from external sources such as command-line arguments, environment variables, console input, text files, and network connections. Depending on how a program is used and deployed, external data may be trusted or untrusted. However, it is often difficult to predict all the ways software may be used. Frequently, assumptions made during development are no longer valid when the code is deployed. Changing assumptions is a common source of vulnerabilities. Consequently, it is safer to view all external data as untrusted.

In software security analysis, a value is said to be tainted if it comes from an untrusted source (outside of the program’s control) and has not been sanitized to ensure that it conforms to any constraints on its value that consumers of the value require—for example, that all strings are null-terminated.

Tainted Data

Example 2.3 is a simple program that checks a user password (which should be considered tainted data) and grants or denies access.

Example 2.3. The IsPasswordOK Program

01  bool IsPasswordOK(void) {
02    char Password[12];
03
04    gets(Password);
05   r eturn 0 == strcmp(Password, "goodpass");
06  }
07
08  int main(void) {
09    bool PwStatus;
10
11    puts("Enter password:");
12    PwStatus = IsPasswordOK();
13    if (PwStatus == false) {
14      puts("Access denied");
15      exit(-1);
16    }
17  }

This program shows how strings can be misused and is not an exemplar for password checking. The IsPasswordOK program starts in the main() function. The first line executed is the puts() call that prints out a string literal. The puts() function, defined in the C Standard as a character output function, is declared in <stdio.h> and writes a string to the output stream pointed to by stdout followed by a newline character ('\n'). The IsPasswordOK() function is called to retrieve a password from the user. The function returns a Boolean value: true if the password is valid, false if it is not. The value of PwStatus is tested, and access is allowed or denied.

The IsPasswordOK() function uses the gets() function to read characters from the input stream (referenced by stdin) into the array pointed to by Password until end-of-file is encountered or a newline character is read. Any newline character is discarded, and a null character is written immediately after the last character read into the array. The strcmp() function defined in <string.h> compares the string pointed to by Password to the string literal "goodpass" and returns an integer value of 0 if the strings are equal and a nonzero integer value if they are not. The IsPasswordOK() function returns true if the password is "goodpass", and the main() function consequently grants access.

In the first run of the program (Figure 2.2), the user enters the correct password and is granted access.

Figure 2.2

Figure 2.2. Correct password grants access to user.

In the second run (Figure 2.3), an incorrect password is provided and access is denied.

Figure 2.3

Figure 2.3. Incorrect password denies access to user.

Unfortunately, this program contains a security flaw that allows an attacker to bypass the password protection logic and gain access to the program. Can you identify this flaw?

Security Flaw: IsPasswordOK

The security flaw in the IsPasswordOK program that allows an attacker to gain unauthorized access is caused by the call to gets(). The gets() function, as already noted, copies characters from standard input into Password until end-of-file is encountered or a newline character is read. The Password array, however, contains only enough space for an 11-character password and a trailing null character. This condition results in writing beyond the bounds of the Password array if the input is greater than 11 characters in length. Figure 2.4 shows what happens if a program attempts to copy 16 bytes of data into a 12-byte array.

Figure 2.4

Figure 2.4. Copying 16 bytes of data into a 12-byte array

The condition that allows an out-of-bounds write to occur is referred to in software security as a buffer overflow. A buffer overflow occurs at runtime; however, the condition that allows a buffer overflow to occur (in this case) is an unbounded string read, and it can be recognized when the program is compiled. Before looking at how this buffer overflow poses a security risk, we first need to understand buffer overflows and process memory organization in general.

The IsPasswordOK program has another problem: it does not check the return status of gets(). This is a violation of “FIO04-C. Detect and handle input and output errors.” When gets() fails, the contents of the Password buffer are indeterminate, and the subsequent strcmp() call has undefined behavior. In a real program, the buffer might even contain the good password previously entered by another user.

Buffer Overflows

Buffer overflows occur when data is written outside of the boundaries of the memory allocated to a particular data structure. C and C++ are susceptible to buffer overflows because these languages

  • Define strings as null-terminated arrays of characters
  • Do not perform implicit bounds checking
  • Provide standard library calls for strings that do not enforce bounds checking

Depending on the location of the memory and the size of the overflow, a buffer overflow may go undetected but can corrupt data, cause erratic behavior, or terminate the program abnormally.

Buffer overflows are troublesome in that they are not always discovered during the development and testing of software applications. Not all C and C++ implementations identify software flaws that can lead to buffer overflows during compilation or report out-of-bound writes at runtime. Static analysis tools can aid in discovering buffer overflows early in the development process. Dynamic analysis tools can be used to discover buffer overflows as long as the test data precipitates a detectable overflow.

Not all buffer overflows lead to software vulnerabilities. However, a buffer overflow can lead to a vulnerability if an attacker can manipulate user-controlled inputs to exploit the security flaw. There are, for example, well-known techniques for overwriting frames in the stack to execute arbitrary code. Buffer overflows can also be exploited in heap or static memory areas by overwriting data structures in adjacent memory.

Before examining how these exploits behave, it is useful to understand how process memory is organized and managed. If you are already familiar with process memory organization, execution stack, and heap management, skip to the section “Stack Smashing,” page 59.

Process Memory Organization

Process memory is generally organized into code, data, heap, and stack segments, as shown in column (a) of Figure 2.5.

Figure 2.5

Figure 2.5. Process memory organization

The code or text segment includes instructions and read-only data. It can be marked read-only so that modifying memory in the code section results in faults. (Memory can be marked read-only by using memory management hardware in the computer hardware platform that supports that feature or by arranging memory so that writable data is not stored in the same page as read-only data.) The data segment contains initialized data, uninitialized data, static variables, and global variables. The heap is used for dynamically allocating process memory. The stack is a last-in, first-out (LIFO) data structure used to support process execution.

The exact organization of process memory depends on the operating system, compiler, linker, and loader—in other words, on the implementation of the programming language. Columns (b) and (c) show possible process memory organization under UNIX and Win32.

Stack Management

The stack supports program execution by maintaining automatic process-state data. If the main routine of a program, for example, invokes function a(), which in turn invokes function b(), function b() will eventually return control to function a(), which in turn will return control to the main() function (see Figure 2.6).

Figure 2.6

Figure 2.6. Stack management

To return control to the proper location, the sequence of return addresses must be stored. A stack is well suited for maintaining this information because it is a dynamic data structure that can support any level of nesting within memory constraints. When a subroutine is called, the address of the next instruction to execute in the calling routine is pushed onto the stack. When the subroutine returns, this return address is popped from the stack, and program execution jumps to the specified location (see Figure 2.7). The information maintained in the stack reflects the execution state of the process at any given instant.

Figure 2.7

Figure 2.7. Calling a subroutine

In addition to the return address, the stack is used to store the arguments to the subroutine as well as local (or automatic) variables. Information pushed onto the stack as a result of a function call is called a frame. The address of the current frame is stored in the frame or base pointer register. On x86-32, the extended base pointer (ebp) register is used for this purpose. The frame pointer is used as a fixed point of reference within the stack. When a subroutine is called, the frame pointer for the calling routine is also pushed onto the stack so that it can be restored when the subroutine exits.

There are two notations for Intel instructions. Microsoft uses the Intel notation

mov eax, 4 # Intel Notation

GCC uses the AT&T syntax:

mov $4, %eax # AT&T Notation

Both of these instructions move the immediate value 4 into the eax register. Example 2.4 shows the x86-32 disassembly of a call to foo(MyInt, MyStrPtr) using the Intel notation.

Example 2.4. Disassembly Using Intel Notation

01  void foo(int, char *); // function prototype
02
03  int main(void) {
04    int MyInt=1; // stack variable located at ebp-8
05    char *MyStrPtr="MyString"; // stack var at ebp-4
06    /* ... */
07    foo(MyInt, MyStrPtr); // call foo function
08      mov  eax, [ebp-4]
09      push eax            # Push 2nd argument on stack
10      mov  ecx, [ebp-8]
11      push ecx            # Push 1st argument on stack
12      call foo            # Push the return address on stack and
13                          # jump to that address
14      add  esp, 8
15    /* ... */
16  }

The invocation consists of three steps:

  1. The second argument is moved into the eax register and pushed onto the stack (lines 8 and 9). Notice how these mov instructions use the ebp register to reference arguments and local variables on the stack.
  2. The first argument is moved into the ecx register and pushed onto the stack (lines 10 and 11).
  3. The call instruction pushes a return address (the address of the instruction following the call instruction) onto the stack and transfers control to the foo() function (line 12).

The instruction pointer (eip) points to the next instruction to be executed. When executing sequential instructions, it is automatically incremented by the size of each instruction, so that the CPU will then execute the next instruction in the sequence. Normally, the eip cannot be modified directly; instead, it must be modified indirectly by instructions such as jump, call, and return.

When control is returned to the return address, the stack pointer is incremented by 8 bytes (line 14). (On x86-32, the stack pointer is named esp. The e prefix stands for “extended” and is used to differentiate the 32-bit stack pointer from the 16-bit stack pointer.) The stack pointer points to the top of the stack. The direction in which the stack grows depends on the implementation of the pop and push instructions for that architecture (that is, they either increment or decrement the stack pointer). For many popular architectures, including x86, SPARC, and MIPS processors, the stack grows toward lower memory. On these architectures, incrementing the stack pointer is equivalent to popping the stack.

foo() Function Prologue

A function prologue contains instructions that are executed by a function upon its invocation. The following is the function prologue for the foo() function:

1  void foo(int i, char *name) {
2    char LocalChar[24];
3    int LocalInt;
4      push ebp       # Save the frame pointer.
5      mov ebp, esp   # Frame pointer for subroutine is set to the
6                     # current stack pointer.
7      sub esp, 28    # Allocates space for local variables.
8    /* ... */

The push instruction pushes the ebp register containing the pointer to the caller’s stack frame onto the stack. The mov instruction sets the frame pointer for the function (the ebp register) to the current stack pointer. Finally, the function allocates 28 bytes of space on the stack for local variables (24 bytes for LocalChar and 4 bytes for LocalInt).

The stack frame for foo() following execution of the function prologue is shown in Table 2.2. On x86, the stack grows toward low memory.

Table 2.2. Stack Frame for foo() following Execution of the Function Prologue

Address

Value

Description

Length

0x0012FF4C

?

Last local variable—integer: LocalInt

4

0x0012FF50

?

First local variable—string: LocalChar

24

0x0012FF68

0x12FF80

Calling frame of calling function: main()

4

0x0012FF6C

0x401040

Return address of calling function: main()

4

0x0012FF70

1

First argument: MyInt (int)

4

0x0012FF74

0x40703C

Second argument: pointer toMyString (char *)

4

foo() Function Epilogue

A function epilogue contains instructions that are executed by a function to return to the caller. The following is the function epilogue to return from the foo() function:

1  /* ... */
2   return;
3     mov  esp, ebp   # Restores the stack pointer.
4     pop  ebp        # Restores the frame pointer.
5     ret             # Pops the return address off the stack
6                     # and transfers control to that location.
7  }

This return sequence is the mirror image of the function prologue shown earlier. The mov instruction restores the caller’s stack pointer (esp) from the frame pointer (ebp). The pop instruction restores the caller’s frame pointer from the stack. The ret instruction pops the return address in the calling function off the stack and transfers control to that location.

Stack Smashing

Stack smashing occurs when a buffer overflow overwrites data in the memory allocated to the execution stack. It can have serious consequences for the reliability and security of a program. Buffer overflows in the stack segment may allow an attacker to modify the values of automatic variables or execute arbitrary code.

Overwriting automatic variables can result in a loss of data integrity or, in some cases, a security breach (for example, if a variable containing a user ID or password is overwritten). More often, a buffer overflow in the stack segment can lead to an attacker executing arbitrary code by overwriting a pointer to an address to which control is (eventually) transferred. A common example is overwriting the return address, which is located on the stack. Additionally, it is possible to overwrite a frame- or stack-based exception handler pointer, function pointer, or other addresses to which control may be transferred.

The example IsPasswordOK program is vulnerable to a stack-smashing attack. To understand why this program is vulnerable, it is necessary to understand exactly how the stack is being used.

Figure 2.8 illustrates the contents of the stack before the program calls the IsPasswordOK() function.

Figure 2.8

Figure 2.8. The stack before IsPasswordOK() is called

The operating system (OS) or a standard start-up sequence puts the return address from main() on the stack. On entry, main() saves the old incoming frame pointer, which again comes from the operating system or a standard start-up sequence. Before the call to the IsPasswordOK() function, the stack contains the local Boolean variable PwStatus that stores the status returned by the function IsPasswordOK() along with the caller’s frame pointer and return address.

While the program is executing the function IsPasswordOK(), the stack contains the information shown in Figure 2.9.

Figure 2.9

Figure 2.9. Information in stack while IsPasswordOK() is executed

Notice that the password is located on the stack with the return address of the caller main(), which is located after the memory that is used to store the password. It is also important to understand that the stack will change during function calls made by IsPasswordOK().

After the program returns from the IsPasswordOK() function, the stack is restored to its initial state, as in Figure 2.10.

Figure 2.10

Figure 2.10. Stack restored to initial state

Execution of the main() function resumes; which branch is executed depends on the value returned from the IsPasswordOK() function.

Security Flaw: IsPasswordOK

As discussed earlier, the IsPasswordOK program has a security flaw because the Password array is improperly bounded and can hold only an 11-character password plus a trailing null byte. This flaw can easily be demonstrated by entering a 20-character password of “12345678901234567890” that causes the program to crash, as shown in Figure 2.11.

Figure 2.11

Figure 2.11. An improperly bounded Password array crashes the program if its character limit is exceeded.

To determine the cause of the crash, it is necessary to understand the effect of storing a 20-character password in a 12-byte stack variable. Recall that when 20 bytes are input by the user, the amount of memory required to store the string is actually 21 bytes because the string is terminated by a null-terminator character. Because the space available to store the password is only 12 bytes, 9 bytes of the stack (21 – 12 = 9) that have already been allocated to store other information will be overwritten with password data. Figure 2.12 shows the corrupted program stack that results when the call to gets() reads a 20-byte password and overflows the allocated buffer. Notice that the caller’s frame pointer, return address, and part of the storage space used for the PwStatus variable have all been corrupted.

Figure 2.12

Figure 2.12. Corrupted program stack

When a program fault occurs, the typical user generally does not assume that a potential vulnerability exists. The typical user only wants to restart the program. However, an attacker will investigate to see if the programming flaw can be exploited.

The program crashes because the return address is altered as a result of the buffer overflow, and either the new address is invalid or memory at that address (1) does not contain a valid CPU instruction; (2) does contain a valid instruction, but the CPU registers are not set up for proper execution of the instruction; or (3) is not executable.

A carefully crafted input string can make the program produce unexpected results, as shown in Figure 2.13.

Figure 2.13

Figure 2.13. Unexpected results from a carefully crafted input string

Figure 2.14 shows how the contents of the stack have changed when the contents of a carefully crafted input string overflow the storage allocated for Password.

Figure 2.14

Figure 2.14. Program stack following buffer overflow using crafted input string

The input string consists of a number of funny-looking characters: j▸*!. These are all characters that can be input using the keyboard or character map. Each of these characters has a corresponding hexadecimal value: j = 0x6A, ▸ = 0x10, * = 0x2A, and ! = 0x21. In memory, this sequence of four characters corresponds to a 4-byte address that overwrites the return address on the stack, so instead of returning to the instruction immediately following the call in main(), the IsPasswordOK() function returns control to the “Access granted” branch, bypassing the password validation logic and allowing unauthorized access to the system. This attack is a simple arc injection attack. Arc injection attacks are covered in more detail in the “Arc Injection” section.

Code Injection

When the return address is overwritten because of a software flaw, it seldom points to valid instructions. Consequently, transferring control to this address typically causes a trap and results in a corrupted stack. But 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 function invocation whose return address has been overwritten returns, control is transferred to this code. The malicious code runs with the permissions that the vulnerable program has when the subroutine returns, which 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 simply opens 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 IsPasswordOK program can also be exploited to execute arbitrary code because of the buffer overflow caused by the call to gets(). The gets() function also has an interesting property in that it reads characters from the input stream pointed to by stdin until end-of-file is encountered or a newline character is read. Any newline character is discarded, and a null character is written immediately after the last character read into the array. As a result, there might be null characters embedded in the string returned by gets() if, for example, input is redirected from a file. It is important to note that the gets() function was deprecated in C99 and eliminated from the C11 standard (most implementations are likely to continue to make gets() available for compatibility reasons). However, data read by the fgets() function may also contain null characters. This issue is further documented in The CERT C Secure Coding Standard [Seacord 2008], “FIO37-C. Do not assume that fgets() returns a nonempty string when successful.”

The program IsPasswordOK was compiled for Linux using GCC. The malicious argument can be stored in a binary file and supplied to the vulnerable program using redirection, as follows:

%./BufferOverflow < exploit.bin

When the exploit code is injected into the IsPasswordOK program, the program stack is overwritten as follows:

01  /* buf[12] */
02  00 00 00 00
03  00 00 00 00
04  00 00 00 00
05
06  /* %ebp */
07  00 00 00 00
08
09  /* return address */
10  78 fd ff bf
11
12  /* "/usr/bin/cal" */
13  2f 75 73 72
14  2f 62 69 6e
15  2f 63 61 6c
16  00 00 00 00
17
18  /* null pointer */
19  74 fd ff bf
20
21  /* NULL */
22  00 00 00 00
23
24  /* exploit code */
25  b0 0b       /* mov  $0xb, %eax */
26  8d 1c 24    /* lea  (%esp), %ebx */
27  8d 4c 24 f0 /* lea  -0x10(%esp), %ecx */
28  8b 54 24 ec /* mov  -0x14(%esp), %edx */
29  cd 50       /* int  $0x50 */

The lea instruction used in this example stands for “load effective address.” The lea instruction computes the effective address of the second operand (the source operand) and stores it in the first operand (destination operand). The source operand is a memory address (offset part) specified with one of the processor’s addressing modes; the destination operand is a general-purpose register. The exploit code works as follows:

  1. The first mov instruction is used to assign 0xB to the %eax register. 0xB is the number of the execve() system call in Linux.
  2. The three arguments for the execve() function call are set up in the subsequent three instructions (the two lea instructions and the mov instruction). The data for these arguments is located on the stack, just before the exploit code.
  3. The int $0x50 instruction is used to invoke execve(), which results in the execution of the Linux calendar program, as shown in Figure 2.15.
    Figure 2.15

    Figure 2.15. Linux calendar program

The call to the fgets function is not susceptible to a buffer overflow, but the call to strcpy() is, as shown in the modified IsPasswordOK program that follows:

01  char buffer[128];
02
03  _Bool IsPasswordOK(void) {
04    char Password[12];
05
06    fgets(buffer, sizeof buffer, stdin);
07    if (buffer[ strlen(buffer) - 1] == '\n')
08      buffer[ strlen(buffer) - 1] = 0;
09    strcpy(Password, buffer);
10    return 0 == strcmp(Password, "goodpass");
11  }
12
13  int main(void) {
14    _Bool PwStatus;
15
16    puts("Enter password:");
17    PwStatus = IsPasswordOK();
18    if (!PwStatus) {
19      puts("Access denied");
20      exit(-1);
21    }
22    else
23      puts("Access granted");
24    return 0;
25  }

Because the strcpy() function copies only the source string (stored in buffer), the Password array cannot contain internal null characters. Consequently, the exploit is more difficult because the attacker has to manufacture any required null bytes.

The malicious argument in this case is in the binary file exploit.bin:

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 04 fc ff bf 78 78 78 78  78901234....xxxx
020: 31 c0 a3 23 fc ff bf b0 0b bb 27 fc ff bf b9 1f  1..#......'.....
030: fc ff bf 8b 15 23 fc 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.

This malicious argument can be supplied to the vulnerable program using redirection, as follows:

%./BufferOverflow < exploit.bin

After the strcpy() function returns, the stack is overwritten as shown in Table 2.3.

Table 2.3. Corrupted Stack for the Call to strcpy()

Row

Address

Content

Description

1

0xbffff9c0 –0xbffff9cf

"123456789012456"

Storage for Password (16 bytes) and padding

2

0xbffff9d0 –0xbffff9db

"789012345678"

Additional padding

3

0xbffff9dc

(0xbffff9e0)

New return address

4

0xbffff9e0

xor %eax,%eax

Sets eax to 0

5

0xbffff9e2

mov %eax,0xbffff9ff

Terminates pointer array with null pointer

6

0xbffff9e7

mov $0xb,%al

Sets the code for the execve() function call

7

0xbffff9e9

mov $0xbffffa03,%ebx

Sets ebx to point to the first argument to execve()

8

0xbffff9ee

mov $0xbffff9fb,%ecx

Sets ecx to point to the second argument to execve()

9

0xbffff9f3

mov 0xbffff9ff,%edx

Sets edx to point to the third argument to execve()

10

0xbffff9f9

int $80

Invokes execve() system call

11

0xbffff9fb

0xbffff9ff

Array of argument strings passed to the new program

12

0xbffff9ff

"1111"

Changed to 0x00000000 to terminate the pointer array and also used as the third argument

13

0xbffffa03 –0xbffffa0f

"/usr/bin/cal\0"

Command to execute

The exploit works as follows:

  1. The first 16 bytes of binary data (row 1) fill the allocated storage space for the password. Even though the program allocated only 12 bytes for the password, the version of the GCC that was used to compile the program allocates stack data in multiples of 16 bytes.
  2. The next 12 bytes of binary data (row 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.
  3. The return address is overwritten (row 3) to resume program execution (row 4) when the program executes the return statement in the function IsPasswordOK(), resulting in the execution of code contained on the stack (rows 4–10).
  4. A zero value is created and used to null-terminate the argument list (rows 4 and 5) 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.
  5. The system call is set to 0xB, which equates to the execve() system call in Linux (row 6).
  6. The three arguments for the execve() function call are set up (rows 7–9).
  7. The data for these arguments is located in rows 12 and 13.
  8. The execve() system call is executed, which results in the execution of the Linux calendar program (row 10).

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 relax these requirements [Aleph 1996]. 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 four possibilities to test. The location of the shellcode can also be approximated by prefixing a series of nop instructions before the shellcode (often called a nop sled). 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: they overwrite 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 (allowing access to only one program as opposed to running arbitrary code).

Arc Injection

The first exploit for the IsPasswordOK program, described in the “Stack Smashing” section, modified the return address to change the control flow of the program (in this case, to circumvent the password protection logic). The arc injection technique (sometimes called return-into-libc) involves transferring control to code that already exists in process memory. These exploits are called arc injection because they insert a new arc (control-flow transfer) into the program’s control-flow graph as opposed to injecting new code. More sophisticated attacks are possible using this technique, including installing the address of an existing function (such as system() or exec(), which can be used to execute commands and other programs already on the local system) on the stack along with the appropriate arguments. When the return address is popped off the stack (by the ret or iret instruction in x86), control is transferred by the return instruction to an attacker-specified function. By invoking functions like system() or exec(), an attacker can easily create a shell on the compromised machine with the permissions of the compromised program.

Worse yet, an attacker can use arc injection to invoke multiple functions in sequence with arguments that are also supplied by the attacker. An attacker can now install and run the equivalent of a small program that includes chained functions, increasing the severity of these attacks.

The following program is vulnerable to a buffer overflow:

01  #include <string.h>
02
03  int get_buff(char *user_input, size_t size){
04    char buff[40];
05    memcpy(buff, user_input, size);
06    return 0;
07  }
08
09  int main(void) {
10    /* ... */
11    get_buff(tainted_char_array, tainted_size);
12    /* ... */
13  }

Tainted data in user_input is copied to the buff character array using memcpy(). A buffer overflow can result if user_input is larger than the buff buffer.

An attacker may prefer arc injection over code injection for several reasons. Because arc injection uses code already in memory on the target system, the attacker merely needs to provide the addresses of the functions and arguments for a successful attack. The footprint for this type of attack can be significantly smaller and may be used to exploit vulnerabilities that cannot be exploited by the code injection technique. Because the exploit consists entirely of existing code, it cannot be prevented by memory-based protection schemes such as making memory segments (such as the stack) nonexecutable. It may also be possible to restore the original frame to prevent detection.

Chaining function calls together allows for more powerful attacks. A security-conscious programmer, for example, might follow the principle of least privilege [Saltzer 1975] and drop privileges when not required. By chaining multiple function calls together, an exploit could regain privileges, for example, by calling setuid() before calling system().

Return-Oriented Programming

The return-oriented programming exploit technique is similar to arc injection, but instead of returning to functions, the exploit code returns to sequences of instructions followed by a return instruction. Any such useful sequence of instructions is called a gadget. A Turing-complete set of gadgets has been identified for the x86 architecture, allowing arbitrary programs to be written in the return-oriented language. A Turing-complete library of code gadgets using snippets of the Solaris libc, a general-purpose programming language, and a compiler for constructing return-oriented exploits have also been developed [Buchanan 2008]. Consequently, there is an assumed risk that return-oriented programming exploits could be effective on other architectures as well.

The return-oriented programming language consists of a set of gadgets. Each gadget specifies certain values to be placed on the stack that make use of one or more sequences of instructions in the code segment. Gadgets perform well-defined operations, such as a load, an add, or a jump.

Return-oriented programming consists of putting gadgets together that will perform the desired operations. Gadgets are executed by a return instruction with the stack pointer referring to the address of the gadget.

For example, the sequence of instructions

pop %ebx;
ret

forms a gadget that can be used to load a constant value into the ebx register, as shown in Figure 2.16.

Figure 2.16

Figure 2.16. Gadget built with return-oriented programming

The left side of Figure 2.16 shows the x86-32 assembly language instruction necessary to copy the constant value $0xdeadbeef into the ebx register, and the right side shows the equivalent gadget. With the stack pointer referring to the gadget, the return instruction is executed by the CPU. The resulting gadget pops the constant from the stack and returns execution to the next gadget on the stack.

Return-oriented programming also supports both conditional and unconditional branching. In return-oriented programming, the stack pointer takes the place of the instruction pointer in controlling the flow of execution. An unconditional jump requires simply changing the value of the stack pointer to point to a new gadget. This is easily accomplished using the instruction sequence

pop %esp;
ret

The x86-32 assembly language programming and return-oriented programming idioms for unconditional branching are contrasted in Figure 2.17.

Figure 2.17

Figure 2.17. Unconditional branching in x86-32 assembly language (left) and return-oriented programming idioms

An unconditional branch can be used to branch to an earlier gadget on the stack, resulting in an infinite loop. Conditional iteration can be implemented by a conditional branch out of the loop.

Hovav Shacham’s “The Geometry of Innocent Flesh on the Bone” [Shacham 2007] contains a more complete tutorial on return-oriented programming. While return-oriented programming might seem very complex, this complexity can be abstracted behind a programming language and compiler, making it a viable technique for writing exploits.

  • + Share This
  • 🔖 Save To Your Account

Sign Up for Our Newsletters

Subscribing to the InformIT newsletters is an easy way to keep in touch with what's happening in your corner of the industry. We have a newsletters dedicated to a variety of topics such as open source, programming, and web development, so you get just the information you need. Sign up today.