在GCC中无需可执行栈间接调用嵌套函数的技术

Hacker News Top 论文

摘要

本文介绍了一种在GCC中无需可执行栈通过从蹦床读取指针来间接调用嵌套函数的技术,适用于较旧的GCC版本,并在noplate库中实现。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/29 15:36

# Martin Uecker 来源:https://uecker.codeberg.page/2026-08-29.html ### 在GCC上无须可执行栈间接调用嵌套函数 Martin Uecker, 2026-08-29 #### 引言 我们上次讨论过 (https://uecker.codeberg.page/2026-08-06.html) 如何在 GCC 17 和 Clang 上使用嵌套函数进行回调而无需可执行栈。但如果需要支持旧版本的 GCC 怎么办?当然,可以直接接受使用可执行栈(它并非像某些人声称的那么糟糕),但也可以通过某种技巧避免这一点。 #### GCC:嵌套函数与蹦床机制 首先我们来探讨 GCC 如何支持获取嵌套函数的地址。以下是不使用新宏的简单示例(Godbolt 示例 (https://godbolt.org/z/hv4nM5b1j))。 ``` typedef int cb_f(int y); int baz(cb_f p, int x) { return p(x); } int foo(int k) { int bar(int x) { return k + x; } return baz(bar, 2 * k); } ``` 在 x86_64 架构上,生成的汇编代码如下所示。 ``` bar.0: movl %edi, %eax addl (%r10), %eax ret foo: subq $56, %rsp leaq 64(%rsp), %rax movq %rax, 32(%rsp) movl %edi, (%rsp) leaq 4(%rsp), %rax movw $-17591, 4(%rsp) movabsq $bar.0, %rcx movq %rcx, 6(%rsp) movw $-17847, 14(%rsp) movq %rsp, 16(%rsp) movl $-1864106167, 24(%rsp) addl %edi, %edi call *%rax addq $56, %rsp ret ``` 这段代码在栈上放置了一个蹦床,并立即通过内联的 `baz` 函数调用它。蹦床是一段简短的代码序列,它将静态帧寄存器加载到栈上的一个结构体中(该结构体包含从父函数捕获的变量),然后跳转到本地函数执行。如果将常量 -17591、-17847 和 -1864106167 转换回汇编指令,会得到以下 x86_64 代码。 ``` movq $bar.0, r11 movq $frame, r10 jump *r11 ``` 静态链和代码地址都是蹦床代码中移动指令使用的立即常量。我们不必使用蹦床的地址来调用函数,而是可以从蹦床中提取代码地址和静态链,并利用 `__builtin_call_with_static_chain` 内置函数直接调用本地函数。例如,在 x64_64 架构(且使用大内存模型)下,可以借助 noplate 的 `peek` 和 `array_slice` (https://uecker.codeberg.page/2025-07-09.html) 宏以如下方式实现。 ``` unsigned char (*tramp)[24] = (void*)bar; void *code = peek(uint64_t, &array_slice(tramp, 2, 10)); void *chain = peek(uint64_t, &array_slice(tramp, 12, 20)); ``` 这些指针所包含的信息,与在尚未发布的 GCC 17 中使用新内置函数 `__builtin_call_static_chain` 和 `__builtin_call_code_adress` 能获取的信息完全相同,并可用于调用本地函数,具体方法如前所述。 ``` __builtin_call_with_static_chain(((typeof(bar)*)code)(arg), chain); ``` 因此,我们可以将从蹦床读取这两个指针值作为一种回退机制,用于旧版本的 GCC。缺点在于:仍然会创建蹦床,编译器仍无法去虚拟化间接调用,且栈仍会被标记为可执行。那么这样做获得了什么?由于我们实际上从未调用过蹦床,可以使用以下命令将栈重新设置为不可执行,这至少解决了该功能的安全性担忧。 ``` patchelf --clear-execstack program ``` 这个想法已在我实验性的库 noplate (https://codeberg.org/uecker/noplate/) 中实现,其中根据代码地址和静态链构造了一个宽指针。 #### 将蹦床用作函数描述符 另一个我认为值得探讨的想法:也可以将蹦床本身用作函数描述符。我们无需在创建蹦床的地方提取静态链和代码指针,而是像通常一样传递蹦床的地址。但在所有可能调用蹦床的地方,我们先检查该指针是否指向一个蹦床,然后提取代码地址和静态链,直接使用 `__builtin_call_with_static_chain` 调用嵌套函数。从某种意义上说,我们并不是在执行蹦床,而是在调用点使用一个超级简单的解释器来解释蹦床的代码——这个解释器非常简单,能够被内联,且仅能解释这种特定的代码序列(Godbolt 示例 (https://godbolt.org/z/YsfWednfr))。 #### 参考文献 - GCC, 嵌套函数 (https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html) - GCC, 构造函数调用 (https://gcc.gnu.org/onlinedocs/gcc/Constructing-Calls.html)

相似文章

使用GCC的嵌套函数与宽指针及无蹦床 II

Lobsters Hottest

GCC 16 确保不捕获变量的嵌套函数无需蹦床,提升安全性。GCC 17 引入了内建函数,通过宽指针和静态链寄存器,即使对于捕获变量的嵌套函数也能避免蹦床。

栈回溯技术能实现无泄漏的代码执行

Lobsters Hottest

本文探讨了在glibc中使用DWARF字节码进行栈回溯如何被利用来执行代码,并通过CTF挑战和对POSIX系统中线程取消机制的分析加以阐释。