首页 > 解决方案 > 项目中包含程序集文件时,来自 mmap 的意外 exec 权限

问题描述

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

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

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

所以这是我奇怪的例子,我正在使用 Ubunbtu 19.04 和默认 gcc。

如果您使用 ASM 文件(为空)编译目标可执行文件,则mmap返回一个可读且可执行的区域,如果您不使用该文件进行构建,则它的行为正确。/proc/self/maps请参阅我在示例中嵌入的输出。

例子.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 

标签: clinuxassemblymmapdep

解决方案


Linux 有一个称为的执行域READ_IMPLIES_EXEC,它会导致所有分配的页面PROT_READ也被分配PROT_EXEC。较旧的 Linux 内核曾经将其用于使用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文件一起编译,您会看到它已启用,但没有它,它将被禁用。它的初始值来自二进制文件中的 ELF 元信息。做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

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

(如果您依赖的是文件顶部的默认部分,请不要忘记切换到该指令.text.data之后的另一个部分。).section.s.text

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

与NASM类似,section带有正确标志的指令指定不可执行的堆栈。


5.4 和 5.8 之间的现代 Linux 内核改变了 ELF 程序加载器的行为。对于 x86-64,不再打开任何东西READ_IMPLIES_EXEC。最多(通过GNU_STACK添加RWE ld),您将获得可执行的堆栈本身,而不是每个可读页面。(这个答案涵盖了 5.8 中的最后一个更改,但在此之前必须有其他更改,因为该问题显示.data在 x86-64 Linux 5.4 上成功执行代码)

exec-all( READ_IMPLIES_EXEC) 仅适用于链接器根本没有添加标头条目的旧版 32 位可执行文件GNU_STACK。但如此处所示ld,即使输入.o文件缺少注释,modern 总是使用一种设置或另一种设置添加它。

您仍然应该使用本.note节来表示正常程序中的不可执行堆栈。但是,如果您希望在或遵循一些用于测试 shellcode.data的旧教程中测试自修改代码,那么这不是现代内核的选项。


推荐阅读