【问题标题】:Can a C program modify its executable file?C程序可以修改其可执行文件吗?
【发布时间】:2011-04-23 08:47:26
【问题描述】:

我手头的时间有点多,开始想知道我是否可以编写一个自修改程序。为此,我用 C 语言编写了一个“Hello World”,然后使用十六进制编辑器在编译的可执行文件中查找“Hello World”字符串的位置。是否可以修改此程序以打开自身并覆盖“Hello World”字符串?

char* str = "Hello World\n";

int main(int argc, char* argv) {

  printf(str);

  FILE * file = fopen(argv, "r+");

  fseek(file, 0x1000, SEEK_SET);
  fputs("Goodbyewrld\n", file);      
  fclose(file);    

  return 0;
}

这不起作用,我假设有一些东西阻止它自行打开,因为我可以将它分成两个单独的程序(一个“Hello World”和一些修改它的东西)并且它工作正常。

编辑:我的理解是,当程序运行时,它会完全加载到内存中。因此,无论出于何种目的和目的,硬盘驱动器上的可执行文件都是一个副本。为什么修改自己会有问题?

有解决办法吗?

谢谢

【问题讨论】:

  • 哈哈,没有。只是无聊的好奇心。
  • 作弊引擎的工作原理是在程序中查找值并让您更改它们。它可以让你在电子游戏等方面获得额外的生命,但我认为你并不真的想要作弊引擎作为答案。
  • 请注意,修改文件不一定——取决于操作系统——与修改实际执行的内存代码相同。您应该清楚自己希望达到的目标。
  • 是的,您可以修改自己的可执行文件。你问的是修改正在运行的程序。

标签: c metaprogramming self-modifying


【解决方案1】:

在 Windows 上,当程序运行时,整个 *.exe 文件使用memory-mapped-file functions in Windows映射到内存中。这意味着文件不一定是一次全部加载,而是文件的页面在访问时按需加载。

当文件以这种方式映射时,另一个应用程序(包括它自己)不能在同一个文件运行时写入来更改它。 (另外,在 Windows 上运行的可执行文件也不能重命名,但它可以在 Linux 和其他具有基于 inode 的文件系统的 Unix 系统上)。

可以更改映射到内存中的位,但如果您这样做,操作系统会使用“写时复制”语义来执行此操作,这意味着基础文件不会在磁盘上更改,而是内存中的页面是根据您的修改制作的。不过,在被允许这样做之前,您通常必须摆弄相关内存上的保护位(例如VirtualProtect)。

曾经,在非常受限制的内存环境中使用自修改代码的低级汇编程序曾经很常见。然而,没有人再这样做了,因为我们不是在相同的受限环境中运行,而且现代处理器有很长的管道,如果你开始从它们下面更改代码,就会非常不安。

【讨论】:

  • 请注意,在 Unix / Linux 上,虽然您可以重命名甚至删除磁盘上正在运行的可执行文件,但它会原封不动地保存在内存中,直到进程终止。
  • 并且在需​​要的时候,还可以创建修改版本的文件副本,启动新进程执行文件替换并自行终止。
  • 可执行文件在运行时可以重命名。
【解决方案2】:

如果您使用的是 Windows,您可以执行以下操作:

分步示例:

  1. 在您要修改的代码页上调用VirtualProtect(),并使用PAGE_WRITECOPY 保护。
  2. 修改代码页。
  3. 在修改后的代码页上调用VirtualProtect(),使用PAGE_EXECUTE 保护。
  4. 致电FlushInstructionCache()

有关详细信息,请参阅How to Modify Executable Code in Memory(存档时间:2010 年 8 月)

【讨论】:

    【解决方案3】:

    它非常依赖于操作系统。某些操作系统会锁定该文件,因此您可以尝试通过在某处制作新副本来作弊,但您只是在运行该程序的另一个版本。

    其他操作系统对文件进行安全检查,例如iPhone,所以编写它会是很多工作,而且它是一个只读文件。

    对于其他系统,您甚至可能不知道文件在哪里。

    【讨论】:

      【解决方案4】:

      所有目前的答案或多或少都围绕着这样一个事实,即今天你不能再轻易地进行自我修改机器代码了。我同意今天的 PC 基本上是这样。

      但是,如果您真的想看到自己的自修改代码在运行,您有一些可用的可能性:

      • 试用微控制器,较简单的微控制器没有高级流水线。我找到的最便宜和最快的选择是MSP430 USB-Stick

      • 如果仿真适合您,您可以为较旧的非流水线平台运行仿真器。

      • 如果您只是为了好玩而想要自修改代码,您可以在Corewars 使用自毁代码(更确切地说是敌人毁灭)获得更多乐趣。

      • 如果您愿意从 C 语言迁移到 Lisp 方言,那么编写代码的代码在那儿是很自然的。我建议Scheme 故意保持小。

      【讨论】:

        【解决方案5】:

        如果我们谈论的是在 x86 环境中执行此操作,这应该不是不可能的。不过应该谨慎使用它,因为 x86 指令是可变长度的。长指令可能会覆盖后面的指令,而较短的指令会留下覆盖指令的残留数据,这些数据应该被处理(NOP 指令)。

        当 x86 首次受到保护时,intel 参考手册推荐了以下方法来调试对 XO(仅限执行)区域的访问:

        1. 创建一个新的空选择器(远指针的“高”部分)
        2. 将其属性设置为 XO 区域的属性
        3. 如果您只想查看其中的内容,则必须将新选择器的访问属性设置为 RO DATA
        4. 如果要修改数据,必须将访问属性设置为 RW DATA

        所以问题的答案在最后一步。如果您希望能够插入调试器所做的断点指令,则 RW 是必需的。比 80286 更现代的处理器具有内部调试寄存器,以启用可能导致发出断点的非侵入式监控功能。

        从 Win16 开始,Windows 提供了执行此操作的构建块。它们可能还在。我认为微软将这类指针操作称为“thunking”。


        我曾经用 PL/M-86 为 DOS 编写了一个非常快的 16 位数据库引擎。当 Windows 3.1 到来时(在 80386s 上运行),我将它移植到 Win16 环境中。我想利用可用的 32 位内存,但没有可用的 PL/M-32(或 Win32)。

        为了解决我的程序以下列方式使用 thunking 的问题

        1. 使用结构定义 32 位远指针 (sel_16:offs_32)
        2. 使用全局内存分配 32 位数据区域( >64KB 大小)并以 16 位远指针 (sel_16:offs_16) 格式接收它们
        3. 通过复制选择器填充结构中的数据,然后使用 16 位乘法与 32 位结果计算偏移量。
        4. 使用指令大小覆盖前缀将指针/结构加载到 es:ebx 中
        5. 使用指令大小和操作数大小前缀的组合访问数据

        一旦机制没有错误,它就可以顺利运行。我的程序使用的最大内存区域是 2304*2304 双精度,大约为 40MB。即使在今天,我仍将其称为“大”内存块。 1995 年,它是典型 SDRAM 棒 (128 MB PC100) 的 30%。

        【讨论】:

          【解决方案6】:

          在许多平台上都有不可移植的方法来执行此操作。例如,在 Windows 中,您可以使用 WriteProcessMemory() 执行此操作。然而,在 2010 年,这样做通常是一个非常糟糕的主意。这不是 DOS 的时代,您可以在汇编中编写代码并这样做以节省空间。很难做到正确,而且您基本上是在要求稳定性和安全性问题。除非你正在做一些非常低级的事情,比如调试器,我会说不要为此烦恼,否则你将引入的问题不值得你获得任何收益。

          【讨论】:

            【解决方案7】:

            自修改代码用于在内存中进行修改,而不是在文件中进行修改(就像 UPX 那样的运行时解包器)。此外,由于相对虚拟地址、可能的重定位和对大多数更新所需的标头的修改(例如,通过将 Hello world! 更改为 longer Hello World,您需要扩展文件中的数据段)。

            我建议你先学会在记忆中做这件事。对于文件更新,最简单且更通用的方法是运行程序的副本,以便修改原始程序。

            编辑:不要忘记使用自修改代码的主要原因:

            1) 混淆,因此实际执行的代码不是您通过文件的简单静态分析看到的代码。

            2) 性能,类似于 JIT。

            它们都不会从修改可执行文件中受益。

            【讨论】:

              【解决方案8】:

              如果您在 Windows 上操作,我相信它会锁定文件以防止在运行时对其进行修改。这就是为什么您经常需要退出程序才能安装更新的原因。在 linux 系统上情况并非如此。

              【讨论】:

              • 您可以在程序运行时修改其代码。 (在 Windows 上,这将是 WriteProcessMemory())。这就是您的调试器的工作方式。也就是说,这是一个非常糟糕的主意。
              • 我不认为我曾经声称它是一个好的:P。但是Windows为什么要锁定它呢?我的理解是,当程序运行时,它会完全加载到 ram 中。因此,无论出于何种目的和目的,硬盘驱动器上的可执行文件都是一个副本。为什么修改它会有问题?
              • @asveikau 这与磁盘上不在内存中的文件有关,但您对可以在内存中完成的操作是正确的。
              • @Joel,这不一定完全正确。对于足够大的程序窗口,可能会将部分可执行文件交换出内存。无论如何,我认为这种行为没有真正好的理由
              • @Joel - 程序运行时,不一定全部加载到内存中。可以根据需要调入部分内容。对于可能根据需要交换数据的大型程序尤其如此(实际上操作系统会执行所有分页,它对程序是透明的)。通过锁定程序文件,操作系统永远不必换出代码,因为它知道在需要时可以从哪里获取代码的原始副本。
              【解决方案9】:

              在应用程序在用户空间运行的较新版本的 Windows CE(至少 5.x 和更新版本)上(与所有应用程序在超级用户模式下运行的早期版本相比),应用程序甚至无法读取它自己的可执行文件。

              【讨论】:

                猜你喜欢
                • 2016-01-26
                • 1970-01-01
                • 1970-01-01
                • 2010-12-19
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多