24小时热门版块排行榜    

查看: 336  |  回复: 0
当前主题已经存档。

sdlj8051

金虫 (著名写手)

[交流] [转贴]DLL输出类使用研究手记

在写一个程序时,我想使用一个共享软件中的C++类。该类名为Crypt,封装在一个DLL中,文件名为Crypt.dll。通过SoftIce和IDA Pro,我已基本弄清了其成员函数的用法。现在的问题是,没有相应的.H文件及.LIB文件(当然更没有源码)。另外,其成员函数显然不能以GetProcAddress取得地址后直接调用。

该软件是用Borland C++写的。

先用Borland C++提供的工具获取必要的文件

C:\bc5\bin\impdef  Crypt.def  Crypt.dll//得到Crypt.def文件
C:\bc5\bin\implib  Crypt.lib  Crypt.dll//得到Crypt.lib文件

Crypt.def中的相关内容如下:

@Crypt@$bctr$qpxc@1; Crypt::Crypt(const char*)
@Crypt@$bctr$qpxuci@2; Crypt::Crypt(const unsigned char*,int)
@Crypt@DecodeFrom$qpuct1l @3; Crypt:ecodeFrom(unsigned char*,unsigned
                          char*,long)
@Crypt@EncodeTo$qpuct1l @4; Crypt::EncodeTo(unsigned char*,unsigned
                         char*,long)

从分号后的注释,可以得到Demangled后的成员函数原型,但是没有类的定义,我们不知道这个类包含什么数据成员(以及别的未exported的成员函数,这一点不重要,因为原来的编程者在使用这个封装在DLL中的类时,也只能使用exported的函数)。如何构造一个正确的头文件?先来看看原来的代码是如何使用这个类的。以下为IDA Pro的输出:

00453E3E 0F0push   8
00453E40 0F4lea    eax, [ebp+var_8]
00453E43 0F4push   eax
00453E44 0F8lea    ecx, [ebp+var_E0]
00453E4A 0F8push   ecx
00453E4B 0FCcall   Crypt::Crypt(uchar *,int)
00453E50 0FCadd    esp, 0Ch

这段代码调用Crypt::Crypt(uchar *,int)成员函数,使用__cdecl调用规则,由调用者维护堆栈。函数有2个参数,向堆栈中压入了3个值,最后一个push入栈的是指向当前Crypt对象的this指针,即变量var_E0就是在栈上分配的Crypt类对象。从IDA Pro中可看到,var_E0覆盖了从FFFFFF20到FFFFFF80共96字节的空间。我们知道,C++类的成员函数、静态数据成员是不放在对象内的,对象只含有数据成员(若类中或其基类中定义有虚函数,还包含vptr)。也就是说,Crypt类的所有数据成员共占据96字节。具体细节请参照Stanley Lippman的《深度探索C++对象模型》。

由此,我们可以自己定义Crypt类的数据成员(使用字节数组),使其占据同样的内存空间,与原来的类在内存布局上一致即可。实际上,只要我们给出的类定义保证能分配足够的内存空间,原来的构造函数就可以在分配的内存中创建出正确的对象。这种方法与COM的思想有相似之处,都是在二进制的级别上保证内存布局的兼容。我写的头文件如下:

class _import Crypt
{
public:
  Crypt(const char* lpszPassword);
  Crypt(const unsigned char* lpszPassword,int cbBuffer);
  EncodeTo(unsigned char* lpSource,unsigned char* lpDestination,int nSize);
  DecodeFrom(unsigned char* lpSource,unsigned char* lpDestination,int nSize);

public:
  char dummy[96];   //Bingo!:-)
};

