关于glibc-SSP发生方式在不同版本中的研究

本文最后更新于: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的。

注:

  1. 我们探讨的主题是GLIBC,就是”GNU C Library”的缩写。
  2. 所有在文件中看似无尽的循环都只是想让gcc“开心”,这样就可以防止gcc在编译时因为没有发生return而报错。
  3. 所有后续内容都与glibc-2.23或上一版本进行比较。

不同的glibc版本

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 (); } // 调用__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"); } // 调用__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", /* 用2来表示stderr */
msg, __libc_argv[0] ?: "<unknown>"); // 调用__libc_message
}
libc_hidden_def (__fortify_fail) // 调用libc_hidden_def,但在本主题中并无特殊用途,故不进行探究
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
// 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

/* 打开/dev/tty的描述符,除非用户显式地在标准错误上请求错误。 */
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')
{
/* 找到下一个"%s"或字符串的结尾。 */
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';
/* 我们必须释放旧缓冲区,
因为应用程序可能会捕获SIGABRT信号。*/
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
// include/stdio.h
enum __libc_message_action
{
do_message = 0, /* 打印信息 */
do_abort = 1 << 0, /* 中断 */
do_backtrace = 1 << 1 /* 回溯 */
}; // 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
36
// sysdeps/posix/libc_fatal.c
void __libc_message (enum __libc_message_action action, const char *fmt, ...) {

/*
与glibc-2.23相同
*/

/*
没有做回溯前不要调用__libc_secure_getenv,
不然可能会访问损坏的堆栈区
*/
if ((action & do_backtrace)) // 我们可以在注释中明晰创作者的意图
{ // 这其实在很多时候并不会起效
/* 打开/dev/tty的描述符,除非用户显式地在标准错误上请求错误。 */
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);
}

/*
与glibc-2.23相同
*/

while (*cp != '\0')
{
/*
与glibc-2.23相同
*/
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
// 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)

我们注意到原本调用”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
// debug/fortify_fail.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h> // 此处也使用该头文件去定义need_backtrace为一个布尔型变量
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, /* 打印信息 */
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
// 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)

怎么回事?为什么这里使用了相对于上版本更少的代码?仿佛时代倒退了一样!

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, ...)
{
/*
与glibc-2.23相同
*/
if (nlist > 0)
{
/*
与glibc-2.23相同
*/
}
/* 删去了 written = */
WRITEV_FOR_FATAL (fd, iov, nlist, total);

if ((action & do_abort))
{
/*
与glibc-2.25相同
*/
if ((action & do_abort))
/* 关闭进程 */
abort ();
}
}

从glibc-2.31开始,发布者删去了相对于glibc-2.27中的以下内容:

1
2
3
4
/* 打开/dev/tty的描述符,除非用户显式地在标准错误上请求错误。 */
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]”的值。


关于glibc-SSP发生方式在不同版本中的研究
http://example.com/2023/03/25/关于glibc-SSP发生方式在不同版本中的研究/
作者
OSLike
发布于
2023年3月25日
许可协议