13

我想要一个简单的 C 方法能够在 Linux 64 位机器上运行十六进制字节码。这是我拥有的 C 程序:

char code[] = "\x48\x31\xc0";
#include <stdio.h>
int main(int argc, char **argv)
{
        int (*func) ();
        func = (int (*)()) code;
        (int)(*func)();
        printf("%s\n","DONE");
}

我试图运行的代码("\x48\x31\xc0")是我通过编写这个简单的汇编程序获得的(它不应该真正做任何事情)

.text
.globl _start
_start:
        xorq %rax, %rax

然后对其进行编译和 objdump-ing 以获得字节码。

但是,当我运行我的 C 程序时,我遇到了分段错误。有任何想法吗?

4

7 回答 7

24

机器代码必须在可执行页面中。您char code[]在读+写数据部分,没有 exec 权限,因此无法从那里执行代码。

这是一个分配可执行页面的简单示例mmap

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

int main ()
{
  char code[] = {
    0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]
    0xC3                        //  ret
  };

  int (*sum) (int, int) = NULL;

  // allocate executable buffer                                             
  sum = mmap (0, sizeof(code), PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

  // copy code to buffer
  memcpy (sum, code, sizeof(code));
  // doesn't actually flush cache on x86, but ensure memcpy isn't
  // optimized away as a dead store.
  __builtin___clear_cache (sum, sum + sizeof(sum));  // GNU C

  // run code
  int a = 2;
  int b = 3;
  int c = sum (a, b);

  printf ("%d + %d = %d\n", a, b, c);
}

有关详细信息,请参阅此问题的另一个答案__builtin___clear_cache

于 2012-04-01T12:43:56.347 回答
9

在最近的 Linux 内核版本(有时在 5.4 之前)之前,您可以简单地使用gcc -z execstack- 进行编译,这将使所有页面都可执行,包括只读数据 ( .rodata) 和读写数据 ( .data) char code[] = "..."

现在-z execstack仅适用于实际堆栈,因此它目前仅适用于非常量本地数组。char code[] = ...搬入main


有关内核更改,请参阅针对 `.data` 部分的 Linux 默认行为,以及在项目中包含汇编文件时来自 mmap 的意外 exec 权限以了解旧行为:READ_IMPLIES_EXEC为该程序启用 Linux 进程。(在 Linux 5.4 中,Q&A 显示你只会得到READ_IMPLIES_EXEC一个缺失的PT_GNU_STACK,就像一个非常旧的二进制文件;现代 GCC-z execstack会在可执行文件中设置PT_GNU_STACK = RWX元数据,Linux 5.4 会处理它,因为它只使堆栈本身可执行。在那之前的某个时候,PT_GNU_STACK = RWX确实导致了READ_IMPLIES_EXEC。)

另一种选择是在运行时进行系统调用以复制到可执行页面,或更改其所在页面的权限。这仍然比使用本地数组让 GCC 将代码复制到可执行堆栈内存中更复杂。

(我不知道READ_IMPLIES_EXEC在现代内核下是否有一种简单的方法可以启用。ELF 二进制文件中根本没有 GNU-stack 属性可以用于 32 位代码,但不能用于 64 位代码。)

另一个选择是__attribute__((section(".text"))) const char code[] = ...;
工作示例:https ://godbolt.org/z/draGeh 。
如果您需要数组是可写的,例如对于在字符串中插入一些零的 shellcode,您可以使用ld -N. 但可能最好使用 -z execstack 和本地数组。


问题中有两个问题:

  • 页面上的 exec 权限,因为您使用了一个将进入 noexec 读+写.data部分的数组。
  • 您的机器代码不会以ret指令结束,因此即使它确实运行了,执行也会落入内存中的下一个内容,而不是返回。

顺便说一句,REX 前缀是完全多余的。 "\x31\xc0" xor eax,eax 具有与 完全相同的效果xor rax,rax


您需要包含机器代码的页面具有执行权限。与传统的 386 页表不同,x86-64 页表有一个单独的位用于执行与读取权限分开。

让静态数组进入 read+exec 内存的最简单方法是使用gcc -z execstack. (用于使堆栈其他部分可执行,现在只有堆栈)。

直到最近(2018 年或 2019 年),标准工具链 (binutils ld) 将 section.rodata放入与 相同的 ELF 段中.text,因此它们都具有 read+exec 权限。因此 usingconst char code[] = "...";足以将手动指定的字节作为数据执行,而无需 execstack。

