=============================== Lec05: Bypassing stack canaries =============================== 1. Revisiting "crackme0x00" =========================== This is the reversed-engineered source code of the crackme0x00 challenge that we are familiar with: ------------------------------------------------------------ $ cat crackme0x00.c #include #include #include #include int main(int argc, char *argv[]) { 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 -fno-stack-protector -z execstack -o crackme0x00-nossp-exec crackme0x00.c checksec --file crackme0x00-nossp-exec [*] '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 -fno-stack-protector -o crackme0x00-nossp-noexec crackme0x00.c checksec --file crackme0x00-nossp-noexec [*] '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 -fstack-protector -o crackme0x00-ssp-exec -z execstack crackme0x00.c checksec --file crackme0x00-ssp-exec [*] '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 -fstack-protector -o crackme0x00-ssp-noexec crackme0x00.c checksec --file crackme0x00-ssp-noexec [*] '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} 2. Let's crash the "crackme0x00" binary ======================================= crackme0x00-nossp-exec behaves exactly same as "crackme0x00". Not sparingly, it crashes with a long input: $ echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ./crackme0x00-nossp-exec IOLI Crackme Level 0x00 Password:Invalid Password! Segmentation fault (core dumped) What about crackme0x00-ssp-exec that is compiled with a stack protector? $ echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ./crackme0x00-ssp-exec IOLI Crackme Level 0x00 Password:Invalid Password! *** stack smashing detected ***: ./crackme0x00-ssp-exec terminated Aborted (core dumped) 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 ***: crackme0x00-ssp-noexec terminated Program received signal SIGABRT, Aborted. 0xf7fdb440 in __kernel_vsyscall () (gdb) bt #0 0xf7fdb440 in __kernel_vsyscall () #1 0xf7e4d687 in raise () from /lib/i386-linux-gnu/libc.so.6 #2 0xf7e50ab3 in abort () from /lib/i386-linux-gnu/libc.so.6 #3 0xf7e87fd3 in ?? () from /lib/i386-linux-gnu/libc.so.6 #4 0xf7f1ab8b in __fortify_fail () from /lib/i386-linux-gnu/libc.so.6 #5 0xf7f1ab1a in __stack_chk_fail () from /lib/i386-linux-gnu/libc.so.6 #6 0x080485ae in main (argc=97, argv=0xffffd734) at crackme0x00.c:19 3. 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 2017-09-22 10:52:38.378422450 -0400 +++ /dev/fd/62 2017-09-22 10:52:38.378422450 -0400 @@ -6,7 +6,13 @@ push ebx push ecx or DWORD PTR [esp],0x0 - add ebx,0x1ab4 + add ebx,0x1a64 + mov eax,ecx + mov eax,DWORD PTR [eax+0x4] + mov DWORD PTR [ebp-0x2c],eax + mov eax,gs:0x14 + mov DWORD PTR [ebp-0xc],eax + xor eax,eax sub esp,0xc push eax add esp,0x10 @@ -29,6 +35,8 @@ push eax add esp,0x10 mov eax,0x0 + mov edx,DWORD PTR [ebp-0xc] + xor edx,DWORD PTR gs:0x14 pop ecx pop ebx 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: 8048634: 8b 55 f4 mov edx,DWORD PTR [ebp-0xc] 8048637: 65 33 15 14 00 00 00 xor edx,DWORD PTR gs:0x14 804863e: 74 05 je 8048645 8048640: e8 7b 00 00 00 call 80486c0 <__stack_chk_fail_local> __stack_chk_fail() is the function you observed in the gdb's backtrace. 4. Stack Canary =============== This extra value is called, "canary" (a bird, umm why?). More precisely, what are these values? $ gdb ./crackme0x00-ssp-exec (gdb) br *0x08048533 (gdb) r => 0x08048533 <+22>: mov %eax,0x2c(%esp) (gdb) info r eax eax 0x8a2db00 144890624 (gdb) r => 0x08048533 <+22>: mov %eax,0x2c(%esp) (gdb) info r eax eax 0x41d63500 1104557312 Did you notice the canary value keeps changing? This is great because attackers truly guess (i.e., bypass) the canary value before exploitation. 5. 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 -d ./crackme0x00-ssp ... Instead of this: mov %gs:0x14,%eax mov %eax,0x2c(%esp) xor %eax,%eax What about this? This implementation uses a "known" value (i.e., 0xdeadbeef) as a stack canary. movl $0xdeadbeef,0x2c(%esp) So the stack should be like: |<-- 0x14 ------------>|+--- ebp top v [ [ ][canary][fp][ra][ ....] |<---- 0x30 ------------------->| [Task] How could we exploit this program? like last week's tutorial?