如何获取c代码来执行十六进制字节码?

Nos*_*tap 9 c x86 assembly x86-64 shellcode

我想要一个简单的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");
}
Run Code Online (Sandbox Code Playgroud)

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

.text
.globl _start
_start:
        xorq %rax, %rax
Run Code Online (Sandbox Code Playgroud)

然后编译并objdump它以获取字节码.

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

Ant*_*hys 16

这是一个简单的例子:

#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);
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢您的帮助.我只想添加objdump -d <filename>可以获取可执行文件的字节代码. (2认同)

Pet*_*des 7

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

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

有关内核更改,请参阅Linux 默认针对 `.data` 部分的行为,以及当项目中包含程序集文件时,来自 mmap 的意外 exec 权限,以了解旧行为:READ_IMPLIES_EXEC为该程序启用 Linux进程。

另一种选择是在运行时进行系统调用以复制到可执行页面,或更改其所在页面的权限。这比使用本地数组让 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_hdr和进入 ELF 段.eh_frame,并且它只有读取权限。 .text使用 Read + Exec.data进入一个段,并使用 Read + Write 进入一个段(以及.got.got.plt)。(ELF文件格式中section和segment的区别是什么

我认为此更改是通过在可执行页面中没有只读数据来使 ROP 和 Spectre 攻击更加困难,在这些页面中,有用的字节序列可以用作以 aretjmp 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();
}
Run Code Online (Sandbox Code Playgroud)

在较旧的 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
Run Code Online (Sandbox Code Playgroud)

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

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)在将机器代码字节写入缓冲区后使用,因为优化器不会将取消引用函数指针视为从该地址读取字节。如果编译器证明存储没有被任何东西作为数据读取,死存储消除可以将机器代码字节的存储删除到缓冲区中。 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. 因此,在实践中,优化器会假设,即使没有__builtin___clear_cache(). (除非您将函数类型声明为__attribute__((const))。)

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

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


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

与其他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);
}
Run Code Online (Sandbox Code Playgroud)

上Godbolt编译探险与GCC9.2 -O3

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
Run Code Online (Sandbox Code Playgroud)

传递不同的 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
Run Code Online (Sandbox Code Playgroud)

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

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

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


mprotect 在保存现有 C 变量的页面上。

您还可以授予单个现有页面 read+write+exec 权限。这是编译的替代方法-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();
}
Run Code Online (Sandbox Code Playgroud)

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 +++
Run Code Online (Sandbox Code Playgroud)

如果我注释掉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,以确保您已正确地将汇编程序转换为漏洞利用负载的一部分。如果你是 JITing,你不会有像 OP 的 `"\x48\x31\xc0"` 这样的 C 字符串,你只有字节。 (2认同)

Qua*_*nic 5

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


thb*_*thb 5

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

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


归档时间:

查看次数:

10095 次

最近记录:

6 年,3 月 前