直到最近的Linux内核版本(大约在5.4之前),你可以简单地使用
gcc -z execstack
进行编译 - 这将使得
所有页面都可执行,包括只读数据(
.rodata
)和读写数据(
.data
),其中
char code[] = "..."
所在的位置。
现在-z execstack
只适用于实际的堆栈,因此它目前仅适用于非常量的局部数组。也就是说,将
char code[] = ...
移到
main
函数中。现代系统尽可能少地将页面设置为可执行,以防止利用漏洞。
参见
Linux默认对`.data`段的行为以了解内核的更改,以及
在项目中包含汇编文件时,mmap的意外执行权限以了解旧的行为:启用Linux的
READ_IMPLIES_EXEC
进程以用于该程序。(在Linux 5.4中,该问答显示,您只会在缺少
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-stack属性可以实现32位代码,但对于64位代码则不行。)
另一个选项是
__attribute__((section(".text"))) const char code[] = ...;
工作示例:
https://godbolt.org/z/draGeh。
如果你需要数组可写,例如用于将一些零插入字符串的shellcode,你可以尝试使用
ld -N
进行链接。但最好还是使用-z execstack和一个本地数组。
问题有两个:
1. 页面上的执行权限,因为您使用了一个将进入noexec read+write .data部分的数组。
2. 您的机器码没有以ret指令结尾,所以即使它能够运行,执行也会跳转到内存中的下一个位置,而不是返回。
另外,顺便说一句,REX前缀是多余的。"\x31\xc0" xor eax,eax 与 xor rax,rax 有完全相同的效果。
你需要具有执行权限的页面包含机器代码。x86-64页面表具有与读权限分开的执行权限位,不同于传统的386页面表。
使静态数组位于可读+可执行内存中的最简单方法是使用
gcc -z execstack
进行编译。(以前用于使堆栈和其他部分可执行,现在只有堆栈可执行)。
typedef int (*intfunc_int)(int);
int main(void)
{
unsigned char execbuf[] = {
0x8d, 0x47, 0x01,
0xc3
};
__builtin___clear_cache (execbuf, execbuf+sizeof(execbuf)-1);
intfunc_int fptr = (intfunc_int) execbuf;
int res = fptr(2);
return res;
}
编译成简单的汇编代码(
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段中,因此它们都具有可读+可执行的权限。因此,使用const char code[] = "...";就足以将手动指定的字节作为数据执行,而无需execstack。
但在我的Arch Linux系统上,使用的是GNU ld (GNU Binutils) 2.31.1,情况已经不同了。通过readelf -a命令可以看到,.rodata部分已经进入了一个ELF段,与.eh_frame_hdr和.eh_frame一起,并且只具有读权限。.text部分进入了一个具有读和执行权限的段,而.data部分进入了一个具有读和写权限的段(以及.got和.got.plt)。我认为这个改变是为了使ROP和Spectre攻击更加困难,因为不再将只读数据放在可执行页面中,这些页面中的有用字节序列可能被用作“gadget”,以字节ret或jmp reg指令结尾。(
ELF文件格式中的section和segment有什么区别)
我认为这个改变是为了使ROP和Spectre攻击更加困难,因为不再将只读数据放在可执行页面中,这些页面中的有用字节序列可能被用作“gadget”,以字节ret或jmp reg指令结尾。
#include <stdio.h>
static const char code[] = {
0x8D, 0x04, 0x37,
0xC3
};
static const char ret0_code[] = "\x31\xc0\xc3";
int main () {
int (*sum) (int, int) = (void*)code;
int (*ret0)(void) = (void*)ret0_code;
int c = sum (2, 3);
return ret0();
}
在旧的Linux系统上:`gcc -O3 shellcode.c && ./a.out`(因为全局/静态数组上的`const`而起作用)
在Linux 5.5之前(或者左右):`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`上设置断点。
(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
。
在静态数组上使用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
。此外,这个答案中的第一个代码块中,我们使用了一个位于可执行堆栈空间中的局部数组。
(而在非x86架构中,I-cache与D-cache不一致的情况下,它实际上会执行任何必要的缓存同步。在x86上,它纯粹是一个编译时的优化阻塞器,本身不会扩展到任何指令,因为在JIT或自修改代码中,跳转或调用在理论上足够了,并且在真实的x86 CPU上,存储后不可能观察到过时的代码。)
关于带有三个下划线的奇怪名称:这是通常的__builtin_name模式,但name是__clear_cache。
我在@AntoineMathys的答案上进行了编辑,添加了这个。
在实践中,GCC/clang对于mmap(MAP_ANONYMOUS)并不像对malloc那样“知道”。因此,在实践中,优化器将假设将缓冲区中的memcpy可能被非内联函数调用通过函数指针读取,即使没有__builtin___clear_cache()。(除非您将函数类型声明为__attribute__((const))。)
在x86上,指令缓存与数据缓存是一致的,因此在调用之前,将存储操作放在汇编中就足够保证正确性。在其他指令集架构上,__builtin___clear_cache()将会发出特殊指令,并确保正确的编译时顺序。
将其包含在将代码复制到缓冲区时是一个好的实践,因为它不会影响性能,并且可以防止假设性的未来编译器破坏您的代码。(例如,如果它们确实理解mmap(MAP_ANONYMOUS)提供了新分配的匿名内存,没有其他指针指向它,就像malloc一样。)
使用当前的GCC,我能够通过使用__attribute__((const))
来激发GCC进行我们不想要的优化,告诉优化器sum()
是一个纯函数(只读取其参数,而不是全局内存)。然后GCC知道sum()
不能读取memcpy
的结果作为数据。
在调用之后,通过对同一缓冲区进行另一个memcpy
,GCC将死存储消除为仅在调用之后的第二个存储位置。这导致在第一次调用之前没有存储,因此执行00 00 add [rax], al
字节,导致段错误。
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
int main ()
{
char code[] = {
0x8D, 0x04, 0x37,
0xC3
};
__attribute__((const)) int (*sum) (int, int) = NULL;
sum = mmap (0,sizeof(code),PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANON,-1,0);
memcpy (sum, code, sizeof(code));
int c = sum (2, 3);
memcpy(sum, (char[]){0x31, 0xc0, 0xc3, 0}, 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
call rax
mov DWORD PTR [rbx], 12828721
... still CSE and use the RAX return value
第一个存储是因为`__clear_cache`和`sum(2,3)`的调用。 (删除第一个`sum(2,3)`的调用确实可以让死代码消除在`__clear_cache`之间发生。)
第二个存储是因为`mmap`返回的缓冲区的副作用被认为是重要的,并且这是`main`留下的最终值。
Godbolt的`./a.out`选项运行程序似乎总是失败(退出状态为255);也许它对JIT进行了沙箱处理?在我的桌面上使用`__clear_cache`可以正常工作,而没有则会崩溃。
在保存现有C变量的页面上使用`mprotect`。
您还可以为单个现有页面提供读+写+执行权限。这是编译时使用`-z execstack`的替代方法。
在保存只读C变量的页面上,您不需要使用`__clear_cache`,因为没有存储需要优化。但是,如果要初始化一个本地缓冲区(在堆栈上),您仍然需要它。否则,GCC将优化掉此私有缓冲区的初始化程序,而非内联函数调用绝对不会有指向它的指针。(逃逸分析)。除非通过`__builtin___clear_cache`告诉它,否则它不会考虑缓冲区可能保存函数的机器代码的可能性。
#include <stdio.h>
#include <sys/mman.h>
#include <stdint.h>
static const char code[] = {
0x8D, 0x04, 0x37,
0xC3
};
static const char ret0_code[] = "\x31\xc0\xc3";
int main () {
int (*sum) (int, int) = (void*)code;
int (*ret0)(void) = (void*)ret0_code;
uintptr_t page = (uintptr_t)code & -4095ULL;
mprotect((void*)page, 4096, PROT_READ|PROT_EXEC|PROT_WRITE);
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
)。
ret
,因为你所做的间接函数调用应该是一个call
,它会将返回地址推入堆栈。至少,这是我最好的猜测,我从未见过类似的情况。 - Chrisxorq %rax, %rax
,而是使用xor eax, eax
。 - phuclv