如何让C代码执行十六进制机器码?

14

我想要一个简单的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获取字节码。

但是,当我运行我的C程序时,出现了分段错误。有什么想法吗?


5
即使你的数据段是可执行的,或者你没有启用NX(不可执行)保护,你期望这个做什么?它会执行一个指令,然后执行其后面的指令(你无法控制),再执行下一个指令,直到它达到不代表合法代码或触发段错误的内存位置。 - Niklas B.
2
你需要添加字节码来实现 ret,因为你所做的间接函数调用应该是一个 call,它会将返回地址推入堆栈。至少,这是我最好的猜测,我从未见过类似的情况。 - Chris
我期望这个程序什么都不会做,但是希望它能够运行而不崩溃。 - Nosrettap
2
你介意字符串末尾的 \0 吗? - BenjaminB
字符代码[] =“\ x48 \ x31 \ xc0 \ xc3 \ 0”; - LXSoft
请不要使用xorq %rax, %rax,而是使用xor eax, eax - phuclv
7个回答

27

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

以下是使用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的详细信息,请参见此问题的另一个答案


是的,ret 关键在于返回到调用函数中。 - Chris
2
感谢您的帮助。我只想补充一下,使用 objdump -d <文件名> 命令可以获得可执行文件的字节码。 - Jeff
1
static const char code[] 通常链接到可执行文件的文本段中,该文本段已经映射为只读和可执行。 您实际上不需要复制它。(尽管使它非const将是一个问题,因为数据段并不总是可执行的.) 此答案与问题的重要部分是 ret. 请参见 为什么const int main = 195会导致程序工作,但没有const却导致分段错误?. (195 = 0xC3 = ret). - Peter Cordes
这种技术与机器代码的来源无关。我为简单起见,包含了一个在编译时已知的片段,但是该概念适用于在运行时生成的机器代码,这是主要的使用情况。您很快就进行了评论,但我认为您错过了重点。 - user1157391
1
@MaximEgorushkin和Antoine:更新:这个答案仍然可以,但我的static const code[] = ...建议已经不再足够。当前(2019年)的GNU Binutils ld现在将.rodata链接到一个只读的单独段中,没有执行权限。使用gcc -z execstackmprotect()仍然比使用mmap+memcpy更容易。我自己添加了一个包含完整细节和工作示例的答案。 - Peter Cordes
显示剩余17条评论

