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

时间:2012-03-31 23:53:07

标签: c assembly x86 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");
}

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

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

然后编译并对其进行objdump以获取字节码。

然而,当我运行我的C程序时,我遇到了分段错误。有什么想法吗?

7 个答案:

答案 0 :(得分:16)

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

这是一个简单的例子:

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

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

  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));

  // doesn't actually flush cache on x86
  // but is still necessary so 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);

  return 0;
}

答案 1 :(得分:4)

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

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

答案 2 :(得分:3)

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

答案 3 :(得分: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标志,但我不知道该怎么做。而且您的操作系统甚至可能不会让您拥有可写可执行文件的页面。

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

两个问题:

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

顺便说一句,REX前缀完全是多余的。 "\x31\xc0" xor eax,eax has 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[] = "..."; 足以执行手动指定的字节作为数据。

但是在我的带有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

#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(之所以有效,是因为{/ {1}}在全局/静态阵列上)

在旧的和当前的Linux上:const (由于gcc -O3 -z execstack shellcode.c && ./a.out而起作用,无论您的计算机代码存储在哪里)。 还可以与-zexecstack 一起使用。 gcc允许clang -z execstack没有空格,但是clang不允许。

这些也可以在Windows上使用,在Windows中,只读数据进入-zexecstack而不是.rdata

由编译器生成的.rodata看起来像这样(来自main)。 您可以在objdump -drwC -Mintel内运行它,并在gdbcode上设置断点

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 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意味着您仍在从已知位置执行代码,也许可以更轻松地在其上设置断点。

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

在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“知道” mprotect

(并且在I缓存与D缓存不相干的非x86架构上,它实际上将执行任何必要的缓存同步。在x86上,它纯粹是编译时优化阻止程序。)

我对@AntoineMathys答案的编辑添加了此内容。 gcc目前尚不了解malloc,因此确实没有将存储优化到mmap返回的指针中。

但是在包含只读C变量的页面上的mmap之后,您不需要它。

mprotect

在此示例中,我使用了#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还允许您测试可自行修改的shellcode,例如将零编辑为自己的机器代码或避免的其他字节。

PROT_WRITE

如果我将$ 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的操作,那之后我需要ret0_code[2] = 0xc3;来确保没有对存储进行优化,但是如果我不修改静态数组,那么之后就不需要了__builtin___clear_cache(ret0_code+2, ret0_code+2)。在mprotect + mmap或手动存储之后,它是必需的,因为我们要执行用C(用memcpy编写的字节)。

答案 6 :(得分:-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 次。

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