Hacking Diablo II之完整性检查(Integrity Scan)

d2hackmap有一个完整性检查的功能(Integrity Scan),用来检查游戏进程的代码有没有被改过。这个功能在d2hackmap的“安全开地图”中有所应用。所谓的“安全开地图”,其原理大致是在游戏进程分配一块空间,把“开地图”的相关代码(不是一个完整的DLL模块)注入这块空间,这段代码会在游戏的主线程context下运行,调用游戏的内部函数实现“开地图”逻辑,完事儿后再释放分配的空间。这个过程时间很短,也不需要修改游戏进程的代码,因此安全性比较高,不容易被warden抓到。下图是“安全开地图”代码运行前的警告:

Hacking Diablo II之完整性检查(Integrity Scan)

“开地图”(Reveal Map)的代码逻辑大致如下,红色代码行调用了游戏内部函数:

void __stdcallRemoteRevealAutomapAct(RevealMapContext * pctx)
{
AutomapLayer2
* pLayer;
UnitAny
* unit = * pctx -> p_D2CLIENT_PlayerUnit;
if ( ! unit || ! unit -> pPos -> pRoom1) return ;
DWORDcurrlvl
= unit -> pPos -> pRoom1 -> pRoom2 -> pDrlgLevel -> nLevelNo;
DWORDact
= 0 ;
BYTEactlvls[]
= { 1 , 40 , 75 , 103 , 109 , 133 , 134 , 135 , 136 , 137 };
do {} while (currlvl >= actlvls[ ++ act]);
DWORDlvl
= currlvl;
for (lvl = actlvls[act - 1 ];lvl < actlvls[act];lvl ++ ){
DrlgLevel
* pDrlgLevel = pctx -> GetDrlgLevel(( * pctx -> p_D2CLIENT_pDrlgAct) -> pDrlgMisc,lvl);
if ( ! pDrlgLevel)
pDrlgLevel
=pctx->D2COMMON_GetDrlgLevel((*pctx->p_D2CLIENT_pDrlgAct)-> pDrlgMisc,lvl);
if ( ! pDrlgLevel -> pRoom2First){
pctx
-> D2COMMON_InitDrlgLevel(pDrlgLevel);
}
pLayer
=pctx->D2COMMON_GetDrlgLayer(lvl);
pctx->InitAutomapLayer(pLayer->nLayerNo,(DWORD)pctx->D2CLIENT_InitAutomapLayer_I);
pctx->
RevealAutomapLevel(pctx,pDrlgLevel);
}
pLayer
=pctx->D2COMMON_GetDrlgLayer(currlvl);
pctx->InitAutomapLayer(pLayer->nLayerNo,(DWORD)pctx->
D2CLIENT_InitAutomapLayer_I);
}

由于“开地图”需要调用到游戏的内部函数,这给warden检测留下了一点可乘之机:如果warden截获了这几个内部函数中的一个,在调用发生时检查调用者的身份(通过分析函数返回地址得到调用模块信息),就可抓住外挂。在d2hackmap中,为了对付warden的这种检测,“安全开地图”代码在执行前,d2hackmap会对游戏进程做完整性检查,也就是检查游戏进程的代码有没有被改过。这篇文章讲讲“完整性检查”的实现。
首先要明白的是这里说的“完整性检查”主要指的是检查代码的完整性。一个可执行程序的构成,大约可分为文件头、代码段和数据段几部分。程序的代码在运行时不会改变,一般装载在只读内存页面,数据段又可分为只读数据和可读写数据两部分。可读写数据装载在读写内存页面,从通用的角度来说,这部分数据是没法做完整性检查的。d2hackmap的完整性检查功能查的是可执行模块(exe、dll)的只读内存页面,包括代码段和只读数据段。
一个windows的进程加载几十个DLL是很常见的,加上EXE主程序模块,完整性检查需要检测的数据大小一般在几兆到几十兆字节之间。对于这样的数据量,一个好的检测算法是很必要的。d2hackmap使用的策略是,对于每一个待扫描的模块,构建出相应的“干净”模块,然后拿两个模块逐字节比较。在 x86下,内存比较有专用、高效的汇编指令cmpsd和cmpsb。

