Off-by-One Overflow Attack Analysis

Off-by-One Overflow Attack Analysis

Background

Last week, I attended a security course that included an example of an off-by-one overflow vulnerability. Here is the original code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Simple off-by-one overflow example */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void foo(char *input) {
char buf[1024];
strncpy(buf, input, sizeof(buf));
buf[sizeof(buf)] = '\0';
}

void bar(void) {
printf("I've been hacked\n");
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s input_string\n", argv[0]);
exit(EXIT_FAILURE);
}
foo(argv[1]);
return 0;
}

The answer provided for exploiting this vulnerability is:

1
perl -e 'system "./obo", "\x38\x84\x04\x08"x256'

The result of running this command is that multiple lines of I've been hacked are printed on the screen.

Analysis

When the program enters the foo function, the memory layout looks like this (as observed using GDB):

From top to bottom, the layout contains the return address, the saved frame pointer, and the buffer (buf).

When the line buf[sizeof(buf)] = '\0'; is executed, the least significant byte of the saved frame pointer (ebp) is set to 0. To ensure that ebp still points within the buf region after being partially overwritten, a buffer of at least 1024 bytes is required. Specifically, ebp needs to be overwritten such that it remains within a reasonable range (— up to 0xff), which is why the buffer is set to 0xff * 4 bytes.

Understanding Assembly Commands on foo Return

When the foo function returns, it typically executes the following key assembly instructions:

1. leave Instruction

The leave instruction is equivalent to:

1
2
mov esp, ebp
pop ebp
  • mov esp, ebp: This sets the stack pointer (esp) to the value of the frame pointer (ebp), restoring the stack pointer to the top of the current stack frame and effectively releasing the space occupied by the current function.
  • pop ebp: This pops the value at the top of the stack and assigns it to the frame pointer (ebp), thereby restoring the caller’s frame pointer. Essentially, it writes the return address into ebp, meaning it assigns the stack value (usually the caller’s frame address) to ebp, restoring the caller’s stack frame.

The effect of leave is to restore esp to its state before the function was called and to pop the saved ebp. If ebp has been overwritten to point to a special address (such as an address within the buffer), it can result in an incorrect stack pointer location during function return.

2. ret Instruction

The ret instruction pops an address off the top of the stack and jumps to that address:

1
pop eip

If the return address has been overwritten with the address of the bar function, the execution flow will jump to bar, allowing an attacker to run arbitrary code. Essentially, ret pops an address into the instruction pointer (eip) and jumps to that address to continue execution.

Attack Steps

  • When the command perl -e 'system "./obo", "\x38\x84\x04\x08"x256' is executed, the program takes these repeated bytes as the input to ./obo.
  • As the foo function returns, the leave and ret instructions are executed, leading to the return address being overwritten. This causes the program to jump to the bar function, printing the success message multiple times.

Further Analysis: Determining Effective Overwrite Locations

Stack Frame Layout Explanation

During the GDB debugging session, the memory layout for the stack frame of the foo function looks like this:

1
2
3
4
5
6
7
8
9
10
0xbfffed10   return address
0xbfffed0c saved frame pointer (ebp)
0xbfffed0b buf[1023]
...
0xbfffed03 buf[1015]
0xbfffed02 buf[1014]
0xbfffed01 buf[1013]
0xbfffed00 buf[1012]
...
0xbfffe90c buf[0]
  • Return address: Located at 0xbfffed10, this is the address that the program will jump to after the foo function finishes executing. Overwriting this address can control the flow of the program and potentially redirect it to malicious code (e.g., the bar function).

  • Saved frame pointer (ebp): Stored at 0xbfffed0c, this value is used to restore the calling function’s stack frame after foo finishes. In this example, we can see how the off-by-one overflow can overwrite the least significant byte of ebp.

  • Buffer (buf): The buffer starts at address 0xbfffe90c and extends to 0xbfffed0b, with buf[0] located at 0xbfffe90c and buf[1023] at 0xbfffed0b. The vulnerable line in the code, buf[sizeof(buf)] = '\0';, writes a null terminator (\0) just outside the bounds of this buffer, affecting the saved frame pointer.

In the off-by-one overflow scenario, the write operation overwrites the least significant byte of ebp, which is stored at 0xbfffed0c. By manipulating the value of ebp, we can influence the stack behavior when the leave and ret instructions are executed, eventually allowing us to control the program flow and redirect execution to the bar function.

To perform a successful attack, it’s crucial to determine precisely which bytes need to be overwritten in order to manipulate the control flow effectively. In this example, the overflow occurs when buf[sizeof(buf)] = '\0' is executed, causing the least significant byte of the saved frame pointer (ebp) to be set to 0. Thus, the value of ebp needs to be adjusted to ensure it points back into the buffer area, allowing the execution to proceed in the desired way and eventually jump to the bar function.

Based on further analysis and testing, the following insights were obtained:

  • To accurately determine the overwrite location, the value of ebp is crucial. However, obtaining this value is challenging because:

    1. GDB debugging affects address layout.
    2. The length of the input parameter affects the address layout.
  • Under GDB debugging, the layout within the foo function looks like this:

  • After executing buf[sizeof(buf)] = '\0';, ebp is modified such that the return address effectively takes the value at ebp + 1, which is the address 0xbfffed00 + 1, or 0xbfffed04.

  • The corresponding offset is at position 255 in buf, meaning the attack can be constructed by filling in the return address only at that specific location. The following command was used for verification in GDB:

    1
    r $(perl -e 'print  "\x01\x01\x01\x01"x254 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x1')
  • This was verified to work under GDB debugging, with some details to note:

    1. The input parameter length must always be 256 bytes; otherwise, the value of ebp will change, as the input parameter occupies stack space, affecting the starting position of the frame and thereby affecting the value of ebp.
    2. Padding must use non-zero values such as 0x01, because strncpy will terminate early if it encounters a 0 value.
  • When executing the program directly (i.e., without GDB), the memory layout differs, resulting in a different offset position. Through experimentation, it was found that the offset is at position 235. The corresponding attack command is:

    1
    ./obo $(perl -e 'print  "\x01\x01\x01\x01"x234 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x21')

This achieves the desired effect of accurately finding the overwrite location and successfully executing the attack.