Hacking Diablo II之完整性检查(Integrity Scan)
Posted on 2008-10-16 11:07 sting feng 阅读(706) 评论(0) 编辑 收藏 举报d2hackmap有一个完整性检查的功能(Integrity Scan),用来检查游戏进程的代码有没有被改过。这个功能在d2hackmap的“安全开地图”中有所应用。所谓的“安全开地图”,其原理大致是在游戏 进程分配一块空间,把“开地图”的相关代码(不是一个完整的DLL模块)注入这块空间,这段代码会在游戏的主线程context下运行,调用游戏的内部函 数实现“开地图”逻辑,完事儿后再释放分配的空间。这个过程时间很短,也不需要修改游戏进程的代码,因此安全性比较高,不容易被warden抓到。下图是 “安全开地图”代码运行前的警告:
“开地图”(Reveal Map)的代码逻辑大致如下,红色代码行调用了游戏内部函数:
{
AutomapLayer2 *pLayer;
UnitAny* unit = *pctx->p_D2CLIENT_PlayerUnit;
if (!unit || !unit->pPos->pRoom1) return;
DWORD currlvl = unit->pPos->pRoom1->pRoom2->pDrlgLevel->nLevelNo;
DWORD act = 0;
BYTE actlvls[] = {1, 40, 75, 103, 109, 133, 134, 135, 136, 137};
do {} while (currlvl >= actlvls[++act]);
DWORD lvl = 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。
{
__asm
{
push esi;
push edi;
shr ecx, 2;
mov eax, edx;
mov esi, edx; // pleft
mov edi, [esp+0x0c]; // pright
rep cmpsd;
sub eax, esi;
neg eax;
pop edi;
pop esi;
ret 4;
}
}
DWORD _declspec(naked) __fastcall mymemcmpb(DWORD nSize, LPBYTE pleft, LPBYTE pright)
{
__asm
{
push esi;
push edi;
mov eax, edx;
mov esi, edx;
mov edi, [esp+0x0c];
rep cmpsb;
test ecx, ecx;
jz notfound;
sub eax, esi;
not eax;
pop edi;
pop esi;
ret 4;
notfound:
xor eax, eax;
pop edi;
pop esi;
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修改的)。
完 整性检查还可以有很多其他用途,不仅仅限于游戏外挂方面。比如说有些流氓软件可能会在一些敏感进程中截获某些API来监控用户的行为,完整性检查可以把它 检测出来。另外,完整性检查还可以用来分析那些依赖于代码截获技术的程序,比如说你想分析D2JSP.DLL的实现技术,那么通过观测它的截获点,以截获 点为起点进行逆向分析是一种很有效的方法。下图是D2JSP加载后的完整性检查结果: