是否可以在标准 C 中从堆栈中执行代码?

Posted

技术标签:

【中文标题】是否可以在标准 C 中从堆栈中执行代码?【英文标题】:Is it possible to execute code from the stack in standard C? 【发布时间】:2011-04-14 21:43:32 【问题描述】:

以下代码无法按预期工作,但希望能说明我的尝试:

long foo (int a, int b) 
  return a + b;


void call_foo_from_stack (void) 
  /* reserve space on the stack to store foo's code */
  char code[sizeof(*foo)];

  /* have a pointer to the beginning of the code */
  long (*fooptr)(int, int) = (long (*)(int, int)) code;

  /* copy foo's code to the stack */
  memcpy(code, foo, sizeof(*foo));

  /* execute foo from the stack */
  fooptr(3, 5);

显然,sizeof(*foo) 不会返回 foo() 函数的代码大小。

我知道在某些 CPU 上执行堆栈受到限制(或者至少在设置了限制标志的情况下)。除了 GCC 的嵌套函数最终可以存储在堆栈中之外,在标准 C 中是否有办法做到这一点?

【问题讨论】:

这通常是邪恶的。 +1:这是一个可怕的想法,但却是一个有趣的问题。 @Steven Sudit:这确实是邪恶的,不便携,但我很好奇它是否可能。 有可能,但很邪恶。在很多平台上都会失败,这是一件非常好的事情。 @R.. c编译器和链接器一般都是c程序,所以可以很清楚的从标准c生成机器码。应该生成什么机器代码以及将其加载到内存中并使其运行取决于平台(并且在某些机器上根本不可能,例如哈佛架构),并且将其“放在堆栈上”是一个更复杂的问题(并且可能是不必要的一个)。 【参考方案1】:

正如其他人所说,不可能以 标准 方式执行此操作 - 您最终得到的将是特定于平台的:CPU 因为操作码的结构方式(相对与绝对参考),操作系统,因为您可能需要设置页面保护以允许从堆栈执行。此外,它依赖于编译器:没有标准且有保证的方法来获取函数的大小。

如果你真的有一个很好的用例,就像flash reprogrammingRBerteig 提到的那样,准备好弄乱链接器脚本,验证反汇编,并知道你正在编写非常非标准和不可移植的代码:)

【讨论】:

【参考方案2】:

此类事物的一个有效用例是嵌入式系统,该系统通常会耗尽闪存,但需要能够在现场重新编程。为此,部分代码必须从其他存储设备运行(在我的情况下,闪存设备本身无法擦除和编程一个页面,同时允许从任何其他页面读取,但有些设备可以做到这一点),并且系统中有足够的 RAM 来容纳闪存写入器和要写入的新应用程序映像。

我们用 C 语言编写了必要的 FLASH 编程函数,但使用 #pragma 指令将其放置在与其余代码不同的 .text 段中。在链接器控制文件中,我们让链接器为该段的开始和结束定义全局符号,并将其置于 RAM 中的基地址,同时将生成的代码与位于 FLASH 中的加载区域一起放置.data 段和纯只读.rodata 段的初始化数据; FLASH 中的基地址也被计算并定义为全局符号。

在运行时,当应用程序更新功能被执行时,我们将新的应用程序映像读入它的缓冲区(并进行了所有应该进行的健全性检查,以确保它实际上是该设备的应用程序映像)。然后我们将更新内核从它在 FLASH 中的休眠位置​​复制到它在 RAM 中的链接位置(使用链接器定义的全局符号),然后像任何其他函数一样调用它。我们不必在调用站点做任何特殊的事情(甚至不需要函数指针),因为就链接器而言,它一直位于 RAM 中。事实上,在正常操作期间,特定的 RAM 块具有非常不同的用途,这对链接器来说并不重要。

也就是说,使这成为可能的所有机制都超出了标准的范围,或者是严格实现定义的行为。该标准并不关心代码在执行之前如何加载到内存中。它只是说系统可以执行代码。

【讨论】:

+1 将函数复制到内存中另一个部分的典型用例示例。我做了类似的事情,但大部分代码都在汇编中。【参考方案3】:

除了所有其他问题,我认为还没有人提到内存中最终形式的代码通常不能重新定位。您的示例 foo 函数,也许,但考虑:

int main(int argc, char **argv) 
    if (argc == 3) 
        return 1;
     else 
        return 0;
    

部分结果:

    if (argc == 3) 
  401149:       83 3b 03                cmpl   $0x3,(%ebx)
  40114c:       75 09                   jne    401157 <_main+0x27>
        return 1;
  40114e:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%ebp)
  401155:       eb 07                   jmp    40115e <_main+0x2e>
     else 
        return 0;
  401157:       c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%ebp)
  40115e:       8b 45 f4                mov    -0xc(%ebp),%eax
    

