PE - 区分数据和函数导出

6
我正在尝试找到一种方法,在IDA中确定哪些导出是数据导出,哪些是真正的函数导出。
例如,让我们来看看Microsoft的msftedit.dll的导出条目: enter image description here 虽然CreateTextServices是一个真正的导出函数: enter image description here IID_IRichEditOle是一个数据导出,IDA无法意识到这一点,将数据解释为代码: enter image description here 有人知道可靠的区分两者的方法吗?非常感谢您的帮助。
提前致谢。

不幸的是,没有任何方法。 - RbMm
@RbMm,你确定吗?我有一些创造性的方法来找到它们。例如:如果函数中有一个ret指令,并且有超过<min>个有效指令,并且IDA识别函数的调用约定,则导出的条目将被视为有效的导出函数。但是,我还是有一些误报想要识别。 - Aviv
你的方法非常不可靠。真正的数据输入和代码输入没有任何区别。我想问另一个问题 - 这有什么必要吗?如果知道目标,可能会有另一个答案。 - RbMm
@RbMm,它确实非常不可靠,但它可以正确预测大约95%的导出条目。我需要区分数据导出和函数导出,因为我需要制作一个可挂钩函数列表。 - Aviv
1个回答

2
对于每个导出,没有完全可靠的方法来处理它。
每个导出只指定可执行文件中的偏移量 - 从逻辑上讲,任何引用它的其他代码都可以将其视为代码或数据。
正如你所提到的,你可以想出启发式方法来检测几乎所有情况下的导出类型,但很容易想出反例,这些反例对于任何给定的启发式方法都不起作用。例如,采取你提出的规则:
导出条目将被认为是有效的导出函数,如果该函数中有一个ret指令,并且有超过个有效指令,并且IDA识别函数的调用约定。
误报:你可能会有一个使用尾调用优化并以jmp指令而不是ret指令结束的函数。任何短的函数也会失败。而且IDA有几种混淆代码不将其视为函数的方式。
误判:内存中可能有一个字符串紧随其后,后面紧跟着类似C3或C2的字符,如db 'BACKGAMMON0',0,0C3h - 这可以逻辑上解释为具有有效的11指令函数的导出,并且没有参数。
当你考虑到一个导出既可以被逻辑上视为代码又可以被视为数据时,界限变得更加模糊:想象一下,一个导出中的字节序列被复制到动态分配的内存中 - 可能甚至在另一个进程中 - 在稍后作为代码执行。
也许一个合理的建议是只信任IDA,并且如果IDA认为它是代码,则将导出视为代码。IDA功能的很大一部分是自动猜测数据的逻辑类型,通常做得很好。正如你所展示的那样,有时候它是错误的。但是你无法获得100%的准确性。你能做的最好的事情就是在误报和误判之间取得平衡。
这个问题不可决的证明:
无法确定导出是否将被执行为代码。导出是否将被读取为数据也是不可决的。由于我们不能保证任何一个是真实的,因此区分看似模棱两可的情况是不可能的。
证明:假设我们有一个神谕A(P,I,E),如果程序P(包括其所有依赖项)使用“输入”(外部状态)I执行(或从中读取)导出E(从在P的执行过程中加载的任何DLL中)则返回1。否则,它返回0。
让我们构建一个最小的程序Z(P,I,E),如果且仅当A(P,I,E)返回0时,它执行(或从中读取)导出E(其DLL已加载到地址空间中)。
现在考虑Z(Z,I,E)的结果:
如果Z(Z,I,E)执行(或从中读取)导出E,那么A(Z,I,E)将返回1。但是,定义Z(Z,I,E)是不访问导出E,除非A(Z,I,E)返回0。这是矛盾的。
如果Z(Z,I,E)不执行(或从中读取)导出E,那么A(Z,I,E)将返回0。但是,定义Z(Z,I,E)是这样的,即当A(Z,I,E)返回0时,它将访问导出E。这是矛盾的。
因此,我们最初的假设oracleA(P,I,E)存在被证明是错误的。
但是,您可以通过插装来做得更好...
根据您要解决的确切问题,您可能能够在运行时确定哪些导出是有效的函数。
例如,您可以编写一个应用程序,对您要分析的程序进行调试,并在包含要钩取的导出的每个页面上放置守卫页面。这意味着每当访问(执行/读取/写入)页面时,都会引发异常,并且调试器程序获得控制。
调试器可以检查程序上下文以查看所做的访问类型及其是否与导出有关。如果访问是尝试执行导出,则它可以在返回控件之前执行一些钩子功能。否则,它可以只返回控件到程序。
在任何情况下,每次异常后都会解除PAGE_GUARD修改器,因此您需要每次都将其放回。
毫不奇怪,这将使您的程序执行非常缓慢,因为对包含导出的任何页面的任何R/W/X访问都会导致昂贵的上下文切换 - 这可能包括您导出的函数的大多数指令的执行以及其他几个与它们无关的指令。
您可以使用其他仪器工具(如Pin)采用类似的方法进行操作。
请注意,通过仪器测试,您可能无法获取有关每个导出项使用情况的信息。这是因为您可能需要确定需要哪些输入/外部状态才能使程序访问每个导出项,以学习其是否被用作代码或数据(如果有)。
此外,请注意,对相同的导出项可能会发生执行和读取(甚至写入)访问。

非常好的答案,谢谢你!对于您建议的方法,稍作改进:将DLL注入到所需程序中并挂钩LdrInitializeThunk。钩子函数将为每个加载的模块调用。在钩子处,遍历模块的导出项并使用NO_ACCESS保护每个导出偏移的页面。然后,当页面被执行并引发访问冲突异常时,您可以将页面更改回执行保护,并确保此函数是代码导出。这对我来说已经足够好了!非常感谢。 - Aviv
@Aviv - LdrInitializeThunk 是用户模式下的线程启动点。每当在进程中创建新线程时都会调用它,但“它将为每个已加载的模块调用”,与此无关。 - RbMm
@RbMm,也许我错了,但另一个选择是钩取LdrxCallInitRoutine或LdrpMapDllNtFileName(因为LdrLoadDll不会为依赖项的依赖项调用)。 - Aviv
1
@Aviv 好观点!我喜欢 DLL 注入的想法更多;这将具有更好的性能。 - user1354557

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