ARM64 Reversing and Exploitation part 6 – Exploiting an Uninitialized Stack Variable Vulnerability

Hello everyone, In this blog post, we’ll look into uninitialized stack variables in ARM64. We explore the dangers posed by these seemingly innocent variables and their potential impact on software security.

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.

Lab setup

You can use the environment of your choice. But if you are new and want to follow the exact same steps you can use QEMU images for emulation.

Uninitialized variables

So what are uninitialized variables ?

Uninitialized variables are variables that are declared but they are not assigned a value. Let’s see an example.

				
					#include <stdio.h>

void main(){

int a;
int b;

printf("%d %d\n",a,b);

}
				
			

Compile this program using gcc.

gcc uni.c -o uni

If you execute this binary what values would be printed ? Let’s see.

The values are 0 and 32. How did these variables get the value 0 and 32 since we didn’t assign them any values?

If we are not specifying or initializing any value to the variables, they will contain unpredictable or garbage values, which are the values left in the memory at that particular location. The values of a and b will depend on the state of memory at the time of execution and can vary each time the program runs.

So, the next question will be, how does this become a vulnerability?

Not all cases of uninitialized stack variables would become vulnerabilities, but there are cases where uninitialized stack variables can become vulnerabilities in computer programs. This is because they may contain arbitrary values leftover in the memory from previous program executions or other parts of the application. Such situations can lead to unintended behavior or security issues if the uninitialized values are used in critical parts of the code or sensitive operations.

Examples of how uninitialized stack variables can become vulnerabilities include :

  • Data Leakage: If uninitialized variables are used to hold sensitive data like passwords, encryption keys, or personal information, an attacker can potentially access and read this data by exploiting the uninitialized variable vulnerability.

  • Information Disclosure: The uninitialized variable’s contents could contain information about the program’s memory layout or internal states, addresses in the stack, which an attacker might use to craft targeted exploits.

  • Crashes and Instability: Using uninitialized variables in calculations or control structures can lead to undefined behavior, causing program crashes or unexpected results.

  • Arbitrary Code Execution: In some cases, an attacker may be able to manipulate uninitialized variables in such a way that they can control the program’s execution flow, leading to arbitrary code execution and potential remote code execution.

Stack Frames

We will look into a more realistic case right away. But Before that consider the c program below.

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

int *add(int* a,int* b){

int c;
c= (*a) + (*b);
return &c;

}

void main(){
int num1 = 1;
int num2 = 2;
int* result = add(&num1,&num2);
printf("The result of addition : %d",*result);


}
				
			

Let’s compile this using gcc.

gcc uni2.c -o uni2

Let’s try running this.

debian@debian:~/pwn/uni_stack$ ./uni2
Segmentation fault

As we can see now, the program has crashed. What could have been the reason for this?

The program has crashed because the add function returns the address of a local variable, c, which is allocated on the stack. When the add function returns, the memory allocated for c is deallocated, and as a result the address of c becomes invalid and the pointer result in the main function will point to an invalid memory location.

Due to this invalid memory access, when the printf statement in the main function tries to dereference the pointer result to print the value, it leads to a segmentation fault (crash).

Let’s also debug this and see what happens.

Load the binary into gdb.

debian@debian:~/pwn/uni_stack$ gdb ./uni2

Put a breakpoint at main.

gef➤ b main

Start the program using r command.

gef➤ r

Let’s put a breakpoint at the branch instruction to the add() function.

Type c to continue the program until it hits this breakpoint.

The program is going to execute the add() function. We can see that x0 and x1 contain the values 1 and 2 which are the arguments to add() function.

Let’s step inside the add() function using the si command

Now we are inside the add() function.

The first instruction sub sp, sp, #0x20 will allocate the space for the add() function. This space is known as the stack frame. This typically includes function arguments, local variables, and other information needed during the function’s execution. Let’s see a high level diagram which shows the stack frame.

After stepping through the sub sp, sp, #0x20 we can see that top of the stack becomes different.

Now sp points to 0x0000fffffffff330. Previously, it was pointing to 0x0000fffffffff350.

Let’s step through using the ni command. The above instructions will allocate the space in the stack and load the values from x0 and x1 into w0 and w1, add these values (1 and 2), and stores the result into w0.

The str w0, [sp, #28] instruction will store the value in w0 (w1 + w0 = 3) into [sp + 28].

mov x0, #0x0       

This will copy 0 into x0. As we know the return value of a function is generally stored in the x0 register. In our source code we are returning the address of the local variable c. But looking at the disassembly x0 is filled with the value 0.

add sp, sp,#0x20

This will increment the stack pointer to deallocate the space allocated for the add() function and the sp will point to 0x0000fffffffff350 which was the top of the stack before branching to the add() function. The stack frame for the add()function is now destroyed. The stack looks like the below figure now.

When the ret is hit the program will go back to the main function.

Now we are back inside the main function.

Let’s execute the remaining instructions in the main function.

str x0, [sp, #24] will store the value of x0 into the [sp + 24]. If we look at x0 , it’s currently holding the value 0.

Let’s step over this instruction using ni and inspect this location.

As expected it contains 0.

The next ldr x0,[sp, #24] will load this value back to x0. So x0 becomes zero.

The next ldr w0, [x0] instruction will crash the application. The ldr instruction will try to load the value pointed to by the address in x0 into w0. However, since x0 contains 0, which is not a valid memory address, it will result in a crash.

Now we have a thorough idea about the stack frames and why the program crashed.

Let’s look into a more realistic example.

Example Program

Consider the below c program below.

				
					#include <stdio.h>

void one()
{
int x = 200;
int y = 300;
int z = 400;
printf("Inside Function one() .The value of x is %d, y is %d and z is %d \n",x,y,z);
}

void two()
{
int a;
int b;
printf("Inside Function two() .The value of a is %d and b is %d \n",a,b);
}

void three()
{
int n1 = 1;
int n2 = 2;
int n3;
printf("Inside Function three() .The value of n1 is %d , n2 is %d and n3 is %d \n",n1,n2,n3);
}

void main()
{
one();
two();
three();
}
				
			

Let’ just do a quick inspection on the code.

In the program there are three user defined functions and they are called in the order,

  • one()

  • two()

  • three()

In function one(), there are three local variables x, y, and z. They are assigned various values. In function two(), there are two local variables, but they are not assigned any values. Finally, in function three(), there are three local variables, but only one of them is assigned a value. The values of these local variables in all the functions are printed.

Let’s compile this code using gcc.

gcc uni4.c -o uni4

Now try running it and see what happens.

The output shows the values in each function. In function one(), x, y, and z are assigned the values 200, 300, and 400, respectively. These values are printed as the output. However, in function two(), the values of local variables a and b are shown as 200 and 300 in the output, even though they are not assigned any values. Why is this happening ? if you understood about stack frames very well, you may already have the answer by now. if not, don’t worry we can figure it out together.

Let’s start by inspecting the stack frames of each functions.

When we are inside the function one() the stack will be look roughly like this,

As you can see above from our stack diagram,

  • x is stored at 1020.

  • y is stored at 1024.

  • z is stored at 1028.

(Note : These are not real addresses)

When the program finishes executing function one(), the sp is readjusted to the position before it was calling function one() and the stack frame is discarded. So, the stack will look like this,