注意jne 401157 &lt;_main+0x27&gt;。在这种情况下,我们有一个 x86 条件近跳转指令0x75 0x09,它向前 9 个字节。所以这是可重定位的:如果我们将代码复制到其他地方,那么我们仍然希望向前移动 9 个字节。但是,如果它是相对跳转或调用,而不是您复制的函数的一部分的代码呢?你会跳转到堆栈上或附近的任意位置。

并不是所有的跳转和调用指令都是这样的(不是所有的架构,甚至不是所有的 x86)。有些是通过将地址加载到寄存器中然后进行远跳转/调用来引用绝对地址。当代码准备好执行时,所谓的“加载器”将通过填写目标最终在内存中实际拥有的任何地址来“修复”代码。复制此类代码(充其量)会导致代码跳转到或调用与原始地址相同的地址。如果目标不在您要复制的代码中,那可能就是您想要的。如果目标在您要复制的代码中,那么您将跳转到原始代码而不是副本。

相对地址与绝对地址的相同问题适用于代码以外的事物。例如,对数据部分(包含字符串文字、全局变量等)的引用如果被相对寻址并且不是复制代码的一部分,则会出错。

另外,函数指针不一定包含函数中第一条指令的地址。例如,在 ARM/thumb 互通模式下的 ARM 处理器上,thumb 函数的地址比其第一条指令的地址大 1。实际上,该值的最低有效位不是地址的一部分,它是一个标志,告诉 CPU 在跳转过程中切换到拇指模式。

【讨论】:

如果最终形式的代码无法重新定位,那么操作系统如何将您的代码加载到不同的区域?嗯。我不认为操作系统通过将程序从源位置复制到固定的“可执行”区域来交换任务。这会消耗太多时间。我使用的许多编译器都有一个用于生成位置无关代码 (PIC) 的标志。 @Thomas:我说过,最终形式的代码一般不能被重新定位。有些代码可以,有些不能。此外,仅仅因为整个程序(或 dll)是与位置无关的,并不意味着每个单独的函数都可以独立于可执行文件的其余部分重新定位,就像提问者希望做的那样。反汇编使用这些标志编译的一些代码:看看您是否可以找到一个引用该函数之外的相对地址的函数。例如,尝试编写两个包含“相同”字符串文字的函数。 @Thomas,可执行格式(特别是在 *nix 上广泛使用的 ELF 和在 Windows 上使用的 PE)包括一个重定位修复部分。操作系统加载程序负责在代码首次加载到进程时应用这些修复。因为这很昂贵,并且虚拟内存允许所有进程具有相同的内存映射,所以这些重定位表通常几乎是空的。与位置无关的代码也有助于减少重定位条目的使用。 哦,是的,当然有些操作系统要么没有受保护的内存,要么它们为共享库保留了一个虚拟地址空间区域,因此可执行文件可以在进程之间共享而无需可重定位因为它们在每个进程中都映射到相同的地址。并非所有东西都有可执行的重映射和 ASLR。【参考方案4】:

您的问题与动态生成的代码大致相似,只是您希望从堆栈而不是通用内存区域执行。

您需要获取足够的堆栈以适应您的函数副本。您可以通过编译它并查看生成的程序集来了解 foo() 函数的大小。然后硬编码你的 code[] 数组的大小以至少适合这个大小。还要确保 code[] 或将 foo() 复制到 code[] 的方式为复制的函数提供了适合您的处理器架构的正确指令对齐。

如果您的处理器有一个指令预取缓冲区,那么您需要在复制之后和从堆栈中执行函数之前刷新它,否则它几乎肯定会预取错误的数据并且您最终会执行垃圾。管理预取缓冲区和相关缓存是我在尝试动态生成代码时遇到的最大障碍。

正如其他人所提到的,如果您的堆栈不可执行,那么这是一个非首发。

【讨论】:

您可以将代码写入堆分配的数据并更改其保护。查看适用于 MS Windows 的 VALloc;一个参数可以让你指定分配的空间是否可以执行。 @Ira Baxter: 或 VirtualProtect() 你的堆栈:)【参考方案5】:

尝试执行此操作的方法有很多可能会出错,但它可以并且已经完成。这是缓冲区溢出攻击起作用的方式之一——编写一个小型恶意程序,用于可能是目标计算机的体系结构以及可能使处理器最终执行恶意的代码和/或数据代码和最坏的希望。

它的恶意使用也较少,但它通常受到操作系统和/或 CPU 的限制。有些 CPU 根本不允许这样做,因为代码和堆栈内存位于不同的地址空间中。

如果您确实想要这样做,您需要考虑的一件事是,您写入堆栈空间的代码将需要编译(或者如果编写为汇编代码或机器代码,则编写为)位置无关代码,否则您必须确保它以某个地址结束(并且它是按预期编写/编译的)。

我不认为 C 标准对此有任何说明。

【讨论】:

【参考方案6】:

您的想法的保留和复制部分很好。获取指向您出色的堆栈代码/数据的代码指针,这更难。将堆栈地址类型转换为代码指针应该可以解决问题。



   u8 code[256];

   int (*pt2Function)() = (int (*)())&code;

   code();

