真是晕啊,这篇文章写到一半的时候竟然有人说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)
{