从未知的 .dll 获取函数原型

逆向工程 视窗 dll
2021-07-09 02:07:45

我有一个来自我想使用的另一个程序的未知 .dll。使用DLL 导出查看器,我能够找到导出的函数。

在此处输入图片说明

但是要调用它们,我需要有关参数和返回类型的信息。

  • 有没有一种简单的方法可以在拆卸中识别它们?
  • 是否存在可以提取这些原型或以某种方式帮助我的工具?
1个回答

注意:我假设在 Windows 上使用 32 位 x86,不幸的是,您的问题并没有确定。但是由于它是 Windows 并且您没有明确提及 x64,这是我可以做出的最明智的假设。

首先,尝试使用搜索引擎搜索函数名称。不要只满足于一个搜索引擎。如果失败,请检查带有 DLL 的包中的任何内容。是否包括进口 LIB?如果是这样,请使用这些来提供线索(可能有效,也可能无效)。

否则 ...

大多数反汇编程序(阅读标签维基 ) 将很容易地向您显示导出的函数。所以定位它们根本不是问题。它们通常还会显示其导出的名称。

从您的屏幕截图显示的输出来看,名称似乎没有mangled/decorated这表明 - 但不是确凿的证据- 函数使用stdcall 调用约定(最好阅读Ange 的这个,这里的主持人 pro temp 之一)。现在我不知道你知道多少,但既然你尝试过 RCE,你可能非常精通调用约定。如果不是,让我们这样总结:调用约定控制如何(顺序、对齐)以及通过什么方式(寄存器、堆栈)将参数传递给函数。我们稍后会回到这个话题。如果您使用的是 x64 Windows 并且 DLL 也是 64 位,则您可以依赖 Microsoft x64 调用约定(阅读本文)。

现在你可以选择两条主要路线

路线 1 - 使用 DLL 分析程序

如果您碰巧有一个使用相关 DLL 的程序,您可以使用调试器或反汇编器找出调用约定和传递的参数数量。只需查找call引用导出的 DLL 函数的说明并查找movpush前面的说明即可。如果你碰巧遇到cdecl函数,栈指针 ( esp) 会在call. 可能是这种情况(请参见下面的示例),但与各种特定于编译器的fastcall变体一样不太可能,因为stdcall提供了尽可能广泛的兼容性。

下面在第二种方法中概述的方法也将更详细地解释这里介绍的一些概念。

途径 2 - 分析 DLL 本身

如果您碰巧有 IDA 并且您分析了 32 位 DLL,那么 IDA 很可能已经使用其启发式方法确定了参数的数量和调用约定。让我演示一下(使用sqlite3.dll)。在“导出”选项卡中,找到您感兴趣的功能并双击它。这将带您到函数开始的地址(此处sqlite3_open)。

在此处输入图片说明

正如您所看到的,IDA 很容易发现该函数有两个参数(您可以查看SQLite3 文档来验证这一发现)。然而,这里还有另一件事。之后call sqlite3_open_v2_0我们可以看到堆栈指针调整了10h(=16)从而清理了四个参数。查看push之前指令,call我们可以看到确实通过堆栈传递了四个 32 位(即 DWORD)参数。由于部分函数sqlite3_open本身没有进一步清理,现在很明显它也可能遵循 C 调用约定 ( cdecl)。同样,我们可以通过查看文档来验证发现(这是您不会有的好处)。事实上,由于没有给出明确的调用约定,你最终默认为cdecl. 单人retn(一些反汇编程序会显示ret),意思是return,也不会清理堆栈,否则它看起来会像retn 8或相似。

这是一个相当小的函数,但即使有间接信息,我们也能够推断出很多关于它的信息。

现在,对于某些stdcall情况,您更有可能遇到前面提到的情况。为什么不选择一些著名的东西,比如kernel32.dllWindows 7?同样,我将采用一个微不足道的功能,因为最好展示要点。请注意,我告诉 IDA 不要使用 Microsoft 的调试符号并跳过使用 FLIRT 签名。这意味着一些默认启动的好东西被抑制以显示如何识别正在发生的事情。看:

在此处输入图片说明

对于我们的案例,绿线并不有趣,但您会经常遇到它们。它在几个编译器中ebp很常见,通常被称为“帧指针”(栈帧中的帧),基本上是访问栈变量所基于的偏移量。您可以在该行中看到一个典型的用途push [ebp+arg_0]IDA 发现了这一点并向我们展示了这一点Attributes: bp-based frame

我们在 之后没有看到堆栈指针的调整call sub_77E29B80,因此看起来(内部)函数也遵循stdcall调用约定。然而,ret 4被调用者(即AddAtomA本例中的函数暗示是为了清理堆栈,这意味着我们可以排除cdecl这种可能性。它是四个字节,因为这是 32 位系统上的“自然”大小。您还可以从我的内联注释中看到,参数是在堆栈上反向传递的。但是在从事 RCE 之前,您无论如何都应该知道这些事情,否则请阅读上面链接的文章和一些书籍,例如这里的那些

在这种特殊情况下,我们可以敢于做出另一个假设,但它可能会咬我们。假设这是 Microsoft 的fastcall约定(请记住,它们因编译器而异),然后将使用寄存器ecxedx,然后是在堆栈上传递的参数。这意味着在我们的例子中,我们可能想假设情况并非如此,因为这些寄存器在调用sub_77E29B80. 对于像这样的机器生成的代码,这是一个很好的论据。但是,如果是手动优化的代码,程序员可以依靠有关调用约定的知识并跳过在调用之前/之后保存/恢复寄存器call. 尽管如此,在这种情况下,手动优化的代码不太可能(或不太可能)使用帧指针。这三个指令并不是完成工作所必需的。所以像这样争论 - 即使没有先验知识 - 我们现在可以开始使用原型编写一个小程序:

int __stdcall AddAtomA(void* unknown)

并使用调试器,看看什么被传递。这通常是一个乏味的过程,但很多过程——尤其是查找参数的数量——可能会被编写成脚本。此外,一旦您确定了一个函数,很可能整个 DLL 中的调用约定都是相同的(当然存在例外)。只要确保您分析的函数至少包含一个参数,否则您将无法区分stdcall区分cdecl环境数据。

X 路线——丑陋的路线

您也可以简单地使用dumpbin或类似的工具来编写测试程序的创建脚本。然后这个测试程序将调用该函数,检查前后的堆栈指针,从而区分stdcallcdecl你也可以玩一些技巧,比如在堆栈上传递 20 个参数(如果你想stdcall为实验做假设),看看你的被调用者清理了多少。有很多可能性可以简单地尝试而不是分析。但是使用前两种方法您会得到更好(更可靠)的结果。

如果您因为不想使用而需要构建导入 LIB GetProcAddress,请参阅我在 StackOverflow 上的这个答案它展示了如何仅从 DLL 构建导入 LIB。

结论

这些方法与其他反汇编器不会有太大区别,我只需要以一种可以重现它们的方式展示事物,这就是我选择 IDA 的原因。IDA 的免费软件版本可能就足够了(32 位、PE、x86)——不过请记住,它不允许用于商业用途。

截取自 IDA 6.4 的屏幕截图。