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

  • 时间:
  • 浏览:9

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

  

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

  void?__stdcall?RemoteRevealAutomapAct(RevealMapContext?*pctx)

  {

  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?

  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)?__fastcall?mymemcmpd(DWORD?nSize,?void*?pleft,?void*?pright)

  {

  __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加载后的完整性检查结果: