读书频道 > 网站 > 网页设计 > 游戏外挂攻防艺术
1.4.3 游戏内存对象布局
13-02-26    奋斗的小年轻
收藏    我要投稿   

本文所属图书 > 游戏外挂攻防艺术

随着网络的普及,网络游戏得到了众多网民的青睐。但是,网络游戏的盛行,也给游戏玩家和游戏公司带来了很多安全问题,如木马盗号、外挂作弊等。对于正常的游戏玩家和游戏公司来说,外挂的危害尤其突出。因为一款...立即去当当网订购

游戏中的各种对象,如玩家、怪物、武器、装备、精灵等,都以内存块的形式存在。其中,有些以结构体形式组织成块,负责保存对象的状态;有些以类的实例形式组织成块,在保存对象的同时提供对象的行为。所以,定位对象内存块的意义重大,不仅可以窥视对象的实例变量的内存布局,而且可以了解对象的行为。另外,修改玩家或怪物的HP/MP和速度、修改武器和装备的属性、召唤精灵等外挂行为,都需要定位对象的内存块。

本节将重点讨论如何定位对象内存块,以及常见对象内存块的内存布局和组织方式。

让我们从一个简化的CPLAYER类开始。
// CPLAYER类的定义
 class CPLAYER
{
public:
 CPLAYER (DWORD dwHP, DWORD dwVector)
{ m_dwHP = dwHp; m_dwVector = dwVector;} // CPLAYER类的构造函数
 virtual DWORD GetVector(){return m_dwVector;} // 获取速度
virtual void SetVector(DWORD dwVector){m_dwVector = dwVector;}
    // 设置速度
virtual DWORD GetHP(){return m_dwHP;}    // 获取血量
virtual void SetHP(DWORD dwHP){m_dwHP = dwHP;} // 设置血量
private:
 DWORD m_dwHP;   // 血量
 DWORD m_dwVector;  // 速度
 
};

上面的CPLAYER类比较简单:1个CPLAYER() 构造函数用于初始化血量及速度,4个虚函数用于获取和设置血量及速度。接下来,我们新建一个CPLAYER对象,将它拖到IDA里,分析一下CPLAYER对象的内存布局,如图1-19所示。


 

可以看到,“new”操作符后面紧跟着就是调用CPLAYER对象的构造函数sub_401000。下面,让我们进入sub_401000函数,如图1-20所示。


 

图1-20中的3条指令如下。
 mov dword ptr[eax], offset off_4050AC
 mov [eax+4], ecx
 mov [eax+8], edx

通过这3条指令的赋值操作,可以分析出CPLAYER对象的内存布局,如图1-21所示。


 

根据图1-21所展示的CPLAYER对象的内存布局,我们可以很方便地修改m_dwHP和m_dwVector,从而改变血量和速度。

从以上对CPLAYER对象的分析可以发现,如果一个类有构造函数,那么新建这个类的对象的时候,后面紧跟着的是这个类的对应构造函数的调用。同时,根据IDA的分析结果,构造函数能够暴露这个类的部分对象的内存布局。所以,要静态窥视类对象的内存布局,可以搜索“new”操作符,然后在它的后面挖掘构造函数,根据构造函数来分析对象内存布局。

在游戏的可执行文件(如game.exe)中定位“new”操作符的方法大致有两种:一是通过IDA的静态反汇编,随机浏览反汇编代码,找到任意一个“new”操作符,通过这个“new”操作的交叉引用一次性全部定位“new”操作;二是通过动态Hook技术“HOOK new”调用。

在使用第二种方法之前,我们有必要了解一下如图1-22所示的Windows内存管理架构。


 

可以看到,“new”和“malloc”都是C运行时堆,都调用底层的堆管理器函数来获取服务。如果读者想深入了解堆管理,可以参考Mario Hewrdt和Daniel Pravat的Advanced Windows Debugging一书中关于Heap的介绍。如果用IDA逆向分析new函数,我们将发现:new函数会调用msvcrt.dll导出的malloc() 函数,malloc() 函数继续调用kernel32.dll导出的HeapAlloc() 函数,而HeapAlloc() 函数其实就是NTDLL.dll导出的RtlAllocateHeap() 函数。所以,如果要Hook new函数,可以使用Hook malloc() 语句或Hook HeapAlloc()/RtlAllocateHeap() 语句。不过,如果要Hook HeapAlloc() 函数或RtlAllocateHeap() 函数,我们就会发现,进程中对堆管理函数的频繁调用将干扰Hook的本意。关于这一点,在Justin Seitz的Gray Hat Python一书关于Hooking的部分也提到过,而且,该书还提供了用hippie_easy.py脚本来动态获取RtlAllocateHeap() 函数的调用情况。不过,为了尽量不受干扰,我们可以直接Hook malloc() 函数,而不深入NTDLL层,然后通过堆栈回溯来定位调用“new”操作符的返回地址。

上面所采用的分析方法,是通过“new”操作符后的构造函数来分析对象的部分内存布局的。当然,如果想知道此次截获的“new”操作针对的是角色对象、怪物对象、装备对象还是其他对象,就要结合差异分析的思想了。例如,我们可以在切换角色、切换地图、重新选怪或脱穿装备等行为的前后,看看是否有新的“new”调用。如果“new”调用发生,那么与引入新行为相关的“new”对象的操作肯定存在于这个行为发生之后的“new”调用集合中,这可以帮助我们缩小分析范围。更详细的运用差异思想来分析外挂和游戏的实例,参见第6.3节。

差异思想在Cheat Engine(作弊器)中也有丰富的体现,例如Cheat Engine的内存扫描(Memory Scan)功能。

在本节的最后,让我们看看如何运用差异思想来定位创建的新堆块。一个NewHeapDlg程序如图1-23所示。


 

NewHeapDlg进程中堆块的分配信息如图1-24所示,我们可以看到通过Heap32 ListFirst、Heap32ListNext、Heap32First和Heap32Next这4个API枚举出来的当前进程堆信息。


 

如果单击“NewHeap”界面上的“new地址”按钮,NewHeapDlg程序将通过“new”操作符为堆分配一个新地址;如果单击“显示new地址”按钮,NewHeapDlg程序将给出当前新分配的堆地址,如图1-25所示。这个时候,如果再次枚举NewHeapDlg进程堆,我们将看到如图1-26所示的堆信息。


 

如图1-24所示,共枚举出824个堆块,而经过一次“new”操作后,如图1-26所示,共枚举出828个堆块。对比两次dump的进程堆信息,我们可以快速发现新建的堆内存。Cheat Engine中有一项枚举进程堆块的信息,它位于“Memory View”→“View”→“Heaplist”菜单中。这个Heaplist只是动态跟踪进程当前已分配的堆块信息,并不像内存扫描那样多次枚举后对比差异。所以,我们既可以通过给Cheat Engine写插件来实现这种差异分析,也可以通过堆块枚举的API来实现这种差异分析。枚举堆块的代码大致如下。
 void EnumProcessHeaps()
{
 BOOL   bHeapSuccess = FALSE;
 BOOL   bHeapListSuccess;
 HEAPENTRY32 stHeapEntry32 = {0};
   HEAPLIST32 stHeapList32 = {0};
 WORD       wHeapCounts = 0;
HANDLE hHeapSnap = CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST, GetCurrentProcessId());
 if( INVALID_HANDLE_VALUE == hHeapSnap )
 {
  OutputDbgInfo(("[-] EnumProcessHeap CreateToolhelp32S napshoterror!"));
  return;
 }

 stHeapList32.dwSize = sizeof(HEAPLIST32);
 bHeapListSuccess = Heap32ListFirst(hHeapSnap, &stHeapList32);
 if ( bHeapListSuccess == FALSE)
 {
     OutputDbgInfo(("[-] Heap32ListFirst error!"));
  return;
 }
 do
 {
  ZeroMemory(&stHeapEntry32, sizeof(stHeapEntry32));
  stHeapEntry32.dwSize  =  sizeof(HEAPENTRY32);
bHeapSuccess         =  Heap32First(&stHeapEntry32, GetCurrentProcessId(), stHeapList32.th32HeapID);
  if ( bHeapSuccess == FALSE)
  {
      bHeapListSuccess = Heap32ListNext(hHeapSnap, &stHeapList32);
   continue;
  }

  do
  {
   OutputDbgInfo(("PID: %d, Heap ID: %d, Addr: 0x%0x, Size: %d", \
                stHeapEntry32.th32ProcessID, stHeapEntry32.th32HeapID, \
       stHeapEntry32.dwAddress, stHeapEntry32.dwBlockSize));
   wHeapCounts++;
   bHeapSuccess = Heap32Next(&stHeapEntry32);
  } while (bHeapSuccess == TRUE);
  bHeapListSuccess = Heap32ListNext(hHeapSnap, &stHeapList32);
 } while (bHeapListSuccess == TRUE);
 OutputDbgInfo(("[!] 共枚举到 %d 个堆地址!", wHeapCounts));
}

到目前为止,本书已经介绍了通过类构造函数来分析对象内存布局和通过动态Hook“new”操作符或差异枚举堆块来定位关键内存块的方法。下面就让我们看看在游戏中一般如何组织和管理一些关键对象内存。

角色、怪物、物品等在游戏画面中出现的对象,一般会存放在一个较大的对象指针数组中,通过多级指针可以定位这个对象指针数组,对象之间通过对象类型字段和ID字段来区分。这些对象的大致组织方式如图1-27所示。


 

可以看出,角色、怪物、物品等对象的地址都保存在一个对象数组中,它们偏移m和n的地方分别代表对象的类型和对象的UID(全局唯一ID)。通过类型我们可以知道这个对象是怪物对象还是物品对象,通过ID我们可以知道这个对象区别于其他对象的全局唯一标识。在游戏中,这个对象数组保存对象指针。根据以上信息,再结合C++ 中的多态思想,我们就可以非常方便地在游戏的每一帧中通过遍历数组来更新对象的状态了。

对于外挂程序作者而言,获取这个对象数组非常重要。这个对象数组里保存了对象的地址,获取对象的地址再进行对象内存布局分析,就可以实现吸怪(与对象坐标有关)、加速等功能。那么,应该如何分析得出对象数组指针呢?我们可以对任意一个对象地址下读断点,然后进行回溯分析,最终推导出这样一个公式—— [[[[全局指针 ]+x ]+y ]+z ]。这个公式就代表了对象数组的首地址。

下面,再让我们看看游戏角色更换装备的时候,装备对象是如何在角色和物品栏之间切换的,如图1-28所示。


 

可以看到,角色对象偏移X的地方是一个拥有装备对象地址的数组,物品栏也是一个对象地址数组。当角色对象从物品栏选取某个装备的时候,根据装备对象偏移 Y处提供的编号,相应装备对象的地址就会放入角色对象所对应的地址(角色对象基地址+X +编号×4)处,同时,物品栏数组的对应位置会被清空。当角色对象脱下某个装备的时候,角色对象所对应的存放装备对象地址的位置将被清空,同时,将该装备对象地址写入物品栏数组的任意位置。根据装备切换过程,我们可以运用Cheat Engine的内存扫描功能,当角色对象穿着装备的时候,扫描一道非零值,当角色对象脱掉装备的时候,扫描一道零值,一直扫描下去,很快就能定位存放装备对象的地址,同时定位装备对象的地址了。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站