当程序集文件包含在项目中时,来自 mmap 的意外执行权限

Unexpected exec permission from mmap when assembly files included in the project

我正在用这个把头撞到墙上。

在我的项目中,当我使用 mmap 分配内存时,映射 (/proc/self/maps) 显示它是一个可读和可执行的区域 尽管 我只请求可读内存。

在查看 strace(看起来不错)和其他调试之后,我能够确定唯一似乎可以避免这个奇怪问题的方法:从项目中删除汇编文件并只留下纯 C。(什么? !)

这是我的奇怪示例,我正在使用 Ubuntu 19.04 和默认 gcc。

如果您使用 ASM 文件(它是空的)编译目标可执行文件,那么 mmap returns 一个可读和可执行的区域,如果您没有构建那么它会正常运行。请参阅我在示例中嵌入的 /proc/self/maps 的输出。

example.c

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main()
{
    void* p;
    p = mmap(NULL, 8192,PROT_READ,MAP_ANONYMOUS|MAP_PRIVATE,-1,0);

    {
        FILE *f;
        char line[512], s_search[17];
        snprintf(s_search,16,"%lx",(long)p);
        f = fopen("/proc/self/maps","r");
        while (fgets(line,512,f))
        {
            if (strstr(line,s_search)) fputs(line,stderr);
        }

        fclose(f);
    }

    return 0;
}

example.s: 是一个空文件!

输出

包含 ASM 的版本

VirtualBox:~/mechanics/build$ gcc example.c example.s -o example && ./example
7f78d6e08000-7f78d6e0a000 r-xp 00000000 00:00 0 

没有 ASM 包含版本

VirtualBox:~/mechanics/build$ gcc example.c -o example && ./example
7f1569296000-7f1569298000 r--p 00000000 00:00 0 

Linux 有一个 execution domain called READ_IMPLIES_EXEC, which causes all pages allocated with PROT_READ to also be given PROT_EXEC. Older Linux kernels used to use this 用于可执行文件,它使用 gcc -z execstack 的等价物。这个程序会告诉你它是否为它自己启用:

#include <stdio.h>
#include <sys/personality.h>

int main(void) {
    printf("Read-implies-exec is %s\n", personality(0xffffffff) & READ_IMPLIES_EXEC ? "true" : "false");
    return 0;
}

如果您将它与一个空的 .s 文件一起编译,您会看到它已启用,但如果没有,它将被禁用。这个comes from the ELF meta-information in your binary的初始值。做 readelf -Wl example。在没有空 .s 文件的情况下进行编译时,您会看到这一行:

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10

但是当你用它编译时这个:

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10

注意 RWE 而不仅仅是 RW。这样做的原因是链接器假定您的程序集文件需要 read-implies-exec,除非明确告知它们不需要,并且如果您的程序的任何部分需要 read-implies-exec,那么它会为您的整个程序启用. GCC 编译的汇编文件告诉它它不需要这个,用这一行(如果你用 -S 编译你会看到这个):

    .section        .note.GNU-stack,"",@progbits

默认部分权限不包括 exec。有关“标志”和@attributes 的含义,请参阅 .section documentation 的 ELF 部分。

(如果你的 .s 依赖于 .text 因为默认部分位于文件顶部。)

将该行放入 example.s(以及项目中的所有其他 .s 文件)。该 .note.GNU-stack 部分的存在将用于告诉链接器此 object 文件不依赖于可执行堆栈,因此链接器将使用 RW 而不是 RWEGNU_STACK 元数据上,然后您的程序将按预期工作。

类似地 for NASM,具有正确标志的 section 指令指定 non-executable 个堆栈。


5.4 和 5.8 之间的现代 Linux 内核改变了 ELF program-loader 的行为。对于 x86-64,不再打开 READ_IMPLIES_EXEC。最多(使用 ld 添加的 RWE GNU_STACK),您将获得堆栈本身是可执行的,而不是每个可读页面。 (This answer 涵盖了 5.8 中的最后一个更改,但在此之前肯定还有其他更改,因为该问题显示 .data on x86-64 Linux 5.4 中的代码成功执行)

exec-all (READ_IMPLIES_EXEC) 只发生在链接器根本没有添加 GNU_STACK header 条目的传统 32 位可执行文件中。但如此处所示,现代 ld 总是添加一个设置或另一个,即使输入 .o 文件缺少注释。

您仍应使用此 .note 部分在正常程序中向 non-executable 堆栈发送信号。但是,如果您希望在 .data 中测试 self-modifying 代码,或者遵循一些针对 testing shellcode 的旧教程,那不是现代内核的选项。

作为使用特定于 GNU 的节指令变体修改程序集文件的替代方法,您可以将 -Wa,--noexecstack 添加到命令行以构建程序集文件。例如,看看我是如何在 musl 的 configure:

中做到的

https://git.musl-libc.org/cgit/musl/commit/configure?id=adefe830dd376be386df5650a09c313c483adf1a

我相信至少某些带有集成汇编程序的 clang 版本可能需要将其作为 --noexecstack 传递(没有 -Wa),因此您的配置脚本可能应该检查两者并查看哪个被接受了。

您也可以在 link 时(在 LDFLAGS 中)使用 -Wl,-z,noexecstack 来获得相同的结果。这样做的缺点是,如果您的项目生成供其他软件使用的静态 (.a) 库文件,则它无济于事,因为您在使用时无法控制 link-time 选项通过其他程序。