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

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++) {
    buffer[i] = 'a';
  }
				
			

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.

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 :P\n");

}

void function(){
    printf("Everything is fine.\n");
}

int main(int argc, char *argv[]){
    
    printf("\033[1mWelcome to ROPLevel7 by @bellis1000!\nThis level involves exploiting an off-by-one vulnerability.\n\n\x1b[0m");
    
    if (argc < 3){
        printf("Usage: %s <data> <block_data>\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.

This is before the loop.

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 <data> <block_data>
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.

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
				
			

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

We can examine this memory location using the x command.

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.

Let’s continue our program.

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