gcc execstack 标志究竟允许什么情况以及它是如何强制执行的?

Exactly what cases does the gcc execstack flag allow and how does it enforce it?

我这里有一些示例代码,我用它来了解初学者 CTF 的一些 C 行为:

// example.c

#include <stdio.h>


void main() {
        void (*print)();

        print = getenv("EGG");
        print();
}

编译:gcc -z execstack -g -m32 -o example example.c

用法:EGG=$(echo -ne '\x90\xc3) ./example

如果我使用 execstack 标志编译代码,程序将执行我在上面注入的操作码。没有这个标志,程序将由于分段错误而崩溃。

这是为什么?是因为 getenv 将实际操作码存储在堆栈上,并且 execstack 标志允许跳转到堆栈吗?或者 getenv 是否将指针压入堆栈,以及关于内存的哪些部分是可执行的还有一些其他规则?我阅读了联机帮助页,但我无法确切地了解规则是什么以及它们是如何执行的。

另一个问题是我认为我也确实缺少一个在调试时可视化内存的好工具,所以很难弄清楚这一点。任何建议将不胜感激。

getenv 不会 store 环境变量的值在堆栈上。它在进程启动时已经在堆栈上,并且getenv获得指向它的指针。

参见 i386 System V ABI 对进程启动时 argv[] 和 envp[] 所在位置的描述:上面 [esp].

_start 在调用 main 之前不会复制它们,只是计算指向它们的指针以作为参数传递给 main。 (链接到 https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI 处的最新版本,其中维护了官方当前版本。)


您的代码正在将指向堆栈内存(包含环境变量的值)的指针转换为函数指针并通过它进行调用。查看 compiler-generated asm(例如 https://godbolt.org/):它将类似于 call getenv / call eax.

-zexecstack 在您的内核版本中 1 使您的所有页面都可执行,而不仅仅是堆栈 。它还适用于 .data.bss.rodata 部分,以及使用 malloc / new.

分配的内存

GNU/Linux 上的确切机制是一个“read-implies-exec”process-wide 标志,它会影响所有未来的分配,包括手动使用 mmap 有关 GNU_STACK ELF header 内容的更多信息,请参阅 Unexpected exec permission from mmap when assembly files included in the project

脚注 1: Linux 在 5.4 左右之后只使堆栈本身可执行,而不是 READ_IMPLIES_EXEC: Linux default behavior of executable .data section changed between 5.4 and 5.9?

有趣的事实:获取访问其 parents 局部变量的嵌套函数的地址会使 gcc 启用 -zexecstack。它将可执行“蹦床”的代码存储到堆栈上,将“静态链”指针传递给实际的嵌套函数,允许它引用其 parent 的 stack-frame.


如果您想在没有 -zexecstack 的情况下将数据作为代码执行,您可以在包含该环境变量的页面上使用 mprotect(PROT_EXEC|PROT_READ|PROT_WRITE)。 (它是您的堆栈的一部分,因此您不应删除写权限;例如,它可能与 main 的堆栈框架位于同一页面中。)


相关:

使用 GNU/Linux ld 来自 binutils 在 2018 年底左右之前,.rodata 部分链接到与 .text 部分相同的 ELF 段,因此 const char code[] = {0xc3} 或字符串文字是可执行的。

当前 ld 提供了 .rodata 自己的段,该段无需执行即可映射读取,因此不再可能在 read-only 数据中找到 ROP / Spectre“小工具”,除非您使用 -zexecstack。即使这样也不适用于当前的内核; char code[] = ...; 作为函数内部的局部变量会将数据放在实际可执行的堆栈中。有关详细信息,请参阅 How to get c code to execute hex machine code?