将此头文件及前面的Crypt.lib文件加入项目,证明此方法是可行的。测试代码如下:

 Crypt obj("123456";
 obj.EncodeTo(lpData,lpData,nSize);

以上的尝试都是在Borland C++下做的,与原来的程序具有同样的环境,如果想在Visual C++下使用该类又如何实现?从DLL中输出类的技术细节是因编译器厂商而异的,显然不能再如法炮制(VC++甚至不能识别用implib生成的Crypt.lib文件)。我们可以变通一下,自己用VC++写一个Crypt.dll,包裹在原来的DLL外面,输出与原有DLL相同名字和序号的函数,用VC++写的客户程序使用这个Wrapper DLL,由其再去调用原来的DLL。这种编写一个包在原有DLL外面的动态链接库的方法,相关资料很多,这里不再详细解释。

将原来的DLL改名为OldDll.dll。源代码如下:

头文件Crypt.h:

#ifdef CRYPT_EXPORTS
#define CRYPT_API __declspec(dllexport)
#else
#define CRYPT_API __declspec(dllimport)
#endif

class CRYPT_API Crypt {

public:
 Crypt& operator=(const Encrypt& rhs);//赋值运算符,禁止
 Crypt(const Encrypt& rhs);//拷贝构造函数,禁止

public:
 Crypt(const char* lpszPassword);
 Crypt(const unsigned char* lpszPassword,int cbBuffer);
 void __cdecl EncodeTo(unsigned char* lpSource,unsigned char*
            lpDestination,int nSize);
 void __cdecl DecodeFrom(unsigned char* lpSource,unsigned char*  
             lpDestination,int nSize);

public:
char dummy[96];   //Bingo!:-)
};

实现文件Crypt.cpp:

#include "stdafx.h"
#include "Crypt.h"

static HINSTANCE hOldDll=NULL;

static DWORD dwRet;
static DWORD dwRetAddr;

static FARPROC lpCrypt1;//带1个参数的构造函数
static FARPROC lpCrypt2;//带2个参数的构造函数
static FARPROC lpEncodeTo;
static FARPROC lpDecodeFrom;

BOOL APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved )
{
 BOOL bRet=false;

 switch (ul_reason_for_call)
 {
  case DLL_PROCESS_ATTACH:

    //加载原来的DLL,获取原函数地址

    hOldDll=LoadLibrary("c:\\test\\OldDll.dll";
    if(hOldDll)
    {
      lpCrypt1=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x1));
      lpCrypt2=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x2));
      lpEncodetTo=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x3));
      lpDecodeFrom=::GetProcAddress(hOldDll,MAKEINTRESOURCE(0x4));

      bRet=true;
    }

    break;

   case DLL_THREAD_ATTACH:
    break;

   case DLL_THREAD_DETACH:
    break;

   case DLL_PROCESS_DETACH:
    if(hOldDll)
    {
     ::FreeLibrary(hOldDll);
    }
    break;
  }
  
  return bRet;
}

__declspec(naked) Crypt::Crypt(const char *lpszPassword)
{
  //手工模仿__cdel调用规则

  _asm
  {
    pop eax//弹出并保存返回地址
    mov dwRetAddr,eax

    push ecx//压this指针入栈

    call lpCrypt1 //调用原函数

    mov dwRet,eax //保存调用返回值

    mov eax,dwRetAddr
    push eax//重新压返回地址入栈

    mov eax,dwRet //恢复调用返回值

    ret 8 //返回,丢弃2个dword(参数和this指针)
  }
}

__declspec(naked) Crypt::Crypt(const unsigned char* lpszPassword,int cbBuffer)
{
  //手工模仿__cdel调用规则

  _asm
  {
    pop eax
    mov dwRetAddr,eax

    push ecx

    call lpCrypt2

    mov dwRet,eax

    mov eax,dwRetAddr
    push eax
    mov eax,dwRet
    ret 0xC    //丢弃3个dword
  }
}

void __declspec(naked) __cdecl Crypt::EncodeTo(unsigned char* lpSource,
unsigned char* lpDestination,int nSize)
{
  //直接跳转到原函数

  _asm jmp far dword ptr lpEncodeTo
}

void __declspec(naked) __cdecl Crypt:ecodeFrom(unsigned char* lpSource,
unsigned char* lpDestination,int nSize)
{
  //直接跳转到原函数

  _asm jmp far dword ptr lpDecodeFrom
}

有几处需要注意:首先,这里使用了naked调用规则(Borland C++不支持),以便于直接操作堆栈及用内嵌的汇编语言编程。另外,虽然我们的类中并没有包含虚函数或对象成员,VC++编译器却仍生成了member-wise的拷贝构造函数和bit-wise赋值运算符,并导致原来的DLL中的构造函数不能正确创建对象。为了禁止编译器自动生成不必要的代码,在头文件中定义了赋值运算符和拷贝构造函数,但并未提供实现。两个构造函数有点特殊,我发现无论指定何种调用规则,生成的代码总是使用thiscall调用规则,即在ecx寄存器中传递this指针,为此构造函数需要特殊处理,用汇编代码手工模仿__cdecl调用规则去调用原来DLL中的函数,包括维护栈指针。

