Tut04: Bypassing Stack Canaries

In this tutorial, we will explore a defense mechanism against stack overflows, namely the stack canary. It is indeed the most primitive form of defense, yet powerful and performant, so very popular in most, if not all, binaries you can find in modern distributions. The lab challenges showcase a variety of designs of stack canaries, and highlight their subtle pros and cons in various target applications.

Step 0. Revisiting "crackme0x00"

This is the original source code of the crackme0x00 challenge that we are quite familiar with:

$ cat crackme0x00.c

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

int main(int argc, char *argv[])
{
  setreuid(geteuid(), geteuid());
  char buf[16];
  printf("IOLI Crackme Level 0x00\n");
  printf("Password:");

  scanf("%s", buf);
  
  if (!strcmp(buf, "250382"))
    printf("Password OK :)\n");
  else
    printf("Invalid Password!\n");
  
  return 0;
}

We are going to compile this source code into four different binaries with following options:

$ make
cc -m32 -g -O0 -mpreferred-stack-boundary=2 -no-pie -fno-stack-protector -z execstack -o crackme0x00-nossp-exec crackme0x00.c
checksec --file crackme0x00-nossp-exec
[*] '/tmp/.../tut04-ssp/crackme0x00-nossp-exec'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
cc -m32 -g -O0 -mpreferred-stack-boundary=2 -no-pie -fno-stack-protector -o crackme0x00-nossp-noexec crackme0x00.c
checksec --file crackme0x00-nossp-noexec
[*] '/tmp/.../tut04-ssp/crackme0x00-nossp-noexec'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
cc -m32 -g -O0 -mpreferred-stack-boundary=2 -no-pie -fstack-protector -o crackme0x00-ssp-exec -z execstack crackme0x00.c
checksec --file crackme0x00-ssp-exec
[*] '/tmp/.../tut04-ssp/crackme0x00-ssp-exec'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
cc -m32 -g -O0 -mpreferred-stack-boundary=2 -no-pie -fstack-protector -o crackme0x00-ssp-noexec crackme0x00.c
checksec --file crackme0x00-ssp-noexec
[*] '/tmp/.../tut04-ssp/crackme0x00-ssp-noexec'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

There are a few interesting compilation options that we used:

  1. -fno-stack-protector: do not use a stack protector
  2. -z execstack: make its stack "executable"

So we name each binary with a following convention:

crackme0x00-{ssp|nossp}-{exec|noexec}

Step 1. Let's crash the "crackme0x00" binary

crackme0x00-nossp-exec behaves exactly same as crackme0x00. Not surprisingly, it crashes with a long input:

$ echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ./crackme0x00-nossp-exec
IOLI Crackme Level 0x00
Password:Invalid Password!
Segmentation fault

What about crackme0x00-ssp-exec compiled with a stack protector?

$ echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ./crackme0x00-ssp-exec
IOLI Crackme Level 0x00
Password:Invalid Password!
*** stack smashing detected ***: <unknown> terminated
Aborted

The "stack smashing" is detected so the binary simply prevents itself from exploitation; resulting in a crash instead of being hijacked.

You might want to run gdb to figure out what's going on this binary:

$ gdb  ./crackme0x00-ssp-noexec
Reading symbols from ./crackme0x00-ssp-noexec...done.
(gdb) r
Starting program: crackme0x00-ssp-noexec
IOLI Crackme Level 0x00
Password:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Invalid Password!
*** stack smashing detected ***: <unknown> terminated

Program received signal SIGABRT, Aborted.
0xf7fd5079 in __kernel_vsyscall ()
(gdb) bt
#0  0xf7fd5079 in __kernel_vsyscall ()
#1  0xf7e14832 in __libc_signal_restore_set (set=0xffffd1d4) at ../sysdeps/unix/sysv/linux/nptl-signals.h:80
#2  __GI_raise (sig=6) at ../sysdeps/unix/sysv/linux/raise.c:48
#3  0xf7e15cc1 in __GI_abort () at abort.c:79
#4  0xf7e56bd3 in __libc_message (action=do_abort, fmt=<optimized out>) at ../sysdeps/posix/libc_fatal.c:181
#5  0xf7ef0bca in __GI___fortify_fail_abort (need_backtrace=false, msg=0xf7f677fa "stack smashing detected") at fortify_fail.c:33
#6  0xf7ef0b7b in __stack_chk_fail () at stack_chk_fail.c:29
#7  0x080486e4 in __stack_chk_fail_local ()
#8  0x0804864e in main (argc=97, argv=0xffffd684) at crackme0x00.c:21

