在最近的 Linux 内核版本(有时在 5.4 之前)之前,您可以简单地使用gcc -z execstack
- 进行编译,这将使所有页面都可执行,包括只读数据 ( .rodata
) 和读写数据 ( .data
) char code[] = "..."
。
现在-z execstack
仅适用于实际堆栈,因此它目前仅适用于非常量本地数组。 即char code[] = ...
搬入main
。
有关内核更改,请参阅针对 `.data` 部分的 Linux 默认行为,以及在项目中包含汇编文件时来自 mmap 的意外 exec 权限以了解旧行为:READ_IMPLIES_EXEC
为该程序启用 Linux 进程。(在 Linux 5.4 中,Q&A 显示你只会得到READ_IMPLIES_EXEC
一个缺失的PT_GNU_STACK
,就像一个非常旧的二进制文件;现代 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 和本地数组。
问题中有两个问题:
- 页面上的 exec 权限,因为您使用了一个将进入 noexec 读+写
.data
部分的数组。
- 您的机器代码不会以
ret
指令结束,因此即使它确实运行了,执行也会落入内存中的下一个内容,而不是返回。
顺便说一句,REX 前缀是完全多余的。 "\x31\xc0"
xor eax,eax
具有与 完全相同的效果xor rax,rax
。
您需要包含机器代码的页面具有执行权限。与传统的 386 页表不同,x86-64 页表有一个单独的位用于执行与读取权限分开。
让静态数组进入 read+exec 内存的最简单方法是使用gcc -z execstack
. (用于使堆栈和其他部分可执行,现在只有堆栈)。
直到最近(2018 年或 2019 年),标准工具链 (binutils ld
) 将 section.rodata
放入与 相同的 ELF 段中.text
,因此它们都具有 read+exec 权限。因此 usingconst char code[] = "...";
足以将手动指定的字节作为数据执行,而无需 execstack。
但在我的 Arch Linux 系统上GNU ld (GNU Binutils) 2.31.1
,情况不再如此。 readelf -a
显示该.rodata
部分进入了一个带有.eh_frame_hdr
and的 ELF 段.eh_frame
,并且它只有读取权限。 .text
使用 Read + Exec.data
进入一个段,并使用 Read + Write 进入一个段(以及.got
and .got.plt
)。(ELF文件格式中section和segment有什么区别)
我认为这种变化是通过在可执行页面中不包含只读数据来使 ROP 和 Spectre 攻击更加困难,其中有用字节序列可以用作以字节结尾的“小工具”ret
或jmp reg
指令。
// TODO: use char code[] = {...} inside main, with -z execstack, for current Linux
// 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
设置断点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 这样的编译器假定数据和代码是分开的。这就像基于类型的严格别名,但即使 usingchar*
也不能很好地定义存储到缓冲区中,然后将该缓冲区作为函数指针调用。
在 GNU C 中,您还需要使用__builtin___clear_cache(buf, buf + len)
after 将机器代码字节写入缓冲区,因为优化器不会将取消引用函数指针视为从该地址读取字节。如果编译器证明存储没有被任何东西读取为数据,则死存储消除可以将机器代码字节的存储删除到缓冲区中。 https://codegolf.stackexchange.com/questions/160100/the-repetitive-byte-counter/160236#160236和https://godbolt.org/g/pGXn3B有一个例子 gcc 确实做了这个优化,因为 gcc “知道” malloc
。
(在 I-cache 与 D-cache 不一致的非 x86 架构上,它实际上会进行任何必要的缓存同步。在 x86 上,它纯粹是一个编译时优化阻塞器,不会扩展到任何指令本身。)
回复:带有三个下划线的奇怪名称:这是通常的__builtin_name
模式,但是name
是__clear_cache
.
我对@AntoineMathys 回答的编辑添加了这个。
在实践中,GCC/clang 并不“了解”mmap(MAP_ANONYMOUS)
他们了解malloc
. 因此,在实践中,优化器会假设缓冲区中的 memcpy 可能会被非内联函数调用通过函数指针读取为数据,即使没有__builtin___clear_cache()
. (除非您将函数类型声明为__attribute__((const))
。)
在 x86 上,I-cache 与数据缓存一致,在调用之前以 asm 形式进行存储足以保证正确性。在其他 ISA 上,__builtin___clear_cache()
实际上会发出特殊指令并确保正确的编译时顺序。
将代码复制到缓冲区时包含它是一种很好的做法,因为它不会降低性能,并且可以阻止假设的未来编译器破坏您的代码。(例如,如果他们确实明白这mmap(MAP_ANONYMOUS)
会提供新分配的匿名内存,而其他任何东西都没有指针指向,就像 malloc 一样。)
__attribute__((const))
使用当前的 GCC,我能够通过告诉优化器sum()
是一个纯函数(仅读取其参数,而不是全局内存)来促使 GCC 真正进行我们不想要的优化。然后 GCC 知道sum()
无法读取memcpy
as 数据的结果。
在调用后将另一个存储在同一个缓冲区中,GCC 会在调用后将memcpy
死存储消除到第二个存储中。这导致在第一次调用之前没有存储,因此它执行字节,segfaulting。00 00 add [rax], al
// 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_cache
和sum(2,3)
电话。(删除第一个sum(2,3)
调用确实可以消除死存储__clear_cache
。)
第二个存储在那里,因为假定返回的缓冲区的副作用mmap
很重要,这就是最终的值main
。
Godbolt./a.out
运行程序的选项似乎仍然总是失败(退出状态为 255);也许它沙箱 JITing?它可以在我的桌面上运行__clear_cache
并且没有崩溃。
mprotect
在包含现有 C 变量的页面上。
您还可以授予单个现有页面读+写+执行权限。这是编译的替代方法-z execstack
您不需要__clear_cache
保存只读 C 变量的页面,因为没有要优化的存储。您仍然需要它来初始化本地缓冲区(在堆栈上)。否则 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
如果我将. _ _ld
.text
如果我做了类似的事情ret0_code[2] = 0xc3;
,我需要__builtin___clear_cache(ret0_code+2, ret0_code+2)
在那之后确保商店没有被优化掉,但是如果我不修改静态数组,那么之后就不需要它了mprotect
。mmap
在+或手动存储之后需要它memcpy
,因为我们想要执行已经用 C 写入的字节(带有memcpy
)。