但在我的 Arch Linux 系统上GNU ld (GNU Binutils) 2.31.1,情况不再如此。 readelf -a显示该.rodata部分进入了一个带有.eh_frame_hdrand的 ELF 段.eh_frame,并且它只有读取权限。 .text使用 Read + Exec.data进入一个段,并使用 Read + Write 进入一个段(以及.gotand .got.plt)。(ELF文件格式中section和segment有什么区别

我认为这种变化是通过在可执行页面中不包含只读数据来使 ROP 和 Spectre 攻击更加困难,其中有用字节序列可以用作以字节结尾的“小工具”retjmp reg指令。

// TODO: use char code[] = {...} inside main, with -z execstack, for current Linux

// Broken on recent Linux, used to work without execstack.
#include <stdio.h>

// can be non-const if you use gcc -z execstack.  static is also optional
static const char code[] = {
  0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]       // retval = a+b;                    
  0xC3                        //  ret                                         
};

static const char ret0_code[] = "\x31\xc0\xc3";   // xor eax,eax ;  ret
                     // the compiler will append a 0 byte to terminate the C string,
                     // but that's fine.  It's after the ret.

int main () {
  // void* cast is easier to type than a cast to function pointer,
  // and in C can be assigned to any other pointer type.  (not C++)

  int (*sum) (int, int) = (void*)code;
  int (*ret0)(void) = (void*)ret0_code;

  // run code                                                                   
  int c = sum (2, 3);
  return ret0();
}

在较旧的 Linux 系统上:(gcc -O3 shellcode.c && ./a.out由于const在全局/静态数组上有效)

在 5.5 之前(左右)的 Linux 上(gcc -O3 -z execstack shellcode.c && ./a.out无论-zexecstack您的机器代码存储在哪里都可以工作)。有趣的事实:gcc 允许-zexecstack没有空格,但 clang 只接受clang -z execstack.

这些也适用于 Windows,其中只读数据进入.rdata而不是.rodata.

编译器生成的main看起来像这样(来自objdump -drwC -Mintel)。 你可以在里面运行它并gdb设置断点coderet0_code

(I actually used   gcc -no-pie -O3 -zexecstack shellcode.c  hence the addresses near 401000
0000000000401020 <main>:
  401020:       48 83 ec 08             sub    rsp,0x8           # stack aligned by 16 before a call
  401024:       be 03 00 00 00          mov    esi,0x3
  401029:       bf 02 00 00 00          mov    edi,0x2           # 2 args
  40102e:       e8 d5 0f 00 00          call   402008 <code>     # note the target address in the next page
  401033:       48 83 c4 08             add    rsp,0x8
  401037:       e9 c8 0f 00 00          jmp    402004 <ret0_code>    # optimized tailcall

或者使用系统调用修改页面权限

gcc -zexecstack您可以不使用编译,而是使用mmap(PROT_EXEC)分配新的可执行页面,或mprotect(PROT_EXEC)将现有页面更改为可执行页面。(包括保存静态数据的页面。)当然,您通常还需要至少PROT_READ,有时也需要PROT_WRITE

在静态数组上使用mprotect意味着您仍在从已知位置执行代码,可能更容易在其上设置断点。

在 Windows 上,您可以使用 VirtualAlloc 或 VirtualProtect。

告诉编译器数据作为代码执行

通常,像 GCC 这样的编译器假定数据和代码是分开的。这就像基于类型的严格别名,但即使 usingchar*也不能很好地定义存储到缓冲区中,然后将该缓冲区作为函数指针调用。

在 GNU C 中,您还需要使用__builtin___clear_cache(buf, buf + len)after 将机器代码字节写入缓冲区,因为优化器不会将取消引用函数指针视为从该地址读取字节。如果编译器证明存储没有被任何东西读取为数据,则死存储消除可以将机器代码字节的存储删除到缓冲区中。 https://codegolf.stackexchange.com/questions/160100/the-repetitive-byte-counter/160236#160236https://godbolt.org/g/pGXn3B有一个例子 gcc 确实做了这个优化,因为 gcc “知道” malloc

(在 I-cache 与 D-cache 不一致的非 x86 架构上,它实际上会进行任何必要的缓存同步。在 x86 上,它纯粹是一个编译时优化阻塞器,不会扩展到任何指令本身。)

回复:带有三个下划线的奇怪名称:这是通常的__builtin_name模式,但是name__clear_cache.

我对@AntoineMathys 回答的编辑添加了这个。

在实践中,GCC/clang 并不“了解”mmap(MAP_ANONYMOUS)他们了解malloc. 因此,在实践中,优化器会假设缓冲区中的 memcpy 可能会被非内联函数调用通过函数指针读取为数据,即使没有__builtin___clear_cache(). (除非您将函数类型声明为__attribute__((const))。)

在 x86 上,I-cache 与数据缓存一致,在调用之前以 asm 形式进行存储足以保证正确性。在其他 ISA 上,__builtin___clear_cache()实际上会发出特殊指令并确保正确的编译时顺序。

将代码复制到缓冲区时包含它是一种很好的做法,因为它不会降低性能,并且可以阻止假设的未来编译器破坏您的代码。(例如,如果他们确实明白这mmap(MAP_ANONYMOUS)会提供新分配的匿名内存,而其他任何东西都没有指针指向,就像 malloc 一样。)


__attribute__((const))使用当前的 GCC,我能够通过告诉优化器sum()是一个纯函数(仅读取其参数,而不是全局内存)来促使 GCC 真正进行我们不想要的优化。然后 GCC 知道sum()无法读取memcpyas 数据的结果。

在调用后将另一个存储在同一个缓冲区中,GCC 会在调用后将memcpy死存储消除到第二个存储中。这导致在第一次调用之前没有存储,因此它执行字节,segfaulting。00 00 add [rax], al

// demo of a problem on x86 when not using __builtin___clear_cache
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main ()
{
  char code[] = {
    0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]
    0xC3                        //  ret                                         
  };

  __attribute__((const)) int (*sum) (int, int) = NULL;

  // copy code to executable buffer                                             
  sum = mmap (0,sizeof(code),PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_ANON,-1,0);
  memcpy (sum, code, sizeof(code));
  //__builtin___clear_cache(sum, sum + sizeof(code));

  int c = sum (2, 3);
  //printf ("%d + %d = %d\n", a, b, c);

  memcpy(sum, (char[]){0x31, 0xc0, 0xc3, 0}, 4);  // xor-zero eax, ret, padding for a dword store
  //__builtin___clear_cache(sum, sum + 4);
  return sum(2,3);
}

