不知道诸位看官是否有过这样的经历:在不经意之间发现一个DLL文件,它里边有不少有趣的导出函数——但是由于你不知道如何调用这些函数,所以只能大发感慨而又无能为力焉。固然有些知名的DLL可以直接通过搜索引擎来找到它的使用方式(比如本文中的例子ipsearcher.dll),不过我们诚然不能希望自己总能交到这样的好运。所以在本文中,李马希望通过自己文理不甚通达的讲解能够给大家以授人以渔的效果。

先决条件

阅读本文,你需要具备以下先决条件:

  • 初步了解汇编语言,虽然你并不一定需要去读懂DLL中导出函数的汇编代码,但是你至少应该了解诸如push、mov这些常用的汇编指令。
  • 一个能够查看DLL中导出函数的工具,Visual Studio中自带的Dependency Walker就足够胜任了,当然你也可以选择eXeScope。
  • 一个调试器。理论上讲VC也可以完成调试的工作,但它毕竟是更加针对于源代码一级调试的工具,所以你最好选择一个专用的汇编调试器。在本文中我用的是OllyDbg——我不会介绍有关这个调试工具的任何东西,而只是简要介绍我的调试过程。

准备好了吗?那么我们做一个热身运动吧先。

热身——函数调用约定

这里要详细介绍的是有关函数调用约定的内容,如果你已经了解了这方面的内容,可以跳过本节。

你可能在学习Windows程序设计的时候早已接触过“函数调用约定”这个词汇了,那个时候你所了解的内容可能是一个笼统的概念,内容大抵是说函数调用约定就是指的函数参数进栈顺序以及堆栈修正方式。譬如cdecl调用约定是函数参数自右而左进栈,由调用者修复堆栈;stdcall调用约定亦是函数参数自右而左进栈,但是由被调用者修复堆栈……噢不,这太晦涩了——在源代码上我们是无法看到这些东西的!

那么我们别无选择,只有深入到汇编一层了。考虑以下C++代码:

#include <stdio.h>

int __cdecl max1( int a, int b )
{
    return a > b ? a : b;
}

int __stdcall max2( int a, int b )
{
    return a > b ? a : b;
}

int main()
{
    printf( "max( 1, 2 ) of cdecl version: %d\n", max1( 1, 2 ) );
    printf( "max( 1, 2 ) of stdcall version: %d\n", max2( 1, 2 ) );
    return 0;
}

对应的汇编代码为:

; int __cdecl max1( int a, int b )
00401000 MOV EAX,DWORD PTR SS:[ESP+4]
00401004 MOV ECX,DWORD PTR SS:[ESP+8]
00401008 CMP EAX,ECX
0040100A JG SHORT CppTest.0040100E
0040100C MOV EAX,ECX
0040100E RETN

; int __stdcall max2( int a, int b )
00401010 MOV EAX,DWORD PTR SS:[ESP+4]
00401014 MOV ECX,DWORD PTR SS:[ESP+8]
00401018 CMP EAX,ECX
0040101A JG SHORT CppTest.0040101E
0040101C MOV EAX,ECX
0040101E RETN 8 ; 被调用者的堆栈修正

; max1( 1, 2 )
00401030 PUSH 2
00401032 PUSH 1
00401034 CALL CppTest.00401000
00401039 ADD ESP,8 ; 调用者的堆栈修正

; max2( 1, 2 )
0040104A PUSH 2
0040104C PUSH 1
0040104E CALL CppTest.00401010

好了,我来简要介绍一下。函数参数传入函数体是借由堆栈段完成的,也就是将各个参数依某种次序推入SS中——在cdecl与stdcall约定中,这个次序都是自右而左的。另外,由于将参数推入了堆栈致使堆栈指针ESP发生了变化,所以要在函数结束的时候重新修正ESP。从上边的汇编代码中你也可以很清楚地看到,cdecl约定是在调用max1之后修正的ESP,而stdcall约定则是在max2返回时借由RETN 8完成了这个修正工作。

另外,从上边的汇编代码中还可以看到,函数的返回值是由EAX带回的。

庖丁解牛

在了解了以上的知识后,我们就可以使用调试器来调试那个未知的DLL了。可以说,这整个的调试过程充满了惊险和刺激,而且我们还需要一定的技巧——如果你像我一样不喜欢阅读汇编代码的话。

在本文中,我所选择的调试示例是FTerm中附带的ipsearcher.dll,它提供了对纯真IP数据库的查询接口。下图是用Dependency Walker对其分析的结果:

你可以看到,这里边有两个导出函数:LookupAddress和_GetAddress,那么我们可以按照返回值、调用约定、函数名、参数列表的顺序将它们声明如下:

? ? LookupAddress( ? );
? ? _GetAddress( ? );

是的,有太多的未知,下面李马将要逐一地破解这些问号。

