本文最后更新于:2023年3月25日 上午
[toc]
“本文为《About ***stack smashing detected***》的翻译版本”
关于***stack smashing detected***
GCC编译器有一个参数叫做”stack-protector“,一共被分为了四种,分别是”fstack-protector“、 “fstack-protector-all“、”fstack-protector-strong“和”fstack-protector-explicit“。它们被用来防止栈溢出。当程序因为函数”__stack_chk_fail“而瘫痪时,你就会收到”*** stack smashing detected ***: <NameofProgram> terminated\n“和 “SIGABRT”信号。
今天我们已然知晓Canary作为一个随机数可以防止栈溢出,但是你应该去了解”__stack_chk_fail“是如何使你的程序瘫痪的。(本文后面都会用crash来表示“程序瘫痪”之意)
在这篇文章中,我会带领你去找寻在常见的libc中”__stack_chk_fail“是如何让你的程序crash的。
注:
- 我们探讨的主题是GLIBC,就是”GNU C Library”的缩写。
- 所有在文件中看似无尽的循环都只是想让gcc“开心”,这样就可以防止gcc在编译时因为没有发生return而报错。
- 所有后续内容都与glibc-2.23或上一版本进行比较。
不同的glibc版本
glibc-2.23
1 2 3 4 5
| #include <sys/cdefs.h> extern void __stack_chk_fail (void) __attribute__ ((noreturn)); void __attribute__ ((noreturn)) attribute_hidden __stack_chk_fail_local (void) { __stack_chk_fail (); }
|
1 2 3 4 5 6
| #include <stdio.h> #include <stdlib.h> extern char **__libc_argv attribute_hidden; void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); }
|
1 2 3 4 5 6 7 8 9 10 11
| #include <stdio.h> #include <stdlib.h> extern char **__libc_argv attribute_hidden; void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg){ while (1) __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>"); } libc_hidden_def (__fortify_fail)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| void __libc_message (int do_abort, const char *fmt, ...) { va_list ap; int fd = -1; va_start (ap, fmt); #ifdef FATAL_PREPARE FATAL_PREPARE; #endif const char *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;
struct str_list *list = NULL; int nlist = 0;
const char *cp = fmt; while (*cp != '\0') { const char *next = cp; while (next[0] != '%' || next[1] != 's') { next = __strchrnul (next + 1, '%');
if (next[0] == '\0') break; } const char *str; size_t len; if (cp[0] == '%' && cp[1] == 's') { str = va_arg (ap, const char *); len = strlen (str); cp += 2; } else { str = cp; len = next - cp; cp = next; } struct str_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) { struct iovec *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)); struct abort_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';
struct abort_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); abort (); } }
|
在”sysdeps/posix/libc_fatal.c“中我们可以找到一个叫”__libc_message“函数。如果我们调用这个函数,那么环境变量”LIBC_FATAL_STDERR_“将会从”__libc_secure_gentenv“函数读入值,如果”LIBC_FATAL_STDERR_“的值是’\0’或NULL,stderr会被重定向到”_PATH_TTY‘(一般就是”/dev/tty“这个路径),于是stderr就会被打印在终端上。所以尽管”__fortify_fail“函数有一个看似是无限循环的”while(1)”,但实际上它并没有造成无限循环的效果。
在这样复杂的调用之后,msg字符串和__libc_argv[0]或”<unknown>“会被在终端上输出。
glibc-2.25
“/debug/stack_chk_fail.c“是与在glibc-2.23中的相同位置上的文件是相同的。
在”/debug/stack_chk_fail.c“中,开发者在最后加上了一句strong_alias (__stack_chk_fail, __stack_chk_fail_local)
,但是这仍然在本主题中并无特殊用途,故不进行探究。
但是在”sysdeps/posix/libc_fatal.c“中的函数”__libc_message“相对于在glibc-2.23中相同位置上的文件是有一些区别的。
1 2 3 4 5 6 7
| enum __libc_message_action { do_message = 0, do_abort = 1 << 0, do_backtrace = 1 << 1 };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| void __libc_message (enum __libc_message_action action, const char *fmt, ...) {
if ((action & do_backtrace)) { const char *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); }
while (*cp != '\0') {
if ((action & do_abort)) { if ((action & do_backtrace)) BEFORE_ABORT (do_abort, written, fd); abort (); } }
|
相对来说区别不是很大,但是变得更加的标准规范了。
glibc-2.27
“/debug/stack_chk_fail.c“是与在glibc-2.23中的相同位置上的文件是相同的。
1 2 3 4 5 6 7 8
| #include <stdio.h> #include <stdlib.h> #include <stdbool.h> extern char **__libc_argv attribute_hidden; void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail_abort (false, "stack smashing detected"); } strong_alias (__stack_chk_fail, __stack_chk_fail_local)
|
我们注意到原本调用”stack smashing detected”这个字符串的函数被改为了”__fortify_fail_abort“,并且还添加了另外一个被定义在头文件”stdboo.h“中的参数”false”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> #include <stdlib.h> #include <stdbool.h> extern char **__libc_argv attribute_hidden; void __attribute__ ((noreturn)) __fortify_fail_abort (_Bool need_backtrace, const char *msg) { while (1) __libc_message (need_backtrace ? (do_abort | do_backtrace) : do_abort, "*** %s ***: %s terminated\n", msg, (need_backtrace && __libc_argv[0] != NULL ? __libc_argv[0] : "<unknown>")); } void __attribute__ ((noreturn)) __fortify_fail (const char *msg) { __fortify_fail_abort (true, msg); } libc_hidden_def (__fortify_fail) libc_hidden_def (__fortify_fail_abort)
|
1 2 3 4 5 6
| enum __libc_message_action { do_message = 0, do_abort = 1 << 0, };
|
在这个glibc版本中,发布者通过添加一个叫need_backtrace的布尔值变量,去避免当”__libc_argv[0]“指向损坏的堆栈空间时的回溯操作。
“__libc_message“是与在glibc-2.25相同位置上的文件相同的。
所以glibc-2.27在函数”__libc_message“是否需要回溯上变得更加严格,从而变得更加的安全。
glibc-2.31/glibc-2.36/glibc-2.37
“/debug/stack_chk_fail.c“是与在glibc-2.23中的相同位置上的文件是相同的。
1 2 3 4 5
| #include <stdio.h> void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } strong_alias (__stack_chk_fail, __stack_chk_fail_local)
|
1 2 3 4 5
| #include <stdio.h> void __attribute__ ((noreturn)) __fortify_fail (const char *msg) { while (1) __libc_message (do_abort, "*** %s ***: terminated\n", msg); } libc_hidden_def (__fortify_fail)
|
怎么回事?为什么这里使用了相对于上版本更少的代码?仿佛时代倒退了一样!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void __libc_message (enum __libc_message_action action, const char *fmt, ...) {
if (nlist > 0) {
} WRITEV_FOR_FATAL (fd, iov, nlist, total);
if ((action & do_abort)) {
if ((action & do_abort)) abort (); } }
|
从glibc-2.31开始,发布者删去了相对于glibc-2.27中的以下内容:
1 2 3 4
| const char *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);
|
因为他们删去了BEFORE_ABORT (do_abort, written, fd);
,这些操作变得不再重要并且不再有效。
所以我们可以说glibc-2.31中的代码经过优化,在不改变安全性的情况下提高了可读性和可用性,但是攻击的操作仍可以不改变。
总结
综上所述,当一个ELF文件运行时,其文件名会在内存中保存,并且在第一个产生的rbp寄存器之前。所以如果我们知道输入的位置距离argv[0]的字长,我们就可以利用stack smash attach这种攻击方式去获得修改后”argv[0]”的值。