使用GCC9.2 -O3在 Godbolt 编译器资源管理器上编译

main:
        push    rbx
        xor     r9d, r9d
        mov     r8d, -1
        mov     ecx, 34
        mov     edx, 7
        mov     esi, 4
        xor     edi, edi
        sub     rsp, 16
        call    mmap
        mov     esi, 3
        mov     edi, 2
        mov     rbx, rax
        call    rax                  # call before store
        mov     DWORD PTR [rbx], 12828721    #  0xC3C031 = xor-zero eax, ret
        add     rsp, 16
        pop     rbx
        ret                      # no 2nd call, CSEd away because const and same args

传递不同的参数会得到另一个call reg,但即使有__builtin___clear_cache两个sum(2,3)调用也可以 CSE__attribute__((const))不尊重对函数机器代码的更改。不要这样做。不过,如果您要 JIT 函数一次然后多次调用它是安全的。

取消注释第一个__clear_cache结果

        mov     DWORD PTR [rax], -1019804531    # lea; ret
        call    rax
        mov     DWORD PTR [rbx], 12828721       # xor-zero; ret
       ... still CSE and use the RAX return value

第一家商店在那里是因为__clear_cachesum(2,3)电话。(删除第一个sum(2,3)调用确实可以消除死存储__clear_cache。)

第二个存储在那里,因为假定返回的缓冲区的副作用mmap很重要,这就是最终的值main

Godbolt./a.out运行程序的选项似乎仍然总是失败(退出状态为 255);也许它沙箱 JITing?它可以在我的桌面上运行__clear_cache并且没有崩溃。


mprotect在包含现有 C 变量的页面上。

您还可以授予单个现有页面读+写+执行权限。这是编译的替代方法-z execstack

您不需要__clear_cache保存只读 C 变量的页面,因为没有要优化的存储。您仍然需要它来初始化本地缓冲区(在堆栈上)。否则 GCC 将优化掉非内联函数调用肯定没有指针的私有缓冲区的初始化程序。(逃逸分析)。它不考虑缓冲区可能保存函数的机器代码的可能性,除非您通过__builtin___clear_cache.

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

// can be non-const if you want, we're using mprotect
static const char code[] = {
  0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]       // retval = a+b;                    
  0xC3                        //  ret                                         
};

static const char ret0_code[] = "\x31\xc0\xc3";

int main () {
  // void* cast is easier to type than a cast to function pointer,
  // and in C can be assigned to any other pointer type.  (not C++)
  int (*sum) (int, int) = (void*)code;
  int (*ret0)(void) = (void*)ret0_code;

   // hard-coding x86's 4k page size for simplicity.
   // also assume that `code` doesn't span a page boundary and that ret0_code is in the same page.
  uintptr_t page = (uintptr_t)code & -4095ULL;                  // round down
  mprotect((void*)page, 4096, PROT_READ|PROT_EXEC|PROT_WRITE);  // +write in case the page holds any writeable C vars that would crash later code.

  // run code                                                                   
  int c = sum (2, 3);
  return ret0();
}

PROT_READ|PROT_EXEC|PROT_WRITE在这个例子中使用过,所以不管你的变量在哪里,它都可以工作。如果它是堆栈上的本地并且您遗漏了PROT_WRITEcall则在尝试推送返回地址时使堆栈只读后会失败。

此外,还PROT_WRITE可以让您测试自我修改的 shellcode,例如将零编辑到它自己的机器代码中,或者它避免的其他字节。

$ gcc -O3 shellcode.c           # without -z execstack
$ ./a.out 
$ echo $?
0
$ strace ./a.out
...
mprotect(0x55605aa3f000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

mprotect如果我将. _ _ld.text

如果我做了类似的事情ret0_code[2] = 0xc3;,我需要__builtin___clear_cache(ret0_code+2, ret0_code+2)在那之后确保商店没有被优化掉,但是如果我不修改静态数组,那么之后就不需要它了mprotectmmap在+或手动存储之后需要它memcpy,因为我们想要执行已经用 C 写入的字节(带有memcpy)。

于 2019-04-28T19:20:04.123 回答
5

您需要通过特殊的编译器指令将程序集包含在内,以便它正确地结束在代码段中。请参阅本指南,例如:http ://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

于 2012-03-31T23:57:38.620 回答
5

您的机器代码可能没问题,但您的 CPU 对象。

现代 CPU 按段管理内存。在正常操作中,操作系统将新程序加载到程序文本段中,并在数据段中建立堆栈。操作系统告诉 CPU 永远不要在数据段中运行代码。您的代码位于code[], 数据段中。因此出现了段错误。

于 2012-03-31T23:57:54.737 回答
2

这将需要一些努力。

您的code变量存储在.data可执行文件的部分中:

$ readelf -p .data exploit

String dump of section '.data':
  [    10]  H1À

H1À是你的变量的值。

.data部分不可执行:

$ readelf -S exploit
There are 30 section headers, starting at offset 0x1150:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
[...]
  [24] .data             PROGBITS         0000000000601010  00001010
       0000000000000014  0000000000000000  WA       0     0     8

我熟悉的所有 64 位处理器都在页表中原生支持不可执行的页面。大多数较新的 32 位处理器(支持 PAE 的处理器)在其页表中为操作系统提供了足够的额外空间来模拟硬件不可执行的页面。您需要运行古老的操作系统或古老的处理器才能获得.data标记为可执行的部分。

因为这些只是可执行文件中的标志,所以您应该能够X通过其他一些机制设置标志,但我不知道该怎么做。而且您的操作系统甚至可能不允许您拥有既可写又可执行的页面。

于 2012-04-01T00:03:22.333 回答
1

您可能需要在调用它之前设置页面可执行文件。在 MS-Windows 上,请参阅 VirtualProtect -function。

网址:http: //msdn.microsoft.com/en-us/library/windows/desktop/aa366898%28v=vs.85%29.aspx

于 2012-12-05T12:06:05.693 回答
-1

抱歉,我无法遵循上述复杂的示例。因此,我创建了一个优雅的解决方案,用于从 C 执行十六进制代码。基本上,您可以使用 asm 和 .word 关键字将指令以十六进制格式放置。请参见下面的示例:

asm volatile(".rept 1024\n"
             CNOP
           ".endr\n");

其中 CNOP 定义如下:#define ".word 0x00010001 \n"

基本上,c.nop我当前的汇编程序不支持指令。所以,我用正确的语法定义CNOP为十六进制等价物,c.nop并在我知道的 asm 中使用。 .rept <NUM> .endr基本上,将指令重复 NUM 次。

此解决方案有效且经过验证。

于 2021-07-29T13:27:25.123 回答