6 minutes
Buffer Overflow on ARM - Part 1
Hello again faithful readers! Today’s post is about exploiting a buffer overflow.
A few days ago I was invited to attempt some binary exploitation challenges for ARM architecture. Of course I agreed, despite the fact that I have no experience debugging ARM binaries. That’s okay though, it’s how you learn.
I was happy to see the source code provided, and even happier when I was able to quickly identify the path to success without needing a debugger at all. Here’s the code:
int main(int argc, char **argv)
{
struct {
char buffer[0x80]; //Reserves 0x80 (128) bytes in memory.
char secret[0x10]; //Reserves 0x10 (16) bytes in memory.
} local_vars; //Allows access to reserved memory.
int i;
for(i=0; i<0x10; i++) //Loops 0x10 (16) times.
{
//Stores a random number in local_vars.secret
local_vars.secret[i] = (char)(random()%94+33);
}
printf("Try to guess my secret:");
//Saves input from STDIN in local_vars.buffer
gets(local_vars.buffer);
//Compares values of buffer against secret, gives flag if they match.
if(!strcmp(local_vars.buffer,local_vars.secret))
{
printf("Congratulations, here's your flag %s", FLAG);
}
else
{
printf("Failed!\n";);
}
}
I’ve commented the code explaining what each piece does. Keep in mind, when numbers are defined using 0xn
notation it means they are hexadecimal numbers, so 0x80
is 128
bytes.
The vulnerability exists because of the function gets()
, which has since been removed from glibc. You can read about it here: https://linux.die.net/man/3/fgets
gets() reads a line from stdin into the buffer pointed to by s until either a terminating newline or EOF,
which it replaces with a null byte ('\0').
No check for buffer overrun is performed (see BUGS below).
The gets command will continue to store values from STDIN in memory until it reaches a newline
, EOF
, or it runs out of memory.
So how can we exploit this?
Well, seeing as we only have 128 bytes allocated for our input, once we hit the 129th byte it will continue to overwrite into the 16 bytes reserved for the local_vars.secret
data. We can overwrite the secret with whatever value we want by sending 128 characters to input. The next 16 characters of our input will become the new value for secret.
The code will then compare the strings using strcmp
, which…well… according to the man page:
The strcmp() function compares the two strings s1 and s2. It returns an integer less than, equal to, or greater than zero if s1 is found, respectively, to be less than, to match, or be greater than s2.
That didn’t really make sense to me, so I found the source code for the function to see what’s going on. Here it is:
STRCMP (const char *p1, const char *p2) // p1 and p2 are memory addresses
{
//saves both addresses in new memory location
const unsigned char *s1 = (const unsigned char *) p1;
const unsigned char *s2 = (const unsigned char *) p2;
//allocate memory for values at addresses
unsigned char c1, c2;
do
{
c1 = (unsigned char) *s1++; //saves value at address
c2 = (unsigned char) *s2++; //saves value at address
if (c1 == '\0') //check first value for null byte
//subtract value of c1 from c2.
return c1 - c2;
}
//keep going if it matches and there's no null byte yet
while (c1 == c2);
//If it ever doesnt match, end up here.
return c1 - c2;
}
I added comments to the code, but basically it iterates through both strings one character at a time until it hits a null byte (0) in the first string, or a character which doesn’t match. It then returns the value of the last characters compared, subtracted from each other, which will always be 0 if it’s a match.
So the takeaway here is, gets()
won’t stop at null bytes, but strcmp()
will. Because of this, we can happily pass null bytes to the input, and still execute an overflow into local_vars.secret
Crafting the payload (the magic part)
Knowing this we can craft a payload that will overwrite local_vars.secret
with whatever value we want, and can set our local_vars.buffer
to the same string using null bytes to terminate it for strcmp()
. On top of that, we can abuse the fact the gets()
will happily save as many null bytes as we want, which means we can make both our input, and the secret, immediately terminate by setting the first byte value to 0. It will work like this:
0
+ 127 bytes of whatever we want
+ \n (for newline)
That’s 129 bytes of data, for the 128 byte buffer. Let’s step through the logic and see why this works.
char buffer[0x80]; //Reserves 0x80 (128) bytes in memory.
char secret[0x10]; //Reserves 0x10 (16) bytes in memory.
This code reserves 128 bytes
in memory for our buffer, and 16 bytes for our secret. This is a continuous block of memory, so immediately after the 128 bytes for the buffer are the 16 bytes for the secret.
gets(local_vars.buffer); //Saves input from STDIN in memory allocated for local_vars.buffer
This accepts our input, which is 0
+ 127 bytes of whatever
+ \n
, and stores it in memory starting at the address reserved for our buffer. Once it hits the \n
value, it will know to stop copying. On top of that, it replaces the \n
with 0
, this is simply how the function works. That’s not something I knew off hand, I had to research the gets()
function.
Since the memory allocated for the secret is immediately after the first byte of the secret is now 0
since gets()
replaced \n
with it.
if(!strcmp(local_vars.buffer,local_vars.secret))
This conditional statement checks if the value returned by strcmp()
is 0. We must again refer to documentation, or in this case the source code, to understand what the function is doing.
if (c1 == '\0') //check first value for null byte
In the above snippet from the strcmp()
source code, it checks the first byte at the starting memory address of the first argument passed to the function, which in this case is local_vars.buffer
. We set this byte to 0
with our payload.
Since the if condition is met, the following code is executed:
return c1 - c2; //if null, subtract value of c1 from c2. will be 0 if same
So, it will subtract c2
from c1
. Well c2
is the value at the starting memory address of the second argument passed to the function, which in this case is local_vars.secret
. Since our payload overwrote this value by exceeding the 128 byte buffer, the value here is now 0
.
Well, 0-0 = 0
, so it returns 0, which means the statement if(!strcmp(local_vars.buffer,local_vars.secret))
evaluates to true, and execution continues to this line:
printf("Congratulations, here's your flag %s", FLAG);
Now getting these values to the actual program is kind of tricky. I made a post that goes into more detail about how to do this.
I’m not going to leave you hanging though, the following python statement will send raw binary, represented by \xNN
where N is a hexadecimal digit, to netcat:
python -c 'print("\x00"*128 + "\n")' | nc localhost 9001