Linux内存安全机制汇总

Table of Contents

为了避免受到缓存溢出攻击,除了依赖操作系统本身的安全机制,编译器所提供的安全机制也至关重要。所以这里不区分安全机制是由内核提供的,还是编译器,因为它们存在的目的始终都是为了让程序在内存中安全地运行。

1 ASLR

ASLR:Address space layout randomization

内核是否开启随机地址布局取决于变量randomize_va_space,定义在mm/memory.c中:

int randomize_va_space __read_mostly =
#ifdef CONFIG_COMPAT_BRK
					1;
#else
					2;
#endif

为什么这里要用变量而不是宏定义呢——因为这个值是写到/proc/sys/kernel/randomize_va_space中的,是可以修改的,如果用宏就无法修改了。

紧接着变量下面,是禁用函数的定义:

static int __init disable_randmaps(char *s)
{
	randomize_va_space = 0;
	return 1;
}
__setup("norandmaps", disable_randmaps);

比如arch_align_stack(arch/x86/kernel/process.c)使用ASLR:

unsigned long arch_align_stack(unsigned long sp)
{
	if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
		sp -= get_random_int() % 8192;
	return sp & ~0xf;
}

1.1 禁用和启用ASLR

下面这段C代码可以查看是否开启了ASLR:

#include <stdio.h>

int main(void) {
  int *p;
  p = (int*)&p + sizeof(int);
  printf("%x\n", p);
  return 0;
}

执行下面这条命令就可以关闭ASLR:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

值的说明如下:

  • 0:No randomization. Everything is static.
  • 1:Conservative randomization. Shared libraries, stack, mmap(), VDSO and heap are randomized.
  • 2:Full randomization. In addition to elements listed in the previous point, memory managed through brk() is also randomized.

2 GCC保护机制

GCC启用堆栈保护:

-fstack-protector-all

关闭保护:

-fno-stack-protector

当启用堆栈保护后,GCC会修改栈的组织(包括局部变量的顺序),在栈下面加上canary word(术语来源于矿工利用金丝雀来确认是否有气体泄漏,如果金丝雀中毒死了,表明有气体泄露),当函数返回之前,判断canary word是否被覆盖,如果被覆盖了就跳转到__stack_chk_fail函数,这个函数是GCC定义的,__stack_chk_fail函数定义在libssp/ssp.c里:

void
__stack_chk_fail (void)
{
  const char *msg = "*** stack smashing detected ***: ";
  fail (msg, strlen (msg), "stack smashing detected: terminated");
}

加上栈保护后的反汇编代码:

0x0000000000400566 <+0>:     push   %rbp
0x0000000000400567 <+1>:     mov    %rsp,%rbp
0x000000000040056a <+4>:     sub    $0x10,%rsp
0x000000000040056e <+8>:     mov    %fs:0x28,%rax
0x0000000000400577 <+17>:    mov    %rax,-0x8(%rbp) ; 压栈的第一元素就是canary word
0x000000000040057b <+21>:    xor    %eax,%eax
0x000000000040057d <+23>:    movb   $0x61,-0x10(%rbp)
0x0000000000400581 <+27>:    mov    $0x0,%eax
0x0000000000400586 <+32>:    mov    -0x8(%rbp),%rdx
0x000000000040058a <+36>:    xor    %fs:0x28,%rdx
0x0000000000400593 <+45>:    je     0x40059a <main+52> ; 这里判断是否溢出
0x0000000000400595 <+47>:    callq  0x400440 <__stack_chk_fail@plt>
0x000000000040059a <+52>:    leaveq
0x000000000040059b <+53>:    retq

触发异常将提示:

➜  ./a.out 12345534902039092391900000000000000000000000000000000000000000000000234
*** stack smashing detected ***: ./a.out terminated
zsh: segmentation fault (core dumped)  ./a.out

3 栈不可执行

GCC编译后默认栈中不可执行代码:

➜  readelf -l a.out| fgrep stack -i -A1
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10

上面栈信息中,标志位“RW”表示栈可读、可写,但不可执行。

让栈可执行指令的两个办法:

  1. gcc编译时指定:
➜  gcc hello.c -z execstack
  1. 使用execstack命令:
➜  execstack -s a.out

一旦允许栈执行之后,就可在栈的标志位看到“E”:

GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
               0x0000000000000000 0x0000000000000000  RWE    10

4 _FORTIFY_SOURCE宏

如果用GCC编译C/C++代码时指定了_FORTIFY_SOURCE宏,GCC会对一些能导致缓存溢出的函数调用进行修改。详细可见man feature_test_macros。