其余的代码已作了注释,易于理解,不再赘述。

[ Last edited by sdlj8051 on 2006-10-6 at 12:35 ]
回复此楼
已阅   回复此楼   关注TA 给TA发消息 送TA红花 TA的回帖
相关版块跳转 我要订阅楼主 sdlj8051 的主题更新
普通表情 高级回复 (可上传附件)
最具人气热帖推荐 [查看全部] 作者 回/看 最后发表
[基金申请] 投票:  有多少人是今天查系统知道结果的? +17 爱看书的可乐 2026-08-26 19/950 2026-08-30 22:24 by xiangy672
[基金申请] 国社科又开始会评了,不知道这次命运如何 +5 雨打竹帘 2026-08-30 9/450 2026-08-30 22:23 by 余韵清
[论文投稿] 售SCI一区T0P文章,我:8O.55.1.O.5.4,科目齐全,可+急 +3 jCd0dEvKHShX 2026-08-29 4/200 2026-08-30 19:57 by hZiFeudZyoGR
[考研] 售SCI一区文章,我:8.O.55.1.O.54,科目齐全,可伽急 +5 G6APbkg8SA6w 2026-08-29 6/300 2026-08-30 19:02 by hZiFeudZyoGR
[硕博家园] 售SCI一区T0P文章,我:8O.55.1.O.54,科目全,可伽急 +4 G6APbkg8SA6w 2026-08-29 5/250 2026-08-30 18:40 by hZiFeudZyoGR
[博后之家] 售一区SCI文章T0P,我:8O.551.O54,科目全,可十急 +3 gy1nBQXYQJqL 2026-08-29 4/200 2026-08-30 18:28 by hZiFeudZyoGR
[考博] 售SCI-T0P文章,我:8O.5.5.1.O.54,科目齐全,可+急 +4 gy1nBQXYQJqL 2026-08-29 6/300 2026-08-30 18:20 by hZiFeudZyoGR
[考博] 售SCI一区T0P文章,我:8.O.55.1.O54,科目全,可伽急 +7 ASdOkHsho7FD 2026-08-28 11/550 2026-08-30 17:34 by hZiFeudZyoGR
[硕博家园] 售SCI一区T0P文章,我:8.O55.1.O.54,科目全,可十急 +7 ASdOkHsho7FD 2026-08-28 14/700 2026-08-30 17:01 by hZiFeudZyoGR
[基金申请] 面上意见出来了 +9 黄鸟于飞Chao 2026-08-29 18/900 2026-08-30 16:47 by 黄鸟于飞Chao
[公派出国] 售SCI一区T0P文章,我:8O.55.1.O.54,科目全,可伽急 +3 jCd0dEvKHShX 2026-08-29 5/250 2026-08-30 14:03 by GkCVyODlx4mR
[找工作] 售SCI文章,我:8O5.5.1.O.54,科目齐全,可+急 +5 gy1nBQXYQJqL 2026-08-29 8/400 2026-08-30 12:14 by l0VvVHGBGRLv
[基金申请] 麻烦专家们看看评委们的意见(F口面上) +6 gdd2018 2026-08-28 11/550 2026-08-30 08:58 by 超级无敌华子
[基金申请] 2026年叶企孙基金 +4 bud_bud 2026-08-27 7/350 2026-08-29 07:23 by foolishmani
[基金申请] 基金不中,共勉 +11 eulota 2026-08-26 11/550 2026-08-28 14:22 by 火星超人xi
[基金申请] 申请删除本帖 +6 lyz123lyz 2026-08-27 7/350 2026-08-27 17:31 by 宁静致远sy
[基金申请] 基金未中,这种答复是模板吗? +5 zhaosm1982 2026-08-27 6/300 2026-08-27 16:00 by lfy8008
[基金申请] 为什么国自然不能直接公布 +4 bjdxyxy 2026-08-26 4/200 2026-08-26 13:12 by qingmu1201
[基金申请] 国合里面能看到了 +7 一怀馨秋 2026-08-26 7/350 2026-08-26 11:23 by zhaosm1982
[基金申请] 国合可查了 +3 paperzjh 2026-08-26 3/150 2026-08-26 10:41 by LemmonTr
信息提示
请填处理意见