系统自带扑克牌资源动态链接库cards.dll逆向分析笔记

本文介绍了对Windows XP系统自带的扑克牌资源动态链接库cards.dll进行逆向分析的过程,重点分析了其中的导出函数,包括cdtInit、cdtTerm和cdtDrawExt。通过逆向工程,揭示了这些函数的功能和使用方法,为理解和利用cards.dll提供了指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

真是晕啊,这篇文章写到一半的时候竟然有人说cards.dll的源代码可以在MSDN Library中找到,我看了一下本机的MSDN,反正我是没找到,有谁知道在哪找的麻烦指点一下,哎,害的我后面写那么一大堆废话来凑篇幅

Windows XP 系统自带扑克牌资源动态链接库cards.dll逆向分析笔记


使用工具:IDA Pro, Resource Hacker


0. 前言

  cards.dll是Windows系统目录下的一个动态链接库,主要提供扑克牌图像及相关操作等资源,以供
Windows附带的扑克游戏程序(如纸牌、红心大战等)使用。

  我们希望知道cards.dll具体提供了哪些东西,可供自由编程所用。


1. 反编译


  一般而言,将原始二进制文件还原成高级语言源文件的逆向工程有两个步骤:一是反编译,根据目
标文件反汇编的内容,识别指令、存储单元等基本要素并理解这些要素之间的相互关系,从而写出相应
的高级语言语句;二是自文档化,根据程序的上下文,理解程序中各个变量所代表的含义,给它们重新
赋予意义明了的名称,必要时添加一定的注释,以使得源程序具有一定的文档性,容易看懂和理解。

  由于这里是人工逆向,这两个步骤分得并不清楚,实际上也难以分清楚。毕竟,原始的反汇编文本
中充斥着大量形如dword_70142004以及var_8之类缺乏意义的符号,不同变量的名字看上去都差不多,
如果不及时给它们起个容易分辨的名称,就很容易弄错。这样看来,上述的第二步就部分地提前了。同
时,为了简洁起见,在下面的正文中也不打算贴那种大片的反汇编文本(事实上也不必要,若要看这些
内容,自己用IDA打开这个DLL文件就能看到了),只提供还原出来的源代码,源代码中的变量都已赋予
合适的名称。这就是说,反编译的过程在正文中也省略了。

  但毕竟反编译是本文的一个重要组成部分,不能将它真正省略掉。由于要说清楚这个过程势必又要
附上大量的反汇编文本,因此,关于反编译的一般原则(如识别高级语言的控制结构等内容)我会以附
录形式另起一章,而具体的反汇编过程则把它放到附件里了,附件里的origdasm.asm是最初的没有经过
任何分析的反汇编文本,而annodasm.asm是分析过后的文本,我的分析主要以注释形式放在其中,大家
可以对照着看。

  IDA加载后切换到Exports选项卡,可以看到7个导出函数:

    WEP
      cdtAnimate
      cdtDraw
      cdtDrawExt
      cdtInit
      cdtTerm
      DllEntryPoint

其中DllEntryPoint就是通常所谓的DllMain。其代码如下:


 

代码:

  HINSTANCE hDllModule;    //全局变量
  BOOL DllMain (HINSTANCE hInstDll, DWORD dwReason, LPVOID lpReserved)
  {
      hDllModule = hInstDll;    
      return TRUE;
  } 


WEP和cdtAnimate这两个函数都极其简单:

 

代码:

 BOOL __stdcall WEP (int Arg1)
  {
      return TRUE;
  }

  BOOL __stdcall cdtAnimate (int Arg1, int Arg2, int Arg3, int Arg4, int Arg5)
  {
      return TRUE;
  }

返回值(eax的出口值)只有0和1两种可能的函数,我们认为它是BOOL类型的。如此简单的函数为什么
还要导出呢?实际上,大体可以推测这些函数在前一个版本的操作系统中曾经是有用的,但是后来由于
种种原因,这些函数被废弃了,函数体的代码已基本被删掉,成为我们现在看到的空壳子。

  所有导出函数中最复杂的莫过于cdtDrawExt,这是一个绘画扑克牌的函数。我们还是先从比较简单
的cdtInit和cdtTerm两个函数入手,避免一开始就啃硬骨头。

  cdtInit函数中包含一个GetObject函数的调用,这是逆向cdtInit的突破口。弄清了这个函数调用
的作用后,其他就简单了。


 

代码:

//全局变量
  int nNormalWidth;         //正常大小下卡片的宽 
  int nNormalHeight;        //正常大小下卡片的长
  int nInitCount = 0;       //“初始化计数”,每调用cdtInit一次增加1,
                            //每调用cdtTerm一次减少1
  HBITMAP hPlainBack = NULL;    //平实型背面的位图句柄
  HBITMAP hCrossBack = NULL;    //斜十字图案型背面的位图句柄
  HBITMAP hCircleBack = NULL;   //圆圈图案型背面的位图句柄