12
直到最近的Linux内核版本(大约在5.4之前),你可以简单地使用gcc -z execstack进行编译 - 这将使得所有页面都可执行,包括只读数据(.rodata)和读写数据(.data),其中char code[] = "..."所在的位置。 现在-z execstack只适用于实际的堆栈,因此它目前仅适用于非常量的局部数组。也就是说,将char code[] = ...移到main函数中。现代系统尽可能少地将页面设置为可执行,以防止利用漏洞。
参见Linux默认对`.data`段的行为以了解内核的更改,以及在项目中包含汇编文件时,mmap的意外执行权限以了解旧的行为:启用Linux的READ_IMPLIES_EXEC进程以用于该程序。(在Linux 5.4中,该问答显示,您只会在缺少PT_GNU_STACK的情况下获得READ_IMPLIES_EXEC,就像一个非常旧的二进制文件;现代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和一个本地数组。
问题有两个:
1. 页面上的执行权限,因为您使用了一个将进入noexec read+write .data部分的数组。 2. 您的机器码没有以ret指令结尾,所以即使它能够运行,执行也会跳转到内存中的下一个位置,而不是返回。
另外,顺便说一句,REX前缀是多余的。"\x31\xc0" xor eax,eax 与 xor rax,rax 有完全相同的效果。
你需要具有执行权限的页面包含机器代码。x86-64页面表具有与读权限分开的执行权限位,不同于传统的386页面表。
使静态数组位于可读+可执行内存中的最简单方法是使用gcc -z execstack进行编译。(以前用于使堆栈和其他部分可执行,现在只有堆栈可执行)。
typedef int (*intfunc_int)(int);

int main(void)
{
    unsigned char execbuf[] = {   // compile with -zexecstack
        0x8d, 0x47, 0x01,     // lea 0x1(%rdi),%eax
        0xc3                  // ret
    };
    // a string initializer like  char execbuf[] = "\xc3"; also works

    // Tell GCC we're about to run this data as code.  x86 has coherent I-cache,
    // but this also stops optimization from removing the initialization as dead stores.
    __builtin___clear_cache (execbuf, execbuf+sizeof(execbuf)-1);
    // Without this, the store disappears

    intfunc_int fptr = (intfunc_int) execbuf;  // cast to function pointer.
    int res = fptr(2);           // deref the function pointer
    
    return res;    // returns 3 on non-Windows ISAs where the first arg is in EDI
}

编译成简单的汇编代码(Godbolt - 同时显示没有 __builtin___clear_cache 时的错误 - 它会跳过存储并直接跳转到未初始化的堆栈空间)。只有在使用 -z execstack 时才能正确运行,否则会导致段错误。
# GCC -O3 for x86-64
main:
    sub     rsp, 24              # GCC reserves 16 bytes more stack space than it needed
    mov     edi, 2               # function arg
    mov     DWORD PTR [rsp+12], -1023326323  # store 4 bytes of machine code
    lea     rax, [rsp+12]        # pointer into a register
    call    rax                  # call through the function pointer
    add     rsp, 24
    ret

旧的GNU ld链接器用于使.rodata可读+可执行
直到最近(2018年或2019年),标准工具链(binutils ld)将.rodata节放入与.text相同的ELF段中,因此它们都具有可读+可执行的权限。因此,使用const char code[] = "...";就足以将手动指定的字节作为数据执行,而无需execstack。
但在我的Arch Linux系统上,使用的是GNU ld (GNU Binutils) 2.31.1,情况已经不同了。通过readelf -a命令可以看到,.rodata部分已经进入了一个ELF段,与.eh_frame_hdr和.eh_frame一起,并且只具有读权限。.text部分进入了一个具有读和执行权限的段,而.data部分进入了一个具有读和写权限的段(以及.got和.got.plt)。我认为这个改变是为了使ROP和Spectre攻击更加困难,因为不再将只读数据放在可执行页面中,这些页面中的有用字节序列可能被用作“gadget”,以字节ret或jmp reg指令结尾。(ELF文件格式中的section和segment有什么区别
我认为这个改变是为了使ROP和Spectre攻击更加困难,因为不再将只读数据放在可执行页面中,这些页面中的有用字节序列可能被用作“gadget”,以字节ret或jmp reg指令结尾。
// See above for char code[] = {...} inside main with -z execstack, for current Linux

// This is 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`而起作用)
在Linux 5.5之前(或者左右):`gcc -O3 -z execstack shellcode.c && ./a.out`(无论您的机器代码存储在何处,都可以使用`-zexecstack`)。有趣的事实是:gcc允许无空格的`-zexecstack`,但clang只接受`clang -z execstack`。
这些方法在Windows上也适用,其中只读数据存储在`.rdata`而不是`.rodata`中。
编译器生成的`main`函数如下所示(来自`objdump -drwC -Mintel`)。您可以在`gdb`中运行它,并在`code`和`ret0_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; that's where .rodata goes
  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这样的编译器会假设数据和代码是分开的。这就像基于类型的严格别名规则,但即使使用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。此外,这个答案中的第一个代码块中,我们使用了一个位于可执行堆栈空间中的局部数组。
(而在非x86架构中,I-cache与D-cache不一致的情况下,它实际上会执行任何必要的缓存同步。在x86上,它纯粹是一个编译时的优化阻塞器,本身不会扩展到任何指令,因为在JIT或自修改代码中,跳转或调用在理论上足够了,并且在真实的x86 CPU上,存储后不可能观察到过时的代码。)
关于带有三个下划线的奇怪名称:这是通常的__builtin_name模式,但name是__clear_cache。
我在@AntoineMathys的答案上进行了编辑,添加了这个。
在实践中,GCC/clang对于mmap(MAP_ANONYMOUS)并不像对malloc那样“知道”。因此,在实践中,优化器将假设将缓冲区中的memcpy可能被非内联函数调用通过函数指针读取,即使没有__builtin___clear_cache()。(除非您将函数类型声明为__attribute__((const))。)
在x86上,指令缓存与数据缓存是一致的,因此在调用之前,将存储操作放在汇编中就足够保证正确性。在其他指令集架构上,__builtin___clear_cache()将会发出特殊指令,并确保正确的编译时顺序。
将其包含在将代码复制到缓冲区时是一个好的实践,因为它不会影响性能,并且可以防止假设性的未来编译器破坏您的代码。(例如,如果它们确实理解mmap(MAP_ANONYMOUS)提供了新分配的匿名内存,没有其他指针指向它,就像malloc一样。)

使用当前的GCC,我能够通过使用__attribute__((const))来激发GCC进行我们不想要的优化,告诉优化器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编译在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_cache`和`sum(2,3)`的调用。 (删除第一个`sum(2,3)`的调用确实可以让死代码消除在`__clear_cache`之间发生。)
第二个存储是因为`mmap`返回的缓冲区的副作用被认为是重要的,并且这是`main`留下的最终值。
Godbolt的`./a.out`选项运行程序似乎总是失败(退出状态为255);也许它对JIT进行了沙箱处理?在我的桌面上使用`__clear_cache`可以正常工作,而没有则会崩溃。
在保存现有C变量的页面上使用`mprotect`。
您还可以为单个现有页面提供读+写+执行权限。这是编译时使用`-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[4] = 0xc3;的操作,我需要在此之后使用__builtin___clear_cache(ret0_code+2, ret0_code+2)来确保存储没有被优化掉,但如果我不修改静态数组,那么在mprotect之后就不需要了。在mmap+memcpy或手动存储之后,它是必需的,因为我们希望执行用C编写的字节(使用memcpy)。

3
另一个原因是为了测试一小段shellcode,以确保你已经正确地将汇编程序转换为攻击载荷的一部分。如果你正在进行JIT编译,就不会像OP的"\x48\x31\xc0"那样有一个C字符串,而只会有字节。 - Peter Cordes

6

6

你的机器码可能没问题,但你的CPU有问题。

现代CPU以段为单位管理内存。在正常操作中,操作系统将新程序加载到一个程序文本段中,并在一个数据段中设置堆栈。操作系统告诉CPU永远不要在数据段中运行代码。而你的代码在code[]中,位于一个数据段中。因此会出现段错误。


3

这需要一些努力。

您的 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 标志,但我不知道如何进行。而且您的操作系统可能不允许同时具有可写和可执行的页面。

1

-1

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

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

CNOP 的定义如下:

#define ".word 0x00010001 \n"

基本上,我的当前汇编器不支持 c.nop 指令。因此,我将 CNOP 定义为具有适当语法的十六进制等效形式,并在 asm 中使用,这是我知道的。 .rept <NUM> .endr 基本上会重复执行 NUM 次指令。

这个解决方案已经经过验证并且可行。


通常人们想要测试shellcode时,会有一个字符串,例如 "\x31\xc0\xc3",而不是分解成 .byte 0x??, 0x??, ....word 块。但是,如果你这样做,那就是另一种将其转换为可执行页面中的机器代码的方法。(在函数中间,因此除了NOP之外的大多数指令都需要clobber声明来告诉编译器它对寄存器的影响,除非它退出或执行execve,以便执行不离开asm语句)。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接