比如下面这段代码,当把“hello world”字符串用strcpy函数复制到一个只能容纳5个字符的数组中,是明显会导致溢出的:

#include <string.h>

int main() {
  char string[5];
  strcpy(string, "hello world");
  return 0;
}

当编译时定义_FORTIFY_SOURCE宏时,GCC会发出警告:

$ gcc -D_FORTIFY_SOURCE=1 test_fortify.c -O
In file included from /usr/include/string.h:635:0,
                 from test_fortify.c:1:
在函数‘strcpy’中,
    内联自‘main’于 test_fortify.c:5:3:
/usr/include/bits/string3.h:110:10: 警告:对 __builtin___strcpy_chk 的调用总是导致目标缓冲区溢出
   return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

上面的例子虽然可以在编译时静态检测出问题,但比较少见,因为多数缓存溢出是发生在不可静态检测的情况下。如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
  char string[5];
  strcpy(string, argv[1]);
  printf("%s\n", string);
  return 0;
}

编译器无法通过静态分析知道程序的第一个参数是否会发生溢出,此处参数超过5字节会被溢出,但是否溢出取决于用户的输入。在_FORTIFY_SOURCE启用的情况下,发生溢出时程序会被及时中止,并给出提示:

a.out*  test_fortify.c
$ gcc -D_FORTIFY_SOURCE=1 test_fortify.c -O
$ ./a.out 1111111111111111111
*** buffer overflow detected ***: ./a.out terminated
======= Backtrace: =========
/lib64/libc.so.6(+0x791fb)[0x7f1b455091fb]
/lib64/libc.so.6(__fortify_fail+0x37)[0x7f1b455aa187]
/lib64/libc.so.6(+0x118120)[0x7f1b455a8120]
/lib64/libc.so.6(+0x117482)[0x7f1b455a7482]
./a.out[0x40057b]
/lib64/libc.so.6(__libc_start_main+0xf1)[0x7f1b454b0401]
./a.out[0x40049a]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:00 657528                             /home/lu4nx/tmp/shellcode/test_fortify/a.out
00600000-00601000 r--p 00000000 fd:00 657528                             /home/lu4nx/tmp/shellcode/test_fortify/a.out
00601000-00602000 rw-p 00001000 fd:00 657528                             /home/lu4nx/tmp/shellcode/test_fortify/a.out
01334000-01355000 rw-p 00000000 00:00 0                                  [heap]
7f1b45279000-7f1b4528f000 r-xp 00000000 fd:00 2505812                    /usr/lib64/libgcc_s-6.3.1-20161221.so.1
7f1b4528f000-7f1b4548e000 ---p 00016000 fd:00 2505812                    /usr/lib64/libgcc_s-6.3.1-20161221.so.1
7f1b4548e000-7f1b4548f000 r--p 00015000 fd:00 2505812                    /usr/lib64/libgcc_s-6.3.1-20161221.so.1
7f1b4548f000-7f1b45490000 rw-p 00016000 fd:00 2505812                    /usr/lib64/libgcc_s-6.3.1-20161221.so.1
7f1b45490000-7f1b4564d000 r-xp 00000000 fd:00 2503196                    /usr/lib64/libc-2.24.so
7f1b4564d000-7f1b4584c000 ---p 001bd000 fd:00 2503196                    /usr/lib64/libc-2.24.so
7f1b4584c000-7f1b45850000 r--p 001bc000 fd:00 2503196                    /usr/lib64/libc-2.24.so
7f1b45850000-7f1b45852000 rw-p 001c0000 fd:00 2503196                    /usr/lib64/libc-2.24.so
7f1b45852000-7f1b45856000 rw-p 00000000 00:00 0
7f1b45856000-7f1b4587b000 r-xp 00000000 fd:00 2500300                    /usr/lib64/ld-2.24.so
7f1b45a47000-7f1b45a49000 rw-p 00000000 00:00 0
7f1b45a78000-7f1b45a7b000 rw-p 00000000 00:00 0
7f1b45a7b000-7f1b45a7c000 r--p 00025000 fd:00 2500300                    /usr/lib64/ld-2.24.so
7f1b45a7c000-7f1b45a7d000 rw-p 00026000 fd:00 2500300                    /usr/lib64/ld-2.24.so
7f1b45a7d000-7f1b45a7e000 rw-p 00000000 00:00 0
7ffd73d54000-7ffd73d75000 rw-p 00000000 00:00 0                          [stack]
7ffd73dbb000-7ffd73dbd000 r--p 00000000 00:00 0                          [vvar]
7ffd73dbd000-7ffd73dbf000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
zsh: abort (core dumped)  ./a.out 1111111111111111111