Step 2. Let's analyze!

To figure out, how two binaries are different. We (so kind!) provide you a script, ./diff.sh that can easily compare two binaries.

$ ./diff.sh crackme0x00-nossp-noexec crackme0x00-ssp-noexec
--- /dev/fd/63  2019-09-16 16:31:16.066674521 -0500
+++ /dev/fd/62  2019-09-16 16:31:16.066674521 -0500
@@ -3,38 +3,46 @@
        mov    ebp,esp
        push   esi
        push   ebx
-       sub    esp,0x10
-       call   0x8048480 <__x86.get_pc_thunk.bx>
-       add    ebx,0x1aad
-       call   0x80483d0 <geteuid@plt>
+       sub    esp,0x18
+       call   0x80484d0 <__x86.get_pc_thunk.bx>
+       add    ebx,0x1a5d
+       mov    eax,DWORD PTR [ebp+0xc]
+       mov    DWORD PTR [ebp-0x20],eax
+       mov    eax,gs:0x14
+       mov    DWORD PTR [ebp-0xc],eax
+       xor    eax,eax
+       call   0x8048420 <geteuid@plt>
        mov    esi,eax


...
        add    esp,0x4
        mov    eax,0x0
+       mov    edx,DWORD PTR [ebp-0xc]
+       xor    edx,DWORD PTR gs:0x14
+       call   0x80486d0 <__stack_chk_fail_local>
        pop    ebx
        pop    esi
        pop    ebp

Two notable differences are at the function prologue and epilogue. There is an extra value (%gs:0x14) placed right after the frame pointer on the stack:

+       mov    eax,gs:0x14
+       mov    DWORD PTR [ebp-0xc],eax
+       xor    eax,eax

And it validates if the inserted value is same right before returning to its caller:

+       mov    edx,DWORD PTR [ebp-0xc]
+       xor    edx,DWORD PTR gs:0x14
+       call   0x7c0 <__stack_chk_fail_local>

__stack_chk_fail_local() is the function you observed in the gdb's backtrace.

Step 3. Stack Canary

This extra value is called, "canary" (a bird, umm why?). More precisely, what are these values?

$ gdb ./crackme0x00-ssp-exec
(gdb) br *0x0804863d
(gdb) r
...
(gdb) x/1i $eip
=> 0x0804863d <main+167>: mov    edx,DWORD PTR [ebp-0xc]
(gdb) si
(gdb) info r edx
edx            0xcddc8000 -841187328

(gdb) r
...
(gdb) x/1i $eip
=> 0x0804863d <main+167>: mov    edx,DWORD PTR [ebp-0xc]
(gdb) si
(gdb) info r edx
edx            0xe4b8800  239831040

Did you notice the canary value keeps changing? This is great because attackers should truly guess (i.e., bypass) the canary value before exploitation.

Step 4. Bypassing Stack Canary

However, what if the stack canary implementation is not "perfect", meaning that an attacker might be able to guess (i.e., %gs:0x14)?

Let's check out this binary:

$ objdump -M intel -d ./target-ssp
...

Instead of this:

mov    eax,gs:0x14
mov    DWORD PTR [ebp-0xc],eax
xor    eax,eax

What about this? This implementation uses a known value (i.e., 0xdeadbeef) as a stack canary.

mov    DWORD PTR [ebp-0xc],0xdeadbeef

So the stack should be like:

             |<-- 0x1c -------------------->|+--- ebp
  top                                v
  [          [              ][canary][unused][fp][ra][      ....]
  |<----   0x38  -------------------------->|

[Task] How could we exploit this program? like last week's tutorial? and get the flag?

Reference