没有一种完全可靠的方法可以为每一次导出做到这一点。
每个导出仅指定可执行文件内的偏移量 - 从逻辑上讲,它可以被引用它的任何其他代码视为代码或数据。
正如您所提到的,您可以提出启发式方法来检测几乎所有情况下的导出类型,但是很容易提出不适用于任何给定启发式的反例。以您提出的规则为例:
如果存在,导出的条目将被视为有效的导出函数ret
函数中的指令,and有超过<min>
有效的指示,andIDA 识别函数的调用约定。
漏报:您可能有一个使用的函数尾调用优化 https://en.wikipedia.org/wiki/Tail_call并以jmp
指令而不是ret
指示。任何短函数也会失败。 IDA 可能会通过多种方式混淆,导致不将代码视为函数。
误报:内存中可能有一个字符串,后面紧跟着一个C3
or C2
like db 'BACKGAMMON0',0,0C3h
-- 这可以在逻辑上反汇编为有效的 11 条指令函数ret
并且没有争论。
当您认为导出在逻辑上可以被视为两种代码时,这些界限就更加模糊了and数据:想象一下,导出时的字节序列被复制到动态分配的内存中(甚至可能在另一个进程中),稍后它会作为代码执行。
也许一个合理的建议是只信任 IDA,如果 IDA 认为导出是代码,则将其视为代码。 IDA 的很大一部分功能是自动猜测数据的逻辑类型,并且它通常非常擅长于此。正如你所表明的,有时它是错误的。但无论如何你都无法获得 100% 的准确度。您能做的最好的事情就是在误报和误报之间取得平衡。
该问题不可判定性的证明:
导出是否作为代码执行是不可确定的。导出是否会被读取为数据也是不确定的。由于我们无法保证其中任何一个都是真实的,因此区分看似模棱两可的情况是不可能的。
证明:假设我们有一个预言机A(P,I,E)
如果程序返回 1P
(包括其所有依赖项)执行(或读取)导出E
(从过程中加载的任何DLLP
的执行)与“输入”(外部状态)I
。否则,返回 0。
让我们构建一个最小程序Z(P,I,E)
执行(或读取)导出E
(加载到地址空间的 DLL)当且仅当A(P,I,E)
返回 0。
现在考虑结果Z(Z,I,E)
:
If Z(Z,I,E)
执行(或读取)导出E
, then A(Z,I,E)
会返回 1。但是Z(Z,I,E)
被定义为not访问导出E
unless A(Z,I,E)
返回0。这是一个矛盾。
If Z(Z,I,E)
不执行(或读取)导出E
, then A(Z,I,E)
将返回 0。但是Z(Z,I,E)
被定义为will访问导出E
when A(Z,I,E)
返回0。这是一个矛盾。
因此,我们最初的假设是甲骨文A(P,I,E)
存在被证明是错误的。
但你可以通过仪器做得更好......
根据您尝试解决的具体问题,您也许能够确定哪些导出在运行时是有效的函数。
例如,您可以编写一个应用程序debugs https://msdn.microsoft.com/en-us/library/ms809754.aspx您要分析和放置的程序保护页 https://msdn.microsoft.com/en-us/library/aa366549(v=vs.85).aspx在包含您想要挂钩的导出的每个页面上。这意味着,每当访问(执行/读取/写入)页面时,都会引发异常,并且调试器程序获得控制权。
调试器可以检查程序上下文以查看进行了何种类型的访问以及它是否与导出有关。如果访问是尝试执行导出,则它可以在将控制权返回给程序之前执行一些挂钩功能。否则,它可能会将控制权返回给程序。
无论哪种情况,PAGE_GUARD
每次异常后修饰符都会被解除,因此您每次都需要将其放回原处。
不出所料,这将使您的程序执行非常慢,因为对包含导出的任何页面的任何 R/W/X 访问都会导致昂贵的上下文切换 http://www.linfo.org/context_switch.html-- 这可能包括执行属于导出函数一部分的大多数指令,以及其他一些与它们无关的指令。
您可以对其他仪器工具采取类似的方法,例如Pin https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool.
请注意,您可能无法通过检测获得有关每个导出的使用情况的信息。这是因为您可能需要确定需要什么输入/外部状态才能使程序访问每个导出,以便了解它是用作代码还是用作数据(如果有的话)。
另请注意,执行和读取(甚至写入)访问可能会发生在同一导出上。