【问题标题】:How to get c code to execute hex machine code?如何让c代码执行十六进制机器代码?
【发布时间】:2012-04-15 04:28:05
【问题描述】:

我想要一个简单的 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 程序时,我遇到了分段错误。有什么想法吗?

【问题讨论】:

  • 即使您的数据段是可执行的或者您没有启用 NX,您希望这会做什么?它执行一条指令,然后执行之后的指令(您无法控制),然后执行之后的指令,直到它到达不代表合法代码或触发段错误的代码的内存。
  • 您需要为ret 添加字节码,因为您执行的间接函数调用应该是call,它将返回地址推入堆栈。至少,这是我最有根据的猜测,我从未见过这样的事情。
  • 我希望它什么都不做,但我希望它能够运行而不会崩溃。
  • 你介意字符串末尾的 \0 吗?
  • 字符码[] = "\x48\x31\xc0\xc3\0";

标签: c assembly x86 x86-64 shellcode


【解决方案1】:

机器代码必须在可执行页面中。你的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的详细信息,请参阅another answer on this question

【讨论】:

  • 是的,ret 对于返回调用函数很重要。
  • 感谢您的帮助。我只想添加 objdump -d 可以获得可执行文件的字节码。
  • static const char code[] 通常链接到可执行文件的文本段,该文本段已经映射为只读 + 可执行文件。您实际上不需要复制它。 (不过,使它成为非const 将是一个问题;数据段并不总是可执行的。)这个答案与问题的重要部分是ret。另见Why does const int main = 195 result in a working program but without the const it ends in a segmentation fault?。 (195 = 0xC3 = ret)。
  • 此技术与机器代码的来源无关。为简单起见,我包含了一个在编译时已知的片段,但该概念适用于运行时生成的机器代码,这是主要用例。您很快就发表了评论,但我认为您错过了重点。
  • @MaximEgorushkin 和 Antoine:更新:这个答案仍然很好,但我的 static const code[] = ... 建议已经不够用了。当前 (2019) GNU Binutils ld 现在将 .rodata 链接到一个单独的段,该段在没有 exec 权限的情况下只读。不过,使用 gcc -z execstackmprotect() 仍然比使用 mmap+memcpy 更容易。我用完整的细节和工作示例添加了我自己的答案。
【解决方案2】:

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

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


请参阅Linux default behavior against `.data` section 了解内核更改,以及Unexpected exec permission from mmap when assembly files included in the project 了解旧行为:为该程序启用Linux 的READ_IMPLIES_EXEC 进程。 (在 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 read+write .data 部分。
  • 您的机器代码不会以 ret 指令结束,因此即使它确实运行了,执行也会落入内存中的下一个内容,而不是返回。

顺便说一句,REX 前缀是完全多余的。 "\x31\xc0"xor eax,eaxhas exactly the same effect as xor rax,rax.


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

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

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

但在我的带有GNU ld (GNU Binutils) 2.31.1 的 Arch Linux 系统上,情况不再如此。 readelf -a 表明.rodata 部分进入了带有.eh_frame_hdr.eh_frame 的ELF 段,并且它只有读取权限。 .text 进入带有 Read + Exec 的段,.data 进入带有 Read + Write 的段(以及 .got.got.plt)。 (What's the difference of section and segment in ELF file format)

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

// 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 &amp;&amp; ./a.out(因为 const 在全局/静态数组上有效)

在 5.5 之前的 Linux 上(或更高版本)gcc -O3 -z execstack shellcode.c &amp;&amp; ./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

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

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

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

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

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

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

在 GNU C 中,您还需要在将机器代码字节写入缓冲区后使用 __builtin___clear_cache(buf, buf + len),因为优化器不会将取消引用函数指针视为从该地址读取字节。如果编译器证明存储没有被任何东西读取为数据,则死存储消除可以将机器代码字节的存储删除到缓冲区中。 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 上,它纯粹是一个编译时优化阻止程序,不会扩展到任何指令本身。)

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

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

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

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

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


使用当前的 GCC,我能够通过使用 __attribute__((const)) 告诉优化器 sum() 是一个纯函数(仅读取它的参数,而不是全局内存)。然后 GCC 知道 sum() 无法将 memcpy 的结果作为数据读取。

在调用后将另一个memcpy 放入同一个缓冲区,GCC 在调用后 将死存储消除到第二个存储中。这导致在第一次调用之前没有存储,因此它执行 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 编译 on the Godbolt compiler explorer

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 编译的替代方法

您不需要在包含只读 C 变量的页面上使用 __clear_cache,因为没有要优化的存储。您仍然需要它来初始化本地缓冲区(在堆栈上)。否则 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_WRITE,则call 在尝试推送返回地址时使堆栈只读后会失败。

另外,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,它确实 使用最新版本的 GNU Binutils ld 出现段错误,它不再将只读常量数据放入与 .text 相同的 ELF 段中部分。

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

【讨论】:

  • @AntoineMathys:另一个原因是测试 shellcode 的 sn-p 以确保您已正确地将汇编程序转换为漏洞利用负载的一部分。如果你是 JITing,你不会有像 OP 的 "\x48\x31\xc0" 这样的 C 字符串,你只会有字节。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

    现代 CPU 以分段方式管理内存。在正常操作中,操作系统将新程序加载到 program-text 段中,并在 data 段中建立堆栈。操作系统告诉 CPU 永远不要在数据段中运行代码。您的代码位于 code[] 的数据段中。因此是段错误。

    【讨论】:

      【解决方案5】:

      这需要一些努力。

      您的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 标志,但我不知道该怎么做。而且您的操作系统甚至可能不允许您拥有可写可执行的页面。

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

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

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

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

          基本上,我当前的汇编程序不支持c.nop 指令。因此,我使用正确的语法将CNOP 定义为c.nop 的十六进制等效项,并在我知道的asm 中使用。 .rept &lt;NUM&gt; .endr 基本上会重复指令NUM次。

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

          【讨论】:

          • 通常当人们想要测试 shellcode 时,他们有一个像 "\x31\xc0\xc3" 这样的字符串,而不是分解成 .byte 0x??, 0x??, ....word 块。但是,是的,如果您这样做,那是另一种将其转换为可执行页面中的机器代码的方法。 (在函数的中间,因此除了 NOP 之外的大多数指令都需要使用clobber 声明来告诉编译器它对寄存器做了什么,除非它退出或执行 execve,因此执行不会离开 asm 语句)。
          猜你喜欢
          • 2015-03-21
          • 2013-08-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-20
          • 2021-08-15
          • 2012-06-26
          相关资源
          最近更新 更多