现在,我们对比下有无_FORTIFY_SOURCE时,程序会有什么不同。

以下,在不启用_FORTIFY_SOURCE时,main函数的汇编代码:

0000000000400546 <main>:
  400546:	55                   	push   %rbp
  400547:	48 89 e5             	mov    %rsp,%rbp
  40054a:	48 83 ec 20          	sub    $0x20,%rsp
  40054e:	89 7d ec             	mov    %edi,-0x14(%rbp)
  400551:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  400555:	48 8b 45 e0          	mov    -0x20(%rbp),%rax
  400559:	48 83 c0 08          	add    $0x8,%rax
  40055d:	48 8b 10             	mov    (%rax),%rdx
  400560:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  400564:	48 89 d6             	mov    %rdx,%rsi
  400567:	48 89 c7             	mov    %rax,%rdi
  40056a:	e8 c1 fe ff ff       	callq  400430 <strcpy@plt>
  40056f:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  400573:	48 89 c7             	mov    %rax,%rdi
  400576:	e8 c5 fe ff ff       	callq  400440 <puts@plt>
  40057b:	b8 00 00 00 00       	mov    $0x0,%eax
  400580:	c9                   	leaveq
  400581:	c3                   	retq
  400582:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  400589:	00 00 00
  40058c:	0f 1f 40 00          	nopl   0x0(%rax)

可见,程序是直接调用strcpy的:

400564:	48 89 d6             	mov    %rdx,%rsi
400567:	48 89 c7             	mov    %rax,%rdi
40056a:	e8 c1 fe ff ff       	callq  400430 <strcpy@plt>

启用_FORTIFY_SOURCE重新编译,再看main函数的汇编:

0000000000400566 <main>:
  400566:	48 83 ec 18          	sub    $0x18,%rsp
  40056a:	48 8b 76 08          	mov    0x8(%rsi),%rsi
  40056e:	ba 05 00 00 00       	mov    $0x5,%edx
  400573:	48 89 e7             	mov    %rsp,%rdi
  400576:	e8 e5 fe ff ff       	callq  400460 <__strcpy_chk@plt>
  40057b:	48 89 e7             	mov    %rsp,%rdi
  40057e:	e8 cd fe ff ff       	callq  400450 <puts@plt>
  400583:	b8 00 00 00 00       	mov    $0x0,%eax
  400588:	48 83 c4 18          	add    $0x18,%rsp
  40058c:	c3                   	retq
  40058d:	0f 1f 00             	nopl   (%rax)

现在,并没有直接调用strcpy,而是调用了__strcpy_chk函数:

400576:	e8 e5 fe ff ff       	callq  400460 <__strcpy_chk@plt>

4.1 在GCC中是如何工作的

当定义_FORTIFY_SOURCE=1宏时,GCC在内部重新定义了一个新的宏__SSP_FORTIFY_LEVEL,实现如下:

/* File: libssp/ssp/ssp.h.in */

#if _FORTIFY_SOURCE > 0 && __OPTIMIZE__ > 0 \
    && defined __GNUC__ \
    && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)) \
    && !defined __cplusplus
# if _FORTIFY_SOURCE == 1
#  define __SSP_FORTIFY_LEVEL 1
# elif _FORTIFY_SOURCE > 1
#  define __SSP_FORTIFY_LEVEL 2
# endif
#endif

如果__SSP_FORTIFY_LEVEL的值大于0,GCC会取消会造成缓存溢出的函数的定义,并且重新定义带安全检查的。例,对memcpy的重定义:

/* File: libssp/ssp/string.h */
#if __SSP_FORTIFY_LEVEL > 0

/* 取消对这些危险函数的定义 */
#undef memcpy
#undef memmove
#undef memset
#undef strcat
#undef strcpy
#undef strncat
#undef strncpy
#undef mempcpy
#undef stpcpy
#undef bcopy
#undef bzero

/* 重新实现带内存检查的memcpy函数 */
#define memcpy(dest, src, len) \
  ((__ssp_bos0 (dest) != (size_t) -1)                                 \
   ? __builtin___memcpy_chk (dest, src, len, __ssp_bos0 (dest))               \
   : __memcpy_ichk (dest, src, len))
static inline __attribute__((__always_inline__)) void *
__memcpy_ichk (void *__restrict__ __dest, const void *__restrict__ __src,
	       size_t __len)
{
  return __builtin___memcpy_chk (__dest, __src, __len, __ssp_bos0 (__dest));
}

强烈建议编译代码时开启_FORTIFY_SOURCE。