我想要一个简单的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程序时,我得到了一个分段错误。有什么想法吗?
7条答案
按热度按时间3j86kqsm1#
机器代码必须在可执行页面中。您的
char code[]
位于读+写数据部分,没有exec权限,因此无法从那里执行代码。下面是一个使用
mmap
分配可执行页面的简单示例:字符串
有关
__builtin___clear_cache
的详细信息,请参阅此问题的另一个答案。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,eax
has exactly the same effect asxor rax,rax
。您需要包含机器码的页面具有执行权限。x86-64页表有一个单独的位用于执行和读取权限,与传统的386页表不同。
让静态数组位于read+exec内存中最简单的方法是用**
gcc -z execstack
**编译。(以前是让堆栈 * 和 * 其他部分可执行,现在只有堆栈)。字符串
编译为简单的asm(Godbolt -还显示它在没有
__builtin___clear_cache
的情况下会损坏-它将跳过存储并跳转到未初始化的堆栈空间。)这在-z execstack
下正确运行,在没有它的情况下会发生故障。型
旧版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攻击更难,因为在可执行页面中没有只读数据,其中有用的字节序列可以用作以
ret
或jmp reg
指令的字节结尾的“小工具”。型
在较旧的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
内部运行它,并在code
和ret0_code
上设置断点型
或者使用系统调用修改页面权限
除了使用
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。型
使用GCC9.2 -O3在Godbolt编译器资源管理器上编译
型
传递不同的参数会得到另一个
call reg
,但是即使使用__builtin___clear_cache
,两个sum(2,3)
调用也可以CSE。__attribute__((const))
不尊重对函数机器码的更改。不要这样做。如果你要JIT函数一次,然后调用多次,这是安全的。取消注解第一个
__clear_cache
将导致型
第一个存储区是因为
__clear_cache
和sum(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
告诉它。型
我在这个例子中使用了
PROT_READ|PROT_EXEC|PROT_WRITE
,所以它可以工作,不管你的变量在哪里。如果它是堆栈上的一个局部变量,而你忽略了PROT_WRITE
,那么当call
试图推送一个返回地址时,它会在使堆栈只读后失败。**此外,
PROT_WRITE
还允许您测试自修改的shellcode,例如将零编辑到自己的机器码中,或避免其他字节。型
如果我注解掉
mprotect
,它 * 确实 * 与GNU Binutilsld
的最新版本相冲突,后者不再将只读常量数据放入与.text
部分相同的ELF段中。如果我做了像
ret0_code[4] = 0xc3;
这样的操作,那么在此之后我需要__builtin___clear_cache(ret0_code+2, ret0_code+2)
来确保存储没有被优化掉,但是如果我不修改静态数组,那么在mprotect
之后就不需要它了。在mmap
+memcpy
或手动存储之后需要它,因为我们想要执行用C写的字节(用memcpy
)。sg2wtvxw3#
您需要通过特殊的编译器指令将程序集包含在行内,以便它正确地结束在代码段中。请参阅此指南,例如:http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
r1zk6ea14#
你的机器代码可能没问题,但你的CPU会反对。
现代CPU以段的形式管理内存。在正常操作中,操作系统将一个新程序加载到一个 program-text 段中,并在一个 data 段中建立一个堆栈。操作系统告诉CPU永远不要运行数据段中的代码。您的代码在
code[]
中,在一个数据段中。因此segfault。gt0wga4j5#
这将需要一些努力。
code
变量存储在可执行文件的.data
部分中:字符串
H1À
是变量的值。“.data
部分是 * 不可 * 执行的:型
我所熟悉的所有64位处理器都支持pagetable中的不可执行页面。大多数较新的32位处理器(支持PAE的处理器)在其pagetable中提供了足够的额外空间,以便操作系统模拟硬件不可执行页面。您需要运行一个古老的操作系统或一个古老的处理器来获得一个标记为可执行的
.data
部分。因为这些只是可执行文件中的标志,所以你应该能够通过其他机制设置
X
标志,但我不知道如何做到这一点。你的操作系统甚至可能不允许你拥有可写 * 和 * 可执行的页面。vfh0ocws6#
您可能需要在调用页面之前设置页面可执行文件。在MS-Windows上,请参阅VirtualProtect -函数。
URL:http://msdn.microsoft.com/en-us/library/windows/desktop/aa366898%28v=vs.85%29.aspx
nlejzf6q7#
对不起,我不能遵循上面的例子,这是复杂的。所以,我创建了一个优雅的解决方案,从C执行十六进制代码。基本上,你可以使用asm和.word关键字来放置十六进制格式的指令。请参阅下面的例子:
字符串
其中hocP定义如下:#define“.word 0x00010001 \n”
基本上,我当前的汇编程序不支持
c.nop
指令。因此,我将CNOP
定义为c.nop
的十六进制等价物,并使用正确的语法在asm中使用,我知道。.rept <NUM> .endr
基本上会重复指令NUM次。该解决方案正在运行并得到验证。