GCC compiler has a compile parameter which named “stack-protector“, divided into 4 kinds, “fstack-protector“, “fstack-protector-all“, “fstack-protector-strong“ and “fstack-protector-explicit“. These are used to protect the stack from being attack. When the program crash because of function “__stack_chk_fail“, you will receive “*** stack smashing detected ***: <NameofProgram> terminated\n“ and “SIGABRT” signal.
Today all we know that Canary as a random number can protect our program from stack exploitation, but now you may need to try to learn how “__stack_chk_fail“ make your program crash.
In this article, I will try to find out how “__stack_chk_fail“ make your program crash in common libc.
Note:
We will explore this topic in GLIBC, which is the abbreviation of GNU C Library.
All the seemingly endless loops only want to make gcc “happy”, so that it can prevent gcc from reporing an error because of no retun occurs.
All subsequent content is compared to glibc-2.23 or the previous version.
/* Open a descriptor for /dev/tty unless the user explicitly requests errors on standard error. */ constchar *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_"); if (on_2 == NULL || *on_2 == '\0') fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1) fd = STDERR_FILENO;
structstr_list *list =NULL; int nlist = 0;
constchar *cp = fmt; while (*cp != '\0') { /* Find the next "%s" or the end of the string. */ constchar *next = cp; while (next[0] != '%' || next[1] != 's') { next = __strchrnul (next + 1, '%');
if (next[0] == '\0') break; } /* Determine what to print. */ constchar *str; size_t len; if (cp[0] == '%' && cp[1] == 's') { str = va_arg (ap, constchar *); len = strlen (str); cp += 2; } else { str = cp; len = next - cp; cp = next; } structstr_list *newp = alloca (sizeof (struct str_list)); newp->str = str; newp->len = len; newp->next = list; list = newp; ++nlist; }
bool written = false; if (nlist > 0) { structiovec *iov = alloca (nlist * sizeof (struct iovec)); ssize_t total = 0; for (int cnt = nlist - 1; cnt >= 0; --cnt) { iov[cnt].iov_base = (char *) list->str; iov[cnt].iov_len = list->len; total += list->len; list = list->next; }
written = WRITEV_FOR_FATAL (fd, iov, nlist, total);
if (do_abort) { total = ((total + 1 + GLRO(dl_pagesize) - 1) & ~(GLRO(dl_pagesize) - 1)); structabort_msg_s *buf = __mmap (NULL, total, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if (__glibc_likely (buf != MAP_FAILED)) { buf->size = total; char *wp = buf->msg; for (int cnt = 0; cnt < nlist; ++cnt) wp = mempcpy (wp, iov[cnt].iov_base, iov[cnt].iov_len); *wp = '\0'; /* We have to free the old buffer since the application might catch the SIGABRT signal. */ structabort_msg_s *old = atomic_exchange_acq (&__abort_msg, buf); if (old != NULL) __munmap (old, old->size); } } }
va_end (ap);
if (do_abort) { BEFORE_ABORT (do_abort, written, fd); /* Kill the application. */ abort (); } }
Then in “sysdeps/posix/libc_fatal.c“ we can find function “__libc_message“. If we call this function, the envionment variables which named “LIBC_FATAL_STDERR_“ will be scand with function “__libc_secure_gentenv“, if the value of “LIBC_FATAL_STDERR_“ is ‘\0’ or NULL, stderr will be redirected to “_PATH_TTY‘’, which value is always the path “/dev/tty“, so the stderr will be printed in terminal. So even thought there is a “while(1)” in “__fortify_fail“ look like an endless loop, it’s not.
After such these calls, msg and __libc_argv[0] or “<unknown>“ will be printed out in the terminal.
glibc-2.25
“/debug/stack_chk_fail.c“ is same to the file in the same location in glibc-2.23.
In “/debug/stack_chk_fail.c“, they finally addstrong_alias (__stack_chk_fail, __stack_chk_fail_local) but it is still not useful enough to explian in this topic.
But function “__libc_message“in “sysdeps/posix/libc_fatal.c“ has some difference to the file in the same location in glibc-2.23.
/* Don't call __libc_secure_getenv if we aren't doing backtrace, which may access the corrupted stack. */ if ((action & do_backtrace)) // In comments, we can clarify the creator's intention. { // It does not matter a lot of time actually /* Open a descriptor for /dev/tty unless the user explicitly requests errors on standard error. */ constchar *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_"); if (on_2 == NULL || *on_2 == '\0') fd = __open_nocancel (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY); }
/* same to glibc-2.23 */
while (*cp != '\0') { /* same to glibc-2.23 */ if ((action & do_abort)) { if ((action & do_backtrace)) BEFORE_ABORT (do_abort, written, fd); /* Kill the application. */ abort (); } }
Relatively little difference, but it does become more standardized.
glibc-2.27
“/debug/stack_chk_fail.c“ is same to the file in the same location in glibc-2.23.
We see that the function who calls the string “stack smashing detected” is changed into “__fortify_fail_abort“ and another parament “false” is added, which is imported from header “stdboo.h“.
In this glibc version, publisher add a boolean number whose name is need_backtrace, so that we can use this to avoid doing backtrace since “__libc_argv[0]“ may point to the corrupted stack.
“__libc_message“ is same to glibc-2.25.
So glibc-2.27 is more secure about whether backtrace is needed in function”__libc_message“.
glibc-2.31/glibc-2.35/glibc-2.36
“/debug/stack_chk_fail.c“ is same to the file in the same location in glibc-2.23.
// sysdeps/posix/libc_fatal.c void __libc_message (enum __libc_message_action action, constchar *fmt, ...) { /* same to glibc-2.23 */ if (nlist > 0) { /* same to glibc-2.23 */ } /* remove written = */ WRITEV_FOR_FATAL (fd, iov, nlist, total);
if ((action & do_abort)) { /* same to glibc-2.25 */ if ((action & do_abort)) /* Kill the application. */ abort (); } }
In glibc-2.31 the publisher removed the following relative to glibc-2.27:
1 2 3 4 5
/* Open a descriptor for /dev/tty unless the user explicitly requests errors on standard error. */ constchar *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_"); if (on_2 == NULL || *on_2 == '\0') fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);
Because they removeBEFORE_ABORT (do_abort, written, fd);, these operations are no longer effective and fd is not fully effective.
So you can probably tell that some of the code in glibc-2.31 has been optimized to improve readability and usability without changing security. But it doesn’t change the attacker’s operations much.
Finally
After all is said and done, as an ELF file is running, it’s filename will be saved into the loader, and before the first rbp register. So if we can get the length from the current input position to the address of argv[0], we can use stack smash attach to get the modified value of “argv[0]”.