Minimal reproduction example with disassembly analysis
main.c
void myfunc(char *const src, int len) {
int i;
for (i = 0; i < len; ++i) {
src[i] = 42;
}
}
int main(void) {
char arr[] = {'a', 'b', 'c', 'd'};
int len = sizeof(arr);
myfunc(arr, len + 1); /* Cause smashing by writing one byte too many. */
return 0;
}
GitHub upstream.
Compile and run:
gcc -fstack-protector-all -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out
fails as desired:
*** stack smashing detected ***: terminated
Aborted (core dumped)
Tested on Ubuntu 20.04, GCC 10.2.0.
On Ubuntu 16.04, GCC 6.4.0, I could reproduce with -fstack-protector
instead of -fstack-protector-all
, but it stopped blowing up when I tested on GCC 10.2.0 as per Geng Jiawen's comment. man gcc
clarifies that as suggested by the option name, the -all
version adds checks more aggressively, and therefore presumably incurs a larger performance loss:
-fstack-protector
Emit extra code to check for buffer overflows, such as stack smashing attacks. This is done by adding a guard variable to functions with vulnerable objects. This includes functions that call "alloca", and functions with buffers larger than or equal to 8 bytes. The guards are initialized when a function is entered and then checked when the function exits. If a guard check fails, an error message is printed and the program exits. Only variables that are actually allocated on the stack are considered, optimized away variables or variables allocated in registers don't count.
-fstack-protector-all
Like -fstack-protector except that all functions are protected.
Disassembly
Now we look at the disassembly:
objdump -D a.out
which contains:
int main (void){
400579: 55 push %rbp
40057a: 48 89 e5 mov %rsp,%rbp
# Allocate 0x10 of stack space.
40057d: 48 83 ec 10 sub $0x10,%rsp
# Put the 8 byte canary from %fs:0x28 to -0x8(%rbp),
# which is right at the bottom of the stack.
400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400588: 00 00
40058a: 48 89 45 f8 mov %rax,-0x8(%rbp)
40058e: 31 c0 xor %eax,%eax
char arr[] = {'a', 'b', 'c', 'd'};
400590: c6 45 f4 61 movb $0x61,-0xc(%rbp)
400594: c6 45 f5 62 movb $0x62,-0xb(%rbp)
400598: c6 45 f6 63 movb $0x63,-0xa(%rbp)
40059c: c6 45 f7 64 movb $0x64,-0x9(%rbp)
int len = sizeof(arr);
4005a0: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
myfunc(arr, len + 1);
4005a7: 8b 45 f0 mov -0x10(%rbp),%eax
4005aa: 8d 50 01 lea 0x1(%rax),%edx
4005ad: 48 8d 45 f4 lea -0xc(%rbp),%rax
4005b1: 89 d6 mov %edx,%esi
4005b3: 48 89 c7 mov %rax,%rdi
4005b6: e8 8b ff ff ff callq 400546 <myfunc>
return 0;
4005bb: b8 00 00 00 00 mov $0x0,%eax
}
# Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc.
# If it has, jump to the failure point __stack_chk_fail.
4005c0: 48 8b 4d f8 mov -0x8(%rbp),%rcx
4005c4: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx
4005cb: 00 00
4005cd: 74 05 je 4005d4 <main+0x5b>
4005cf: e8 4c fe ff ff callq 400420 <__stack_chk_fail@plt>
# Otherwise, exit normally.
4005d4: c9 leaveq
4005d5: c3 retq
4005d6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005dd: 00 00 00
Notice the handy comments automatically added by objdump
's artificial intelligence module.
If you run this program multiple times through GDB, you will see that:
- the canary gets a different random value every time
- the last loop of
myfunc
is exactly what modifies the address of the canary
The canary randomized by setting it with %fs:0x28
, which contains a random value as explained at:
Debug attempts
From now on, we modify the code:
myfunc(arr, len + 1);
to be instead:
myfunc(arr, len);
myfunc(arr, len + 1); /* line 12 */
myfunc(arr, len);
to be more interesting.
We will then try to see if we can pinpoint the culprit + 1
call with a method more automated than just reading and understanding the entire source code.
gcc -fsanitize=address
to enable Google's Address Sanitizer (ASan)
If you recompile with this flag and run the program, it outputs:
#0 0x4008bf in myfunc /home/ciro/test/main.c:4
#1 0x40099b in main /home/ciro/test/main.c:12
#2 0x7fcd2e13d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400798 in _start (/home/ciro/test/a.out+0x40079
followed by some more colored output.
This clearly pinpoints the problematic line 12.
The source code for this is at: https://github.com/google/sanitizers but as we saw from the example it is already upstreamed into GCC.
ASan can also detect other memory problems such as memory leaks: How to find memory leak in a C++ code/project?
Valgrind SGCheck
As mentioned by others, Valgrind is not good at solving this kind of problem.
It does have an experimental tool called SGCheck:
SGCheck is a tool for finding overruns of stack and global arrays. It works by using a heuristic approach derived from an observation about the likely forms of stack and global array accesses.
So I was not very surprised when it did not find the error:
valgrind --tool=exp-sgcheck ./a.out
The error message should look like this apparently: Valgrind missing error
GDB
An important observation is that if you run the program through GDB, or examine the core
file after the fact:
gdb -nh -q a.out core
then, as we saw on the assembly, GDB should point you to the end of the function that did the canary check:
(gdb) bt
#0 0x00007f0f66e20428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1 0x00007f0f66e2202a in __GI_abort () at abort.c:89
#2 0x00007f0f66e627ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175
#3 0x00007f0f66f0415c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7f0f66f7a481 "stack smashing detected") at fortify_fail.c:37
#4 0x00007f0f66f04100 in __stack_chk_fail () at stack_chk_fail.c:28
#5 0x00000000004005f6 in main () at main.c:15
(gdb) f 5
#5 0x00000000004005f6 in main () at main.c:15
15 }
(gdb)
And therefore the problem is likely in one of the calls that this function made.
Next we try to pinpoint the exact failing call by first single stepping up just after the canary is set:
400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400588: 00 00
40058a: 48 89 45 f8 mov %rax,-0x8(%rbp)
and watching the address:
(gdb) p $rbp - 0x8
$1 = (void *) 0x7fffffffcf18
(gdb) watch 0x7fffffffcf18
Hardware watchpoint 2: *0x7fffffffcf18
(gdb) c
Continuing.
Hardware watchpoint 2: *0x7fffffffcf18
Old value = 1800814336
New value = 1800814378
myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
3 for (i = 0; i < len; ++i) {
(gdb) p len
$2 = 5
(gdb) p i
$3 = 4
(gdb) bt
#0 myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
#1 0x00000000004005cc in main () at main.c:12
Now, this does leaves us at the right offending instruction: len = 5
and i = 4
, and in this particular case, did point us to the culprit line 12.
However, the backtrace is corrupted, and contains some trash. A correct backtrace would look like:
#0 myfunc (src=0x7fffffffcf14 "abcd", len=4) at main.c:3
#1 0x00000000004005b8 in main () at main.c:11
so maybe this could corrupt the stack and prevent you from seeing the trace.
Also, this method requires knowing what is the last call of the canary checking function otherwise you will have false positives, which will not always be feasible, unless you use reverse debugging.