调试器不可能孤立地对DLL进行调试,我们所需要的应该是一个合适的EXE,这样有助于我们的探究工作。在这里我选择的EXE是我编写的ipsearcher.exe,当然这可能会让你认为我这篇文章的组织顺序有问题——毕竟是我已经知道了这两个导出函数之后(编写了ipsearcher.exe)还要假装成不知道的样子来对ipsearcher.dll来进行探究,所以我决定在下文中不对ipsearcher.exe的代码进行任何关注,而是直接进入到ipsearcher.dll的领空。

打开调试器,载入ipsearcher.exe。当ipsearcher.dll被装载后,会引发一个访问异常,可以忽略这个异常继续调试。根据Dependency Walker的分析结果,在ipsearcher.dll的0x00001BB0和0x00001C40处各下一个断点。现在在“IP地址”中输入一个IP地址(这里以寒泉BBS的IP为例),点击“查询”,会发现指令跳入0x00001C40中(也就是_GetAddress),它的代码如下:

10001C40 MOV EAX,DWORD PTR SS:[ESP+4] ; 一个参数
10001C44 PUSH ipsear_1.10009BE8
10001C49 PUSH EAX
10001C4A CALL ipsear_1.LookupAddress ; 两个参数
10001C4F ADD ESP,8 ; LookupAddress是cdecl调用约定
10001C52 MOV EAX,ipsear_1.10009BE8
10001C57 RETN ; _GetAddress这厮也是cdecl调用约定

很短的几行代码,不过它已经可以提供这些信息了:

  • 从SS的使用来看,_GetAddress只带有一个参数。
  • _GetAddress中调用了LookupAddress,后者带有两个参数。
  • 调用LookupAddress之后进行了堆栈修正,所以LookupAddress是cdecl调用约定。
  • _GetAddress返回时并未进行堆栈修正,所以_GetAddress也是cdecl调用约定。

于是,我们可以替换一下刚才的问号了:

? CDECL LookupAddress( ?, ? );
? CDECL _GetAddress( ? );

下面可以进行单步调试了,当代码步至10001C44时,你会发现寄存器窗口发生了如下的变化:

“202.207.177.9”终于出现了,这样一来我们可以继续对问号进行替换了:

? CDECL LookupAddress( PCSTR, ? );
? CDECL _GetAddress( PCSTR );

现在继续对代码进行跟踪,是进入LookupAddress的时候了。我们可以从先前_GetAddress的代码中可以发现,这两个导出函数一直在围绕10009BE8这个地址做文章,那么我们就要在单步调试LookupAddress的同时关注这个地址的数据改变。几步跟踪之后,你会发现10009BE8开头的8字节(两个DWORD)数据发生了改变,变成了10009AB4和10009B1C。那么我们再转向这两个地址,会发现:

这样一来就很清楚了,10009BE8是一个字符串指针的数组,它有两个元素。也就是说,我们的函数声明可以换成这样:

? CDECL LookupAddress( PCSTR, PSTR* );
PSTR* CDECL _GetAddress( PCSTR );

接下来需要确定的就是LookupAddress的返回值了。纵观LookupAddress的返回代码,你会发现这样的片断:

; 片断1
10001C0B XOR EAX,EAX
10001C0D POP ESI
10001C0E RETN
; 片断2
10001C2B MOV EAX,1
10001C30 POP ESI
10001C31 RETN

也就是说,这个函数有两个返回值:0或1。那么最后的真相终于大白于天下——

BOOL CDECL LookupAddress( PCSTR, PSTR* );
PSTR* CDECL _GetAddress( PCSTR );

GetProcAddress?

到此为止,这两个函数的声明终于让我们找出来了。也许你会觉得这就够了——接下来就是用typedef定义函数指针,然后使用LoadLibrary、GetProcAddress调用这些函数的事情了。

如果你真的这么认为的话,那我认为我有必要向你介绍这另外的一种方式。

首先请你建立一个名为ipsearcher.def的文件,然后在其中写入如下内容:

LIBRARY "ipsearcher"

EXPORTS
LookupAddress @1
_GetAddress   @2

将文件保存后,进入到命令行模式下,输入以下命令(前提是你拥有Visual Studio的附带工具lib.exe并有正确的路径指向。以Visual Studio 6.0为例,这个工具通常位于Microsoft Visual Studio\VC98\Bin下):

lib /def:ipsearcher.def

执行的结果有一个警告,不必理会。这时候我们会发现,lib为我们生成了一个ipsearcher.lib。

然后,我们继续编写ipsearcher.h文件,如下:

#ifndef IPSEARCHER_H
#define IPSEARCHER_H

#include <windows.h>

#pragma comment( lib, "ipsearcher.lib" )

extern "C"
{

BOOL CDECL LookupAddress( PCSTR, PSTR* );

PSTR* CDECL _GetAddress( PCSTR );

};