在托管系统上,绝不应允许执行此代码。在共享代码和数据存储器的嵌入式系统上,它应该可以正常工作。当你的同事阅读代码时,当然会有缓存问题、安全问题、工作安全问题等等……

【讨论】:

【参考方案7】:

您的操作系统不应该让您轻易做到这一点。不应该有任何同时具有写入和执行权限的内存,特别是堆栈具有许多不同的保护(请参阅 ExecShield、OpenWall 补丁...)。 IIRC,Selinux 还包括堆栈执行限制。您必须找到一种方法来完成以下一项或多项:

在操作系统级别禁用堆栈保护。 允许从堆栈对特定可执行文件执行。 mprotect() 堆栈。 也许还有其他一些事情......

【讨论】:

您可能还需要一个依赖于 CPU 的信号,表明您正在修改的内存中执行指令。有关与 Intel CPU 相关的更多详细信息,请参阅 Intel 参考手册;对于其他 CPU 类型,您可能需要其他东西。【参考方案8】:

如果您需要测量函数的大小,让编译器/链接器输出一个映射文件,您可以根据该信息计算函数大小。

【讨论】:

不是一个超级好的解决方案 - 当函数大小发生很大变化时需要手动更新。由于这整个交易是一个超级依赖平台的事情,你不妨编写不可移植的代码来获取函数长度。 @snemarch - 它不必是手动的,程序可以读入并解析它自己的地图文件。它需要保留地图文件,但解析纯文本文件通常比尝试分析来自可执行文件本身的二进制数据更容易。您甚至可以将地图文件数据作为构建过程的一部分进行解析,并将其嵌入到二进制文件的一部分中。不过,这可能更类似于在启用调试符号的情况下进行编译,然后从嵌入式调试信息中提取您需要的内容。 在构建过程中提取信息会有所帮助,但您仍然需要针对每个环境构建特定的代码,因此您不会获得很多收益 - 而且它无济于事。其他注意事项。【参考方案9】:

sizeof(*foo) 不是函数foo 的大小,它是指向 foo 的 指针 的大小(通常与您平台上的所有其他指针大小相同)。

sizeof 无法测量函数的大小。原因是sizeof是静态运算符,编译时不知道函数的大小。

由于函数的大小在编译时是未知的,这也意味着你不能定义一个足够大的静态大小的数组来包含一个函数。

你也许可以使用alloca 和一些讨厌的黑客来做一些可怕的事情,但简短的回答是,我认为你不能用标准 C 来做到这一点。

还应注意,堆栈在现代安全操作系统上不可执行。在某些情况下,您可能可以使其可执行,但这是一个非常糟糕的主意,这会使您的程序大范围地暴露于堆栈破坏攻击和可怕的错误。

【讨论】:

由于编译器无法知道函数代码的大小,是否有一个技巧可以定义具有固定代码大小的“填充”函数?想象一下 foo() 函数用 nop 指令填充到给定大小或类似的东西。 是的,请查看链接器说明手册中的定义段。使用某些特定于平台的pragmas 将函数放在单独的段中。将片段的内容复制到任何需要的位置。 我不相信你可以用 C 标准的方式定义这个大小。您可以在函数(甚至是后续函数)定义的末尾放置一个 C 风格的 goto 标签,然后使用自定义(汇编)代码来计算函数头的字节位置和最后一个字节位置之间的差异(以字节为单位)标签以获取尺寸。这是否有效取决于您的编译器可以在目标文件周围混洗多少代码。 GCC 有一个开关来防止函数在内存中被重新排序;您可以使用它来获得良好的效果,但从根本上说,您的解决方案将依赖于实现。 @Ira Baxter:函数末尾的标签不是一个好主意,因为它不会考虑函数结尾代码。最好依赖于非重新排序并在要调整大小的函数之后放置一个虚拟函数......无论如何,这个堆栈执行交易是不可移植的。 @snemarch:我实际上在之前使用了一个虚拟函数的地址,在之后使用了一个虚拟函数,以及(不幸的是)未承诺的非重新排序编译函数来确定 PC 是否在 相关活动的特定功能。我实际上并没有复制函数体;正如其他人所观察到的,它可能有一些不可重定位的位置。【参考方案10】:

在 Linux 上,您不能这样做,因为堆栈内存区域不可执行。 您可以在ELF 上阅读。

【讨论】:

以上是关于是否可以在标准 C 中从堆栈中执行代码?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Android 中从 Java 代码执行 Python 脚本

如何在 Python 中从 SXS 加载 C DLL?

如何在 c / c++ 程序中检测可能/潜在的堆栈溢出问题?

是否可以在 Lua 代码中从 SQL 中提取数据?

堆的Java和堆栈是否都驻留在RAM中,类似于C ++?

在 WebBrowser 的文档中从 JavaScript 调用 C# 代码