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”和一些修改它的东西)并且它工作正常。

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

有解决方法吗?

谢谢

4

9 回答 9

32

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

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

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

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

于 2010-10-10T03:08:17.343 回答
6

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

分步示例:

  1. 调用VirtualProtect()要修改的代码页,并带有PAGE_WRITECOPY保护。
  2. 修改代码页。
  3. 调用VirtualProtect()修改后的代码页,带有PAGE_EXECUTE保护。
  4. 打电话FlushInstructionCache()

有关详细信息,请参阅如何修改内存中的可执行代码(存档:2010 年 8 月)

于 2010-11-23T00:36:03.920 回答
4

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

其他操作系统会对该文件进行安全检查,例如 iPhone,因此编写它需要大量工作,而且它是作为只读文件驻留的。

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

于 2010-10-10T03:04:02.550 回答
4

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

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

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

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

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

  • 如果你愿意从 C 转移到说 Lisp 方言,那么编写代码的代码在那里是很自然的。我建议有意保持较小的方案。

于 2010-10-10T12:49:58.367 回答
2

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

当 x86 第一次受到保护时,英特尔参考手册推荐了以下方法来调试对 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%。

于 2011-01-24T03:13:47.490 回答
1

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

于 2010-10-10T03:06:34.010 回答
1

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

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

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

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

2) 性能,比如 JIT。

他们都没有从修改可执行文件中受益。

于 2010-10-10T10:41:59.627 回答
0

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

于 2010-10-10T02:59:34.430 回答
0

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

于 2010-10-10T11:02:57.353 回答