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

16 浏览
0 Comments

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

我想要一个简单的C方法,在Linux 64位机器上运行十六进制字节码。这是我拥有的C程序:\n

char code[] = "\x48\x31\xc0";
#include 
int main(int argc, char **argv)
{
        int (*func) ();
        func = (int (*)()) code;
        (int)(*func)();
        printf("%s\n","DONE");
}

\n我尝试运行的代码(\"\\x48\\x31\\xc0\")是通过编写这个简单的汇编程序获得的(它本来不应该执行任何操作):\n

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

\n然后编译和objdump以获得字节码。\n然而,当我运行我的C程序时,出现了段错误。有什么想法吗?

0
0 Comments

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

为了使C代码执行十六进制机器码,您需要通过特殊的编译器指令将汇编代码嵌入到C代码中,以便它能够正确地进入代码段。您可以参考这个指南:http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

在C代码中嵌入汇编代码可以通过GCC的内联汇编(inline assembly)实现。内联汇编是一种将汇编代码直接嵌入到C代码中的机制,它可以使得C代码更加灵活和高效。

以下是一个示例,展示了如何使用GCC的内联汇编将十六进制机器码嵌入到C代码中:

#include 
int main() {
    // 声明一个指向机器码的函数指针
    void (*func)();
    // 十六进制机器码
    unsigned char code[] = "\x48\x31\xc0\x48\x89\xc3\x48\x89\xc8\x48\x8d\x35\x00\x00\x00\x00\x48\x83\xec\x08\x48\x31\xff\x48\x83\xc7\x01\x48\x83\xc6\x01\x48\x83\xc2\x01\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x0f\x05";
    // 将机器码赋值给函数指针
    func = (void (*)()) code;
    // 执行机器码
    func();
    return 0;
}

在上面的示例代码中,我们声明了一个指向机器码的函数指针`func`,并将十六进制机器码赋值给它。然后,我们通过调用`func()`来执行这段机器码。

通过使用GCC的内联汇编,您可以将汇编代码直接嵌入到C代码中,并使用函数指针来执行这段机器码。这种方法可以用于实现一些特殊的功能或者与底层硬件进行交互。

0
0 Comments

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

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

现在-z execstack只适用于实际的堆栈,因此它当前仅对非const本地数组有效,即将char code[] = ...移到main中。

问题的两个问题:

1.页面上的执行权限,因为您使用的数组将进入noexec读写.data部分。

2.您的机器码没有以ret指令结尾,因此即使它能够运行,执行也会跳转到内存中的下一个位置而不是返回。

你需要使包含机器码的页面具有执行权限。x86-64页表具有与只读权限不同的执行位,与传统的386页表不同。

获取静态数组位于可读+可执行内存中的最简单方法是使用gcc -z execstack进行编译。直到最近(2018或2019年),标准工具链(binutils ld)将.rodata部分放入与.text相同的ELF段中,因此它们将具有可读+可执行权限。因此,使用const char code[] = "..."就足以将手动指定的字节作为数据执行,而不需要execstack。

另一种选择是在运行时进行系统调用,将其复制到可执行页面中,或更改所在页面的权限。这比使用本地数组让GCC将代码复制到可执行堆栈内存中更复杂。

还有一种选择是__attribute__((section(".text"))) const char code[] = ...;

如果您需要使数组可写,例如用于在字符串中插入一些零的shellcode,您可以使用ld -N进行链接。但最好使用-z execstack和一个本地数组。

另一个选择是使用mmap(PROT_EXEC)在运行时分配新的可执行页面,或者使用mprotect(PROT_EXEC)将现有页面更改为可执行。这也适用于持有静态数据的页面。

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

告诉编译器数据将作为代码执行通常情况下,像GCC这样的编译器会假设数据和代码是分开的。这类似于基于类型的严格别名,但即使使用char*也不能将其定义为将缓冲区存储为数据,然后将该缓冲区作为函数指针调用的定义。

在GNU C中,您还需要使用__builtin___clear_cache(buf, buf + len)来在将机器码字节写入缓冲区后,通知优化器从该地址读取字节。这是因为优化器不会将函数指针的解引用视为从该地址读取字节。如果编译器证明存储的机器码字节不会被任何东西作为数据读取,那么死存储消除可能会消除对缓冲区的存储。在非x86架构上,I-cache与D-cache不一致时,它实际上会执行任何必要的缓存同步。在x86上,它仅仅是编译时的优化阻塞器,不会扩展到任何指令本身。

在实践中,GCC / clang不会像了解malloc那样了解mmap(MAP_ANONYMOUS)。因此,在实践中,优化器将假定将缓冲区的memcpy作为数据读取,即使没有__builtin___clear_cache()。除非您将函数类型声明为__attribute__((const)),否则它不会考虑缓冲区可能保存函数的机器码的可能性。

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

将其包含在复制代码到缓冲区中很好,因为它不会影响性能,并阻止假设未来的编译器破坏您的代码。例如,如果它们了解mmap(MAP_ANONYMOUS)会为新分配的匿名内存提供指针,而不像malloc一样。

使用当前的GCC,我成功地使GCC真正执行了我们不想要的优化,方法是使用__attribute__((const))。GCC知道sum()只能将其参数读取为数据。

使用另一个memcpy复制到相同的缓冲区后,GCC会将其优化为仅在调用之后的第二个存储位置进行的死存储消除。这导致第一个调用之前没有存储,因此它会执行00 00 add [rax], al字节,导致段错误。

现行GCC版本下,使用__attribute__((const))两个sum(2,3)调用可以进行CSE。在这种情况下,__attribute__((const))不会考虑函数的机器码是否发生了更改。不要这样做。如果您打算JIT函数一次,然后多次调用,这是安全的。

在一个现有的页面上使用mprotect给出读+写+执行权限是另一种选择。这是编译时使用-z execstack的替代方法。

如果您不在栈上本地的局部缓冲区中留出PROT_WRITE,则call在将栈设置为只读后尝试推送返回地址时将失败。

在此示例中,我使用了PROT_READ|PROT_EXEC|PROT_WRITE,这样它就可以在变量位于任何位置时工作。如果它是堆栈上的局部变量,并且您省略了PROT_WRITE,则在尝试推送返回地址时,call将失败。

如果我将mprotect注释掉,它会导致最近版本的GNU Binutils ld崩溃,因为它不再将只读常量数据放入与.text部分相同的ELF段中。

如果我做了类似ret0_code[2] = 0xc3;的事情,我需要在此之后使用__builtin___clear_cache(ret0_code+2, ret0_code+2)来确保存储未被优化掉,但如果我不修改静态数组,则在mprotect之后不需要它。在mmap+memcpy或手动存储之后,它是需要的,因为我们希望执行C中写入的字节(使用memcpy)。

如果我注释掉mprotect,那么最近版本的GNU Binutils ld会导致它实际上在没有mprotect的情况下崩溃,因为它不再将只读常量数据放入与.text部分相同的ELF段中。

0
0 Comments

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

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

下面是一个使用mmap分配可执行页面的简单示例:

#include 
#include 
#include 
int main ()
{
  char code[] = {
    0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]
    0xC3                        //  ret
  };
  int (*sum) (int, int) = NULL;
  
  // 分配可执行缓冲区
  sum = mmap (0, sizeof(code), PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  // 将代码复制到缓冲区
  memcpy (sum, code, sizeof(code));
  // 在x86上实际上不会刷新缓存,但确保memcpy不会被优化为死存储。
  __builtin___clear_cache (sum, sum + sizeof(sum));  // GNU C
  
  // 运行代码
  int a = 2;
  int b = 3;
  int c = sum (a, b);
  printf ("%d + %d = %d\n", a, b, c);
}

__builtin___clear_cache的详细信息,请参见此问题的另一个答案

是的,ret对于返回到调用函数中很重要。

感谢帮助。我只想补充一点,使用objdump -d 可以获取可执行文件的字节码。

static const char code[]通常链接到可执行文件的文本段,该段已经映射为只读和可执行。您实际上不需要复制它。(如果不是const,那将是一个问题;数据段并不总是可执行的。)与问题相比,这个答案的重要部分是ret。另请参见为什么const int main = 195会生成一个可工作的程序,但没有const会导致段错误?(195 = 0xC3 = ret)。

这种技术与机器码的来源无关。我包含了一个在编译时已知的片段,以简化示例,但是这个概念适用于在运行时生成的机器码,这是主要用例。你评论得很快,但我认为你错过了重点。

我删除了“static const”以避免给人留下代码必须在编译时已知的印象。

你确定它不在只读的data段中吗?data是可读写的。这就是.data段所在的位置,其中包含诸如int global_var = 2;这样的内容。是的,我确定。自己查看编译器输出,或者在使用gcc + ld生成的二进制文件上使用readelf -a

我是指只读数据段。将再次仔细检查。

当前工具链不使用单独的段来存储只读数据。 section .rodata(或Windows上的section .rdata)与文本段链接在一起,因此它与代码部分处于同一读+执行映射中。请参见ELF文件格式中段和段之间的区别

Antoine:更新:此答案仍然有效,但我的static const code[] = ...建议不再足够。当前(2019年)GNU Binutils的ld.rodata链接到一个单独的只读段中,没有执行权限。使用gcc -z execstackmprotect()仍然比使用mmap+memcpy更容易。我添加了自己的答案,其中包含完整的详细信息和可工作的示例。

我认为不需要__builtin___clear_cache(),因为sum被调用,因此其依赖于memcpy()的结果,而memcpy()又依赖于mmap的结果。是吗?

是的,确实如此。如果使用malloc并使用gcc -z execstack编译,GCC不会进行死存储消除。理论上,它可以识别mmap(MAP_ANONYMOUS)并表现得相同。此外,编译器无法“看到”实际执行memcpy和调用sum()之间的依赖关系,因此,如果稍后将其他字节存储到同一缓冲区中,您将只得到一次memcpy,可能在第一次调用之后!

是的__builtin___clear_cache()在最终的代码生成阶段会扩展为零条指令。正如我在我的编辑和注释中解释的那样,以及我的答案中。它的作用是通过防止编译时重新排序来使其他代码正确编译,而不是发出任何指令。它就像该代码区域的仅限于编译时的屏障。我的答案是否没有清楚地解释这一点?是否在示例中包含两个对同一缓冲区的memcpy有所帮助?

这就足够了,如果我们讨论混合多个memcpysum()调用,我建议在每个memcpy之后执行,而不是在每个sum()之前执行,前提是自从上次对sum()的调用以来已经写入了内存。我认为你的意思是在每个sum()之前执行一次,而不是在每个sum()之前执行一次,但是“每个”可能暗示了另一种含义。

我是指在第一次对sum()调用之前添加__builtin__clear_cache(),之后对sum()的每次后续调用,在此之前,只有在自上次对sum()的调用以来已经写入内存的情况下才添加__builtin__clear_cache()。

AntoineMathys和:我尝试重现我被建议的修改缓冲区后在第一次调用之后再次调用的影响。 godbolt.org/z/nEpaQX。但是GCC必须假设sum()读取或修改任意全局内存作为数据,它不能优化调用之间的存储。GCC不知道mmap(MAP_ANONYMOUS),因此必须假设mmap返回的值可能是通过其他方法访问的内存的指针。这也是为什么第一次memcpy和对sum()的调用之间对顺序的保留的原因。

我建议回滚到第10个版本;我认为解释需要一个可执行页面的英文文本至少与这个使用mmap的复杂的方法一样有用。此外,我同意__builtin___clear_cache()对于只是测试shellcode的人来说是多余的干扰。尤其是当非内联函数的潜在数据对非局部内存的写入在实践中意味着没有问题,除非使用malloc() + mprotect()。在使用gcc -z execstack编译的情况下。使用#ifdef __GNUC__会使混乱变得更糟,并且会让它在非GNU上工作。

和Antoine:我能够通过使用__attribute__((const))将其告知优化器它是一个纯函数(只读取其参数,而不是全局内存)来迫使GCC破坏一些东西。然后,死存储消除可以在调用之前的memcpy之后发生,导致第一次调用之前没有存储= 00 00 add [rax], al = 段错误。godbolt.org/z/6VNsav(Godbolt的./a.out选项运行程序似乎仍然失败,但在我的桌面上可以正常工作,包括__clear_cache和崩溃。)但是attribute(const)仍然会在修改机器码之前进行CSE调用。

我在我的答案中扩展了__builtin___clear_cache部分,加入了我在此答案中作为脚注添加的内容。我包含了一个链接,供阅读此答案的读者了解为什么删除__clear_cache实际上不会破坏这段代码。

这个答案现在对我来说很好,而且对代码中的注释进行了改进,删除了“必要”一词,这可能会被理解为实际上会导致问题。我试图插入一些提供更简单方法(gcc -z execstack)的提及,但如果您真的不想要那个,那我不坚持。提供另一个答案的详细信息,以了解__clear_cache是非常好的,也是我一开始应该做的事情,而不是内联一个很大的脚注。

0