About ***stack smashing detected***

本文最后更新于:2023年3月25日 上午

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:

  1. We will explore this topic in GLIBC, which is the abbreviation of GNU C Library.
  2. 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.
  3. All subsequent content is compared to glibc-2.23 or the previous version.

glibc-2.23

1
2
3
4
5
// debug/stack_chk_fail_local.c
#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 (); } // call __stack_chk_fail
1
2
3
4
5
6
// debug/stack_chk_fail.c
#include <stdio.h>
#include <stdlib.h>
extern char **__libc_argv attribute_hidden;
void __attribute__ ((noreturn))
__stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } // call __fortify_fail
1
2
3
4
5
6
7
8
9
10
11
// debug/fortify_fail.c
#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", /* use 2 for stderr */
msg, __libc_argv[0] ?: "<unknown>"); // call __libc_message
}
libc_hidden_def (__fortify_fail) // using libc_hidden_def, is not useful enough in this topc
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
106
// sysdeps/posix/libc_fatal.c
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

/* Open a descriptor for /dev/tty unless the user explicitly
requests errors on standard error. */
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')
{
/* Find the next "%s" or the end of the string. */
const char *next = cp;
while (next[0] != '%' || next[1] != 's')
{
next = __strchrnul (next + 1, '%');

if (next[0] == '\0')
break;
}
/* Determine what to print. */
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';
/* We have to free the old buffer since the application might
catch the SIGABRT signal. */
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);
/* 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.

1
2
3
4
5
6
7
// include/stdio.h
enum __libc_message_action
{
do_message = 0, /* Print message. */
do_abort = 1 << 0, /* Abort. */
do_backtrace = 1 << 1 /* Backtrace. */
}; // do_message=0, do_abort=1, do_backtrace=2
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
// sysdeps/posix/libc_fatal.c
void __libc_message (enum __libc_message_action action, const char *fmt, ...) {

/*
same to 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. */
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);
}

/*
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.

1
2
3
4
5
6
7
8
// debug/stack_chk_fail.c
#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)

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“.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// debug/fortify_fail.c
#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
// include/stdio.h
enum __libc_message_action
{
do_message = 0, /* Print message. */
do_abort = 1 << 0, /* Abort. */
};

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.

1
2
3
4
5
// debug/stack_chk_fail.c
#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
// debug/fortify_fail.c
#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)

Something wrong? Why is there less code this time? It was as if times had gone backwards!

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
// sysdeps/posix/libc_fatal.c
void __libc_message (enum __libc_message_action action, const char *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. */
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);

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]”.


About ***stack smashing detected***
http://example.com/2023/03/23/About-stack-smashing-detected/
作者
OSLike
发布于
2023年3月23日
许可协议