4 minutes
Buffer Overflow on ARM - Part 2
Hello again faithful readers! Today we are going to explore another buffer overflow on an ARM binary. The previous challenge involved overwriting a variable. This one is going to overflow the return address instead.
What’s the return address?
The return address is a value stored on the stack whenever a function is called. After the function finishes, the CPU will load the value of the return address and attempt to execute the data there. This means if we can overflow the return address we can get the CPU to jump to any memory address.
Exploring the Challenge
Here’s the source code for this challenge:
#include <stdio.h>
#include "flag.h"
void win()
{
printf("Wait, what? How?\n%s\n", FLAG);
}
void test()
{
char buffer[0x80];
printf("This time, there really is no way to get to the flag.\nDo you finally give up?\n");
gets(buffer);
if(strcmp(buffer,"Yes")){
printf("That wasn't a \"Yes\"\nYou better give up now\n");
}
else
{
printf("Damn right\n");
}
}
int main(int argc, char **argv)
{
test();
}
The important bits:
- The function which prints the flag,
win()
is never called in this code. - The input is evaluated by calling the function
test()
. gets()
is used again to store the input in memory.- The buffer for the input is
128 bytes
in size.
This all means we can overflow the memory, into the return address, and point the execution to whatever address we like.
Finding the Right Address
To find the right address we are going to take a look at the assembly of the binary. There’s a lovely tool in Linux called objdump
that will output the assembly if given the -d
tag.
Now, when I tried a standard objdump -d
the program complained about the architecture being unknown. After a quick search I tried objdump -d -m arm
, but it complained about not being able to do that.
Another, somewhat longer search, eventually led to the solution:
apt install gcc-arm-none-eabi
arm-none-eabi-objdump -d a.outl
That apt package installed an arm specifc objdump, and from that I was able to get the assembly. It outputs quite a lot that’s not relevant to our current interests, so I ran it again and piped it into a grep statement:
arm-none-eabi-objdump -d a.out | grep win
There’s our address: 000104b0
If we overwrite the return address with that value, it should continue through the win()
function which will print our flag.
Crafting the Payload
We have 128 bytes to enter before we being to overflow. After that we begin to overflow into memory used for other purposes. Last time it was used for another variable, but this time there is no such variable declared so we end up overflowing into the metadata of the stack.
When a function is called in ARM assembly, the function creates a “stack frame”. There’s a lot of that can be said about this concept, but we can get some help using this image from Azeria Labs:
From the image we can see the local variables are stored immediately before the frame pointer (FP), and link register (LR). These are both 4 byte memory addresses stored on the stack. The link register is also known as the return address, so that’s the value we want to overwrite with 000104b0
to jump to the win()
function.
To do so we fill up our local variable with 128 bytes + 4 bytes to overwrite the frame pointer (this can be whatever we want), and finally 4 more to overwrite the link register, AKA return address.
There’s one catch though…
We have to write the memory address backwards, one byte at a time. This is because the binary we are exploiting was compiled for ARM in Little Endian mode. There’s a lot that can be said about that, but suffice to say for this post we need to simply be aware of this fact and adjust our payload properly.
So 00 01 04 b0
becomes b0 04 01 00
. Our final payload becomes
128 bytes of anything for buffer
+ 4 bytes of anything to overwrite frame pointer
+ address we want to jump to
We can use python to pipe the payload into netcat like so:
python -c 'print("\x41"*132 + "\xb0\x04\x01\x00")' | nc localhost 9003
Bingo, there’s our flag.
You might have noticed it shows the failure message first. That’s because the overflow doesn’t affect anything until the function completes, and the return popped off the stack so execution can jump to it.