//函数定义体  
  BOOL __stdcall cdtInit (LPINT lpNormalWidth, LPINT lpNormalHeight)
  {
      BITMAP loc_stBM;

      if (nInitCount++ == 0)
      {
           hPlainBack = LoadBitmap (hDllModule, 53);
           hCrossBack = LoadBitmap (hDllModule, 67);
           hCircleBack = LoadBitmap (hDllModule, 68);
           
           if (hPlainBack == NULL || hCrossBack == NULL || hCircleBack == NULL)
           {
                MyDeleteHbm (hPlainBack);
                MyDeleteHbm (hCrossBack);
                MyDeleteHbm (hCircleBack);
                return FALSE;
           }

           GetObject (hPlainBack, sizeof(BITMAP), &loc_stBM);

           nNormalWidth = *lpNormalWidth = loc_stBM.bmWidth;
           nNormalHeight = *lpNormalHeight = loc_stBM.bmHeight;
      }else{
           *lpNormalWidth = nNormalWidth;
           *lpNormalHeight = nNormalHeight;
      }
      return TRUE;
  }


由此可见,cdtInit首次被调用时会装入3种样式的卡片背面位图,并且获得和保存这些位图的尺寸。如
果装入位图失败,则返回值是FALSE,应用程序就不能使用这些风格的背面。而往后该函数若再次被调
用时(这时nInitCount > 0),则不做任何事情,只是简单地把首次调用时保存下来的卡片尺寸返回。
由于cdtInit的两个参数是指针类型,而且函数体内对其所指向的对象进行写操作,所以实参不能是空
指针。正常的用法是,在应用程序中定义两个全局变量并将它们的地址作为实参传递给cdtInit函数,
如此就可以在应用程序模块和库模块中各自保存一份卡片正常尺寸的数据信息。

  cdtInit中还调用了一个叫MyDeleteHbm的函数,其代码如下:


 

代码:

  void __stdcall MyDeleteHbm (HBITMAP hBM)
  {
      if (hBM)
         DeleteObject (hBM);
      return;
  }


实际上这个函数可以认为就是DeleteObject,只不过显得更加保险点而已。

  cdtTerm的代码也很简单:


 

代码:

//全局变量
  HBITMAP hCards[52];   //一副扑克牌的位图句柄
  HBITMAP hCustomBack;  //自选的卡片背面的位图句柄
//函数定义体
  void cdtTerm (void)
  {
      int loc_i;

      if (--nInitCount <= 0)
      {
          for (loc_i = 0; loc_i < 52; loc_i++)
              MyDeleteHbm (hCards[loc_i]);

          MyDeleteHbm (hPlainBack);
          MyDeleteHbm (hCustomBack);
          MyDeleteHbm (hCrossBack);
          MyDeleteHbm (hCircleBack);
      }
      return;
  }


显然,这个函数的主要功能是用来做些善后工作,但当“初始化计数”尚大于1时则什么也不做。在IDA
中查看变量nInitCount的引用参考可以发现,只有上述的cdtInit和cdtTerm两个函数引用到它,前者将
其加一而后者将其减一。因此程序中cdtInit和cdtTerm必须成对使用,才能保证最后一次调用cdtTerm
时善后工作得以完成,就如同熟知的LoadLibrary/FreeLibrary那样。

  接下来是主菜cdtDrawExt登场了。在正式给出其源代码之前罗嗦几句。跟踪这个函数之难,不
惟在于其规模的庞大,同时亦在于其反汇编代码的若干不规范之处,例如它中途将栈上原本存放形参的
存储空间挪去做了局部变量的用途,而且这个局部变量跟被替代的形参还不是同一类型。我开始就是忽
略了这一点,结果错认了参数的类型,不得不从头开始。由于代码优化等原因,寄存器被频繁使用,当
一个寄存器所寄存的不同对象多起来的时候,要理出头绪就不那么容易了。虽然从原则上说,非出口的
形参在子程序中不应该被改写,而所有的寄存器到了最后都可以被消去(见附录),但这些工作毕竟很
难一步到位。于是,除了子程序原有的局部变量外,我们还不得不创建一些临时变量,以代表这些形参
和寄存器参与源代码的构建,到最后阶段再设法消去它们。在本函数建议用一个双字变量来代表寄存器
eax,用两个双字变量代表Arg3(第三个参数,即原始反汇编文本中的arg_8)。

  逆向这个函数的突破口是其中一连串的PatBlt函数调用,根据它的形参类型可以确定cdtDrawExt大
部分形参的类型和意义,剩下的可以连猜带蒙弄出来。只要知道参数的意义是什么,其他就容易了。

  下面是这个函数的代码:


 

代码:

  BOOL __stdcall cdtDrawExt (HDC hDC,                //将要绘画的目标设备环境句柄
                   int nXDest,             //nXDest和nYDest两个参数表示一个点的x和y坐标
                   int nYDest,             //以该点为左上角绘画卡片
          
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值