ELF文件可执行栈的深入分析
测试环境:
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
这个函数的功能是判断是否需要可执行栈,为了更好的说明这个问题,我们直接去看这个函数的内容。这个函数具体实现和对应的框架有很大关系,我们来解析一下,x86
和ia64
的具体实现。
/*
* 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_stack
和EXSTACK_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_stack
是EXSTACK_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