DWORD_declspec(naked)__fastcallmymemcmpd(DWORDnSize, void * pleft, void * pright)
{
__asm
{
pushesi;
pushedi;
shrecx,
2 ;
moveax,edx;
movesi,edx;
// pleft
movedi,[esp + 0x0c ]; // pright
repcmpsd;
subeax,esi;
negeax;
popedi;
popesi;
ret
4 ;
}
}

DWORD_declspec(naked)__fastcallmymemcmpb(DWORDnSize,LPBYTEpleft,LPBYTEpright)
{
__asm
{
pushesi;
pushedi;
moveax,edx;
movesi,edx;
movedi,[esp
+ 0x0c ];
repcmpsb;
testecx,ecx;
jznotfound;
subeax,esi;
noteax;
popedi;
popesi;
ret
4 ;
notfound:
xoreax,eax;
popedi;
popesi;
ret
4 ;
}
}

现在问题的关键是如何构建一个“干净”的模块,这跟黑客的反击中一文中提到的“模块重建”是非常相似的,唯一的区别在于“模块重建”的代码运行在游戏进程中,和目标模块在同一个内存空间。

构建一个“干净”模块的算法步骤和手工加载DLL的步骤是比较类似的,描述如下:
1,把目标模块的数据完整复制一份到本地进程空间(ReadProcessMemory),以下称为“脏”模块;
2,分配一块空间以存放“干净”模块。
3,把目标模块的磁盘文件映射到本地进程空间(CreateFile/CreateFileMapping/MapViewOfFile),以下称为磁盘文件映象;
4,把“脏”模块数据再复制到“干净”模块空间(memcpy)-这样保证了可写数据段是相同的;
5,把磁盘文件映象的可执行文件头(PE header)复制到“干净”模块(memcpy)-pe header需要检测;
6,分析pe header,把磁盘文件映象中的只读section逐一复制到“干净”模块-只读section需要检测;
7,接下来对“干净”模块做进一步的修正(fix-up),包括导入表(IAT)和重定位表(relocation table);
8,IAT的修正稍微有点儿繁琐,也和普通的加载DLL不同,主要的问题是同一个DLL,在本地加载和在游戏进程加载的基地执有可能是不一样的。对于IAT中链接到的DLL,修正时应该以该DLL在目标游戏进程中加载的基地址为基准;
9,重定位表的修正也类似,应该使用“脏”模块的重定位数据-这和普通的加载DLL也不同。
经过这几步以后,“干净”模块就构建好了。接下来的完整性检查用前面给出的mymemcmpd和mymemcmpb函数就行了。使用这种方法,完整性检查的效率还是比较高的,一般情况下扫描一个进程的时间在几秒钟(<5秒)以内。下图是d2hackmap插件(d2hackmap.dll)注入后对游戏进程的完整性检查的结果,可以看到d2hackmap.dll修改了很多处,视图中的每一项列出了被修改的dll名称(入d2win.dll),修改地址,修改长度,修改后的指令,如是跳转指令,还给出了跳转模块的名称(如图中都是d2hackmap.dll,根据这点我们就可以判断出该处是被 d2hackmap.dll修改的)。
Hacking Diablo II之完整性检查(Integrity Scan)
完整性检查还可以有很多其他用途,不仅仅限于游戏外挂方面。比如说有些流氓软件可能会在一些敏感进程中截获某些API来监控用户的行为,完整性检查可以把它检测出来。另外,完整性检查还可以用来分析那些依赖于代码截获技术的程序,比如说你想分析D2JSP.DLL的实现技术,那么通过观测它的截获点,以截获点为起点进行逆向分析是一种很有效的方法。下图是D2JSP加载后的完整性检查结果:

Hacking Diablo II之完整性检查(Integrity Scan)

你可能感兴趣的:(游戏,windows,算法,UP)