#endif // IPSEARCHER_H

大功告成!这样我们就为这个光秃秃的ipsearcher.dll做了一份SDK开发包,而不必再使用动态加载的方法了。

总结一下再

其实,探究一个DLL并非像我这里所讲述的这么简单。这项工作很可能需要阅读大量的汇编代码,了解DLL函数体的流程才能使真相大白于天下。另外,还不能排除有的DLL被加密、加壳、反跟踪……也就是说对于ipsearcher.dll,那简直就是我捡了个便宜来借花献佛了。

点这里下载ipsearcher SDK

转载于:https://www.cnblogs.com/flying_bat/archive/2006/09/27/516416.html

调用未知DLL中的导出函数相关推荐

  1. 10.4.4 使用ctypes调用kernel32.dll中的函数

    10.4.4 使用ctypes调用kernel32.dll中的函数 2007-10-17 14:41 孙广磊 人民邮电出版社 字号:T | T 综合评级: 想读(5)  在读(0)  已读(6)   ...

  2. C#委托实现C++ Dll中的回调函数

    from:https://blog.csdn.net/ferrycooper/article/details/63261771 很多的Dll都是C和C++写的,那么如果C#想要调用Dll中的函数怎么办 ...

  3. Qt调用dll中的功能函数

    http://www.cnblogs.com/hicjiajia/archive/2010/08/27/1810239.html 声明: 事先我已经自己动手写了一个简单的dll文件(myDLL.dll ...

  4. 【转】Qt调用dll中的功能函数

    DLL 优点 ------------------------------------- 1.扩展了应用程序的特性: 2.可以用许多种编程语言来编写: 3.简化了软件项目的管理: 4.有助于节省内存: ...

  5. 动态链接库DLL创建及导出函数、调用DLL

    1.动态链接库(dll)概述 没接触dll之前觉得它很神秘,就像是一个黑盒子,既不能直接运行,也不能接收消息.它们是一些独立的文件,其中包含能被可执行程序或其他dll调用来完成某项工作的函数,只有在其 ...

  6. html 调用c#dll中的控件,C#调用ActiveX控件的方法

    C#调用ActiveX控件时通常要进行两步操作: 第一步:注册控件,例如ABE.DLL,将ABE.DLL拷贝至系统文件夹SYSTEM32下,然后在开始中执行CMD命令,regsvr32 ABE.DLL ...

  7. 调用外部 DLL 中的函数(显示调用)

    unit Unit1;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Di ...

  8. 查看dll出的导出函数

    一般项目中调用其他人封装的dll, 如果在确认函数调用及参数无问题的前提下,可以尝试看下dll中是否包含该导出函数,具体查看步骤如下: 1. 点击搜索vs, 打开VS 2017的x64_x86交叉工具 ...

  9. c#调用c++dll接口及回调函数

    在合作开发时,C#时常需要调用C++DLL,当传递参数时时常遇到问题,尤其是传递和返回字符串是,现总结一下,分享给大家: VC++中主要字符串类型为:LPSTR,LPCSTR, LPCTSTR, st ...

最新文章

  1. 爬取京东淘宝商品销量并可视化处理数据
  2. HanLPTokenizer HanLP分词器
  3. Cppunit 源码 SynchronizedObject
  4. split出现得问题
  5. Ubuntu安装Python的相对简单方法
  6. C# async await 例子
  7. 【java】中缀表达式转后缀表达式 java实现
  8. android百度在线音乐api,百度音乐免费API接口
  9. LaTeX数学公式环境
  10. 高性能服务器天梯,2019服务器CPU天梯图 多路CPU性能排名
  11. 专业人员选择关键词的标准和原则
  12. 有没有测试牙齿需不需要修正的软件,小虎正畸:测一测你到底需不需要进行牙齿矫正?...
  13. linux分配设备编号
  14. please configure web facet first
  15. 蒋志平从零到亿的创业路
  16. 解决Android模拟器不显示问题
  17. 绘图杂记【3】Python / R语言 制作峰峦图
  18. 教你如何将朋友圈的视频发到自己的朋友圈
  19. 通达信指标公式编写常用函数(三)——HHVBARS、LLVBARS
  20. c语言字符超过127,在C中处理超过127的char值

热门文章

  1. rollup学习小记
  2. Leetcode之javascript解题(No33-34)
  3. iis下php 500错误
  4. Flask学习 一 基本结构
  5. Cocos2d-x 3.0 简捷的物理引擎
  6. 老web换新枝----Sails.js移动设备的全新生产力(五)
  7. 程序员容易不能生育?
  8. 图灵社区 : 阅读 : 谁说Vim不是IDE?(三)
  9. 认识数据分析_认识您的最佳探索数据分析新朋友
  10. 熊猫分发_熊猫新手:第二部分