x86如何区分指令和数据

5
有没有一种更或多或少可靠的方法来判断内存中某个位置的数据是处理器指令的开头还是其他数据呢?
例如,E8 3F BD 6A 00可能是相对偏移为0x6ABD3F call 指令(E8),也可能是属于另一条指令的三个字节的数据,后面跟着 push 0 (6A 00)。
我知道这个问题听起来很傻,可能没有简单的方法,但也许指令集设计时就考虑了这个问题,也许一些简单的代码检查该位置周围的+-100个字节,可以得出一个非常可能正确的答案。
我想知道这个是因为我扫描程序的代码并用我的替换函数来取代所有调用某个函数的调用。到目前为止这样做是有效的,但不排除在我增加要替换的函数数量时,有些数据看起来恰好像是调用该地址的函数调用,并且将被替换,这将导致程序以最意外的方式崩溃。我想减少这种情况发生的概率。

1
这让我想起了我曾经使用过的一个旧的反汇编器。它通过机器码进行了9个不同的分析步骤,试图将代码与数据分离开来。但还是经常出错。 - Hans Passant
5个回答

8
如果是您的代码(或保留链接和调试信息的其他代码),最好的方法是扫描目标文件中的符号/重定位表。否则,没有可靠的方法来确定某个字节是指令还是数据。
可能最有效的方法是递归反汇编。即从入口点和所有找到的跳转目标处反汇编代码。但这并不完全可靠,因为它不遍历跳转表(您可以尝试使用一些启发式方法解决此问题,但这也不是完全可靠的)。
解决您的问题的方法是替换自身的补丁函数:用跳转指令覆盖其开头以调用您的函数。

4
很遗憾,没有一种100%可靠的方法来区分代码和数据。从CPU的角度来看,只有当某个跳转操作码引导处理器尝试执行字节时,代码才是代码。您可以尝试通过从程序入口点开始,并跟随所有可能的执行路径来进行控制流分析,但在存在指向函数的指针时,这可能会失败。
对于您的具体问题:我了解到您想用自己的替换函数替换现有函数。我建议您修补被替换的函数本身。即,不要查找对“foo()”函数的所有调用并将它们替换为对“bar()”的调用,而是将“foo()”的前几个字节替换为跳转到“bar()”(一个“jmp”,而不是“call”:您不想搞乱堆栈)。这样做不太令人满意,因为需要双重跳转,但它是可靠的。

+1 for the jmp trick. 如果我没记错的话,这也是Visual Studio实现导入库的方式。 - Didier Trosset
我需要一个调用,而不是跳转,并且我不想干扰堆栈。我希望我的替换函数可以处理参数,调用原始函数,处理结果并返回。jmp会覆盖原始函数的部分内容,而我希望保持其完整性。虽然这是一种可能性;我可以在原始函数的开头放置jmp到我的函数中,然后在我的函数的开头将原始代码改回原来的样子,调用它,当它返回时再把jmp放回去。 - AUTOMATIC
2
使用JMP指令意味着您可以轻松地操纵堆栈。例如,代码执行“CALL foo”,则foo的前几个字节被修改为“JMP bar”,因此在bar()中的堆栈与foo()完全相同,并且当bar()返回时,直接返回给调用者而不经过foo()。 - Roddy
我想从bar中调用foo,所以这并不简单。 - AUTOMATIC
1
正如您在先前的评论中所说,在bar()函数内,您需要用原本存在的代码替换jmp指令,然后调用foo()函数,最后再将jmp指令放回原处。 - Martin B

1

通常情况下,不可能区分数据和指令,这是由于冯·诺伊曼结构所致。分析周围的代码很有帮助,反汇编工具可以做到这一点(这个可能会有所帮助。如果您无法使用IDA Pro /它是商业软件/,请使用其他反汇编工具。)


1

普通代码具有非常特定的熵,因此很容易将其与大多数数据区分开来。然而,这是一种概率方法,但足够大的普通代码缓冲区可以被识别出来(特别是编译器输出时,您还可以识别模式,例如函数的开始)。

此外,一些操作码被保留以供将来使用,其他操作码仅在内核模式下可用。在这种情况下,通过了解它们并知道如何计算指令长度(您可以尝试由Z0mbie编写的例程),您可以做到这一点。


0

Thomas提出了正确的想法。要正确实现它,您需要拆解前几条指令(您将使用JMP覆盖的部分),并生成一个简单的跳板函数来执行它们,然后跳转到原始函数的其余部分。

有一些库可以为您完成此操作。其中一个著名的库是Detours,但它具有相当尴尬的许可条件。一个更自由许可证的相同想法的不错实现是Mhook


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接