【问题标题】:PE - Distinguish data from function exportPE - 将数据与函数导出区分开来
【发布时间】:2016-10-31 08:26:32
【问题描述】:

我正在尝试找到一种方法来在 IDA 中找出哪些导出是数据导出,哪些是实际函数导出。

例如,让我们看看微软的 msftedit.dll 的导出条目:

虽然CreateTextServices 是一个真正的导出函数:

IID_IRichEditOle 是一个数据导出,IDA 没有意识到这一点,将数据作为代码进行交互:

有人知道区分两者的可靠方法吗?非常感谢您的帮助。

提前致谢。

【问题讨论】:

  • 很遗憾没有办法
  • @RbMm,你确定吗?我有一些创造性的方法来找到这些。像:如果函数中有ret 指令并且有超过<min> 有效指令并且IDA 识别函数的调用约定,则导出的条目将被视为有效的导出函数。尽管如此,还是得到了一些我希望识别的误报
  • 你的方法很不靠谱。真正的导出数据输入与代码输入没有任何不同。我要问另一个问题 - 这需要什么?可能是如果知道目标 - 可能是另一个答案
  • @RbMm,确实很不靠谱,但它正确预测了大约95%~的导出条目。我需要区分数据导出和函数导出,因为我需要列出可挂钩函数。

标签: windows debugging portable-executable ida


【解决方案1】:

对于每次导出都没有完全可靠的方法。

每个导出仅在可执行文件中指定一个偏移量——从逻辑上讲,它可以被任何其他引用它的代码视为代码或数据。

正如您所提到的,您可以想出启发式方法来检测几乎所有情况下的导出类型,但是很容易想出对任何给定启发式都不起作用的反例。以您提出的规则为例:

如果函数中有ret指令,则导出的条目将被视为有效的导出函数,并且有超过<min>有效指令, IDA 识别函数的调用约定。

误报:您可能有一个函数使用tail call optimization 并以jmp 指令而不是ret 指令结束。任何简短的功能也会失败。 IDA 有几种方式可能会混淆为不将代码视为函数。

误报:内存中可能有一个字符串,后面紧跟一个C3C2,如db 'BACKGAMMON0',0,0C3h——这在逻辑上可以反汇编为一个有效的11条指令函数ret 并且没有参数。

当您考虑到导出可以在逻辑上同时被视为代码 数据时,界限就更加模糊了:想象一下,导出时的字节序列被复制到动态分配的内存中——甚至可能在另一个进程中——稍后它会作为代码执行。

也许一个合理的建议是信任 IDA,如果 IDA 认为它是代码,则将导出视为代码。 IDA 的很大一部分功能是自动猜测数据的逻辑类型,而且它通常很擅长。正如你所展示的,有时它是错误的。但无论如何,你无法获得 100% 的准确率。你能做的最好的就是在假阴性和假阳性之间取得平衡。


证明这个问题的不可判定性:

导出是否将作为代码执行是不确定的。导出是否将被读取为数据也是不确定的。由于我们无法保证两者都是正确的,因此不可能区分看似模棱两可的情况。

证明:假设我们有一个 oracle A(P,I,E),如果程序 P(包括其所有依赖项)执行(或读取)导出 E(从在 @987654337 过程中加载的任何 DLL,则返回 1 @ 的执行)带有“输入”(外部状态)I。否则返回 0。

让我们构造一个最小程序Z(P,I,E),当且仅当A(P,I,E)返回0时,它执行(或读取)export 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) 的定义使其 访问导出EA(Z,I,E) 返回 0 时,这是矛盾的。

因此,我们最初假设 oracle A(P,I,E) 存在被证明是错误的。


但你可以通过仪器做得更好...

根据您要解决的具体问题,您可能能够确定哪些导出是运行时有效的函数。

例如,您可以编写一个应用程序,其中debugs 是您要分析的程序,并将guard pages 放置在包含您希望挂钩的导出的每个页面上。这意味着,无论何时访问(执行/读取/写入)页面,都会引发异常,并且调试器程序将获得控制权。

调试器可以检查程序上下文以查看访问的类型以及它是否与导出有关。如果访问是尝试执行导出,它可以在将控制权返回给程序之前执行一些挂钩功能。否则,它只能将控制权返回给程序。

在任何一种情况下,PAGE_GUARD 修饰符在每次异常后都会被解除,因此您每次都需要将其放回。

不出所料,这会使您的程序的执行非常慢,因为对任何包含导出的页面的任何 R/W/X 访问都会导致代价高昂的context switch——这可能包括执行作为导出函数一部分的大多数指令,以及与它们无关的其他一些指令。

您可以对其他检测工具采取类似的方法,例如 Pin

请注意,您可能无法通过检测获得有关每个导出的使用情况的信息。这是因为您可能需要确定使程序访问每个导出所需的输入/外部状态,以便了解它是用作代码还是用作数据(如果有的话)。

另请注意,执行和读取(甚至写入)访问都可能发生在同一个导出上。

【讨论】:

  • 伟大的答案芽,非常感谢!对您建议的方法的一点改进是:将 DLL 注入所需的程序并挂钩 LdrInitializeThunk。将为每个加载的模块调用钩子函数。在钩子上,迭代模块的导出并使用 NO_ACCESS 保护每个导出的偏移量页面。然后,当页面将被执行并抛出访问冲突异常时,您可以将页面改回执行保护,并确保该功能是代码导出。这对我来说已经足够了!非常感谢。
  • @Aviv - LdrInitializeThunk 这是用户模式下的线程起点。每次在过程中创建新胎面时调用。但是 ' 将为每个加载的模块调用。 " - 这里绝对不相关
  • @RbMm,也许我记错了,反正另一种选择是挂钩 LdrxCallInitRoutine 或 LdrpMapDllNtFileName (因为 LdrLoadDll 不调用依赖的依赖)
  • @Aviv 好点!我更喜欢 DLL 注入的想法;那会有更好的性能。
猜你喜欢
  • 2017-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多