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

pkbketx9  于 11个月前  发布在  其他
关注(0)|答案(7)|浏览(109)

我想要一个简单的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


然后对其进行编译和对象转储以获得字节码。
然而,当我运行我的C程序时,我得到了一个分段错误。有什么想法吗?

3j86kqsm

3j86kqsm1#

机器代码必须在可执行页面中。您的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的详细信息,请参阅此问题的另一个答案。

xsuvu9jc

xsuvu9jc2#

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

**现在-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显示,如果缺少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堆栈属性可以为32位代码启用,但不能为64位代码启用。)
另一个选项是__attribute__((section(".text"))) const char code[] = ...;
工作示例:https://godbolt.org/z/draGeh
如果你需要数组是可写的,例如shellcode要在字符串中插入一些零,你可以使用ld -N链接。但最好使用-z execstack和一个本地数组。
问题中的两个问题:

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

顺便说一下,雷克斯前缀是冗余的。"\x31\xc0"xor eax,eaxhas exactly the same effect as xor rax,rax

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

让静态数组位于read+exec内存中最简单的方法是用**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
}

字符串
编译为简单的asm(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段中,因此它们都具有read+exec权限。因此使用**const char code[] = "...";**足以执行手动指定的字节作为数据,而无需execstack。
但是在我的Arch Linux系统中,GNU ld (GNU Binutils) 2.31.1不再是这样了。readelf -a显示.rodata部分进入了一个带有.eh_frame_hdr.eh_frame的ELF段,它只有读取权限。.text进入了一个带有Read + Exec的段,而.data以读+写的方式进入一个段(沿着.got.got.plt)。(What's the difference of section and segment in ELF file format
我假设这个变化是为了使ROP和Spectre攻击更难,因为在可执行页面中没有只读数据,其中有用的字节序列可以用作以retjmp 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而有效)

在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; 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#160236和https://godbolt.org/g/pGXn3B有一个示例,其中gcc实际上因为gcc“知道”malloc。同样是这个答案中的第一个代码块,我们在可执行堆栈空间中使用了一个本地数组。

(And在非x86体系结构上,I-cache与D-cache不一致,它实际上将执行任何必要的缓存同步。在x86上,它纯粹是一个编译时优化拦截器,本身不会扩展到任何指令,因为在纸面上,对于JIT或自修改代码,一个跳转或调用就足够了,而in practice it's completely impossible to observe stale code after a store在真实的x86 CPU上。)
Re:带三个下划线的奇怪名称:它是通常的__builtin_name模式,但name__clear_cache
我对@AntoineMathys的回答的编辑添加了这个。
在实践中,GCC/clang并不像它们知道malloc那样“知道”mmap(MAP_ANONYMOUS)。因此,在实践中,优化器会假设,即使没有__builtin___clear_cache(),非内联函数调用也可能通过函数指针将缓冲区中的memcpy作为数据读取。(除非您将函数类型声明为__attribute__((const))。)
在x86上,I-cache与数据缓存是一致的,在调用之前在asm中进行存储就足以保证正确性。在其他ISA上,__builtin___clear_cache()实际上会发出特殊指令,并确保正确的编译时顺序。
在将代码复制到缓冲区中时,包含它是一个很好的做法,因为它不会降低性能,并阻止假设的未来编译器破坏您的代码。(例如,如果他们确实理解mmap(MAP_ANONYMOUS)提供了新分配的匿名内存,其他内存都没有指针指向,就像malloc一样。)

**在当前的GCC中,我可以通过使用__attribute__((const))**来告诉优化器sum()是一个纯函数(只读取其args,而不是全局内存),从而激发GCC进行我们不希望的优化。然后GCC就知道sum()不能将memcpy的结果作为数据读取。

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

// 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);也许是沙箱JIT?它在我的桌面上与__clear_cache和崩溃没有。

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

您也可以给予单个现有页的读+写+执行权限。
在保存只读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)。

sg2wtvxw

sg2wtvxw3#

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

r1zk6ea1

r1zk6ea14#

你的机器代码可能没问题,但你的CPU会反对。
现代CPU以段的形式管理内存。在正常操作中,操作系统将一个新程序加载到一个 program-text 段中,并在一个 data 段中建立一个堆栈。操作系统告诉CPU永远不要运行数据段中的代码。您的代码在code[]中,在一个数据段中。因此segfault。

gt0wga4j

gt0wga4j5#

这将需要一些努力。
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位处理器都支持pagetable中的不可执行页面。大多数较新的32位处理器(支持PAE的处理器)在其pagetable中提供了足够的额外空间,以便操作系统模拟硬件不可执行页面。您需要运行一个古老的操作系统或一个古老的处理器来获得一个标记为可执行的.data部分。
因为这些只是可执行文件中的标志,所以你应该能够通过其他机制设置X标志,但我不知道如何做到这一点。你的操作系统甚至可能不允许你拥有可写 * 和 * 可执行的页面。

vfh0ocws

vfh0ocws6#

您可能需要在调用页面之前设置页面可执行文件。在MS-Windows上,请参阅VirtualProtect -函数。
URL:http://msdn.microsoft.com/en-us/library/windows/desktop/aa366898%28v=vs.85%29.aspx

nlejzf6q

nlejzf6q7#

对不起,我不能遵循上面的例子,这是复杂的。所以,我创建了一个优雅的解决方案,从C执行十六进制代码。基本上,你可以使用asm和.word关键字来放置十六进制格式的指令。请参阅下面的例子:

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

字符串
其中hocP定义如下:#define“.word 0x00010001 \n”
基本上,我当前的汇编程序不支持c.nop指令。因此,我将CNOP定义为c.nop的十六进制等价物,并使用正确的语法在asm中使用,我知道。.rept <NUM> .endr基本上会重复指令NUM次。
该解决方案正在运行并得到验证。

相关问题