测试环境:

OS Version : Debian Jessie(stable)

GCC Version :  gcc (Debian 4.9.2-10) 4.9.2

ld Version : GNU ld (GNU Binutils for Debian) 2.25

测试代码

#include <unistd.h>

char shellcode[] = 
"\x48\x31\xd2\x48\x31\xf6\x48\xbf"
"\x2f\x62\x69\x6e\x2f\x73\x68\x11"
"\x48\xc1\xe7\x08\x48\xc1\xef\x08"
"\x57\x48\x89\xe7\x48\xb8\x3b\x11"
"\x11\x11\x11\x11\x11\x11\x48\xc1"
"\xe0\x38\x48\xc1\xe8\x38\x0f\x05";

int main(int argc, char ** argv) {
        void (*fp)();
        fp = (void(*)())shellcode;
        (void)(*fp)();

        return 0;
}

原理分析

GCC 编译选项中,开始/关闭可执行栈的选项是 “-z execstack/noexecstack”,默认情况下 GCC 是关闭可执行栈的。我们来深层次解析一下这个选项之后的内容:

$ gcc -Wall -Wextra -ggdb -z execstack -o verifyexec verifyexec.c
$ readelf -l verifyexec

Elf file type is EXEC (Executable file)
Entry point 0x4003c0
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001c0 0x00000000000001c0  R E    8
  INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000000068c 0x000000000000068c  R E    200000
  LOAD           0x0000000000000690 0x0000000000600690 0x0000000000600690
                 0x0000000000000281 0x0000000000000288  RW     200000
  DYNAMIC        0x00000000000006a8 0x00000000006006a8 0x00000000006006a8
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000564 0x0000000000400564 0x0000000000400564
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK    0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  **RWE**    10
...... 

$ gcc -Wall -Wextra -ggdb -z noexecstack -o verifyexec verifyexec.c
$ readelf -l verifyexec
Elf file type is EXEC (Executable file)
Entry point 0x4003c0
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001c0 0x00000000000001c0  R E    8
  INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000000068c 0x000000000000068c  R E    200000
  LOAD           0x0000000000000690 0x0000000000600690 0x0000000000600690
                 0x0000000000000281 0x0000000000000288  RW     200000
  DYNAMIC        0x00000000000006a8 0x00000000006006a8 0x00000000006006a8
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000564 0x0000000000400564 0x0000000000400564
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK    0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  **RW**     10

从以上部分可以看出来,选项 “-z execstack/noexecstack” 真正影响到的地方其实是GNU_STACK这个 segment 的 flags。

为了真正理解GNU_STACK段的真正含义,我们将深入 linux 内核中的load_elf_binary这个ELF文件的加载函数。

在这里为了方便大家更加明白加载函数的内容,我需要先说明一下ELF文件的 program header 的结构体.

typedef struct elf64_phdr {
    Elf64_Word p_type;
    Elf64_Word p_flags;
    Elf64_Off  p_offset;           /* Segment file offset */
    Elf64_Addr p_vaddr;           /* Segment virtual address */
    Elf64_Addr p_paddr;           /* Segment physical address */
    Elf64_Xword p_filesz;         /* Segment size in file */
    Elf64_Xword p_memsz;          /* Segment size in memory */
    Elf64_Xword p_align;          /* Segment alignment, file & memory */
} Elf64_Phdr;

p_type : 表示这个 program header 的类型,这里可以上面参见readelf -l中 “Type”。

p_flags : 表示这个 program header 的读写执行的标志,这里可以参见readelf -l中 “Flags”,这个是我们这次研究的重点字段。

现在让我们来分析一下GNU_STACK这个 segment 的作用,仔细看如下代码:

/* Stack area protections */
#define EXSTACK_DEFAULT   0     /* Whatever the arch defaults to */
#define EXSTACK_DISABLE_X 1     /* Disable executable stacks */
#define EXSTACK_ENABLE_X  2     /* Enable executable stacks */

int executable_stack = EXSTACK_DEFAULT;
......
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
    switch (elf_ppnt->p_type) {
    case PT_GNU_STACK:
        if (elf_ppnt->p_flags & PF_X)
            executable_stack = EXSTACK_ENABLE_X;
        else
            executable_stack = EXSTACK_DISABLE_X;
        break;
......

上面的for循环是遍历每一个 segment,当发现存在PT_GNU_STACK,也就是GNU_STACK segment 的存在;如果存在这个这个段的话,看这个段的 flags 是否有可执行权限,来设置对应的值。这里可能会出现一个问题,如何没有这个GNU_STACK segment,会出现什么样的情况?

下面的代码是判断栈可不可执行的关键位置:

if (elf_read_implies_exec(loc->elf_ex, executable_stack))
    current->personality |= READ_IMPLIES_EXEC;

很明显,elf_read_implies_exec这个函数的功能是判断是否需要可执行栈,为了更好的说明这个问题,我们直接去看这个函数的内容。这个函数具体实现和对应的框架有很大关系,我们来解析一下,x86ia64的具体实现。

/*
 * An executable for which elf_read_implies_exec() returns TRUE will
 * have the READ_IMPLIES_EXEC personality flag set automatically.
 */
#define elf_read_implies_exec(ex, executable_stack)     \
    (executable_stack != EXSTACK_DISABLE_X)

x86的实现只是简单的比较了一下,executable_stackEXSTACK_DISABLE_X,如果不相等,函数返回true(非0);相等的话,函数返回false(0)。

#define elf_read_implies_exec(ex, executable_stack)     \
    ((executable_stack!=EXSTACK_DISABLE_X) &&           \
    ((ex).e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK) != 0)

ia64的实现除了进行executable_stack变量进行检查,还加上了对于 elf header 属性的检查。

对于两者,我们可以发现它们只是检查 (executable_stack!=EXSTACK_DISABLE_X),如果上面那个问题成立的话,这里就会出现一个问题, executable_stackEXSTACK_DEFAULT,不是EXSTACK_DISABLE_X,所以按照逻辑判断它应该是可执行的,但是 实际上它的 flag 是不可执行。说了这么多,其实就是一句话,如果没有GNU_STACK这个 segment,the process of judging executable property is not right.

最后让我们来说一下READ_IMPLIES_EXEC,为什么有了这个PERSONALITY之后,栈就可以执行了。

READ_IMPLIES_EXEC : If this bit is set, then PROT_READ will also add the PROT_EXEC bit. This constant, READ_IMPLIES_EXEC, is checked in some memory related functions such as in mmap.c, mprotect.c and nommu.c (all in linux-2.6.32.61/mm/). For instance, when creating the VMAs (Virtual Memory Areas) by the do_mmap_pgoff() function in mmap.c, it verifies the personality so it can add the PROT_EXEC (execution allowed) to the memory segments.

Then, do_mmap_pgoff function in mmap.c

static inline unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot, unsigned long flags,
    unsigned long pgoff, unsigned long *populate)
{
    return do_mmap(file, addr, len, prot, flags, 0, pgoff, populate);
}

In do_mmap, it process READ_IMPLIES_EXEC in the following code:

/*
 * Does the application expect PROT_READ to imply PROT_EXEC?
 *
 * (the exception is when the underlying filesystem is noexec
 * mounted, in which case we dont add PROT_EXEC.)
 */
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
    if (!(file && path_noexec(&file->f_path)))
        prot |= PROT_EXEC;

参考:

[1] http://blog.ioactive.com/2013/11/a-short-tale-about-executablestack-in.html