武侠梦

大学起的生活很快让看电视节目成了再没真正拾起的旧习。可有闲有幻想的中学时代里那些金庸剧中的刀光剑影,仍时不时闪回在生活的很多瞬间。

下班路上,会兴奋地抓起着雪,攥成球后飞掷向敌方头目(路牌和栏杆);跆拳道课后的回家途中,要是遇到一段人少的小巷,会情不自禁地突展手臂企图用六脉神剑的光影照亮前方;又或是赶上大风天肆虐,有多少次幻想着用自己的掌风与之相抵;出门晚了的话,走的一快,深呼吸间就开始尝试施展凌波微步,哦,施展不成功~基本身法等级不够,还没做拿武学图谱的任务呢。

2000年算得上是网游元年,《石器时代》、《传奇》等屈指可数的网络游戏招揽了绝大部分有志投身互联网娱乐行业的莘莘学子们。我那会儿就用《金庸群侠传online》(网金)杀过时间。初一下学期开始,玩了快两年,什么点卡、道具、外挂、代练能买的都买了。最后半年,第一次听到其他玩家劝说:“如胶似漆,莫做固陋井底之蛙;回头是岸,迈向精彩大千世间”时开始萌生了去意(这句话放在当年其实一点不突兀,很多玩家多少有点古诗文言文的情节。客观原因是古风古气的游戏作派,而且金庸老前辈是那会儿中学生心目中深厚文字功底的代表,主观原因是经常为了考试题目背对联、背古文)。直到有一次琢磨着要汇款1000给代练公司的时候,突然虎躯一震,意识到大势已去,网金的故事已经跑偏了,该撤了。得益于那段传奇经历,今天对更为粘人的页游手游都不太感冒。

如果说网金提供了在虚拟世界做大侠的平台,游戏外挂就称得上是现实世界中的让你梦想成真的绝世武功。那会儿的神行太保、金庸通天、网金游侠,网金也疯狂等外挂都能让你手中虚拟人物在地图上快速飞跃,自动练习伐木、采矿等铸造神兵利器的技能,自动遇敌战斗获取学点,自动跟掌门学习武功,跨越游戏的限制任意驰骋。耗时乏味的练级时间全部可以让外挂代劳。外挂能通过挂钩游戏进程后强制执行特定代码片段跳过部分耗时操作,或按照游戏协议发送网络封包给服务器,直接完成目标任务。外挂开发者对于游戏代码的逆向分析几乎达到了完备的状态,每个NPC的编号、武功、属性依赖、任务条件、地图、技能的练习方法、哪些游戏规则可以适当逾越、哪些就要严格遵守才能不被封号以及游戏和服务器的通信的协议等。虽然那会儿也想自己写一个或者绕过验证免费用人家的外挂,但实在知之甚浅、无从下手。

十五年过去了,网金居然仍在线运营,外挂们也是一路相伴。就在初冬的一个下午,我闪身回眸间,又望见了尘封十五年的往事。加上最近脚扭伤了,没法去上跆拳道课,就重温了一下网金。这次打算好好折腾一下外挂,跨越时空为那个小小少年圆个梦。


网金游侠是一个仍然活跃的外挂,最近一次更新是在2015-7-28。除了能够快速飞跃、自动战斗、自动练习技能等一系列必不可少的元素,它甚至支持玩家通过自行编写lua脚本的方式扩展外挂的功能,完成自定义任务。构成网金游侠的主要文件包括GMK3.0.exe的外挂主控程序,IME32.dll的内挂代码以及很多包含NPC、物品、地图相关编号的文档。

购买了外挂的玩家运行GMK3.0.exe,填写自己的游戏账号,利用外挂启动游戏的主程序时,外挂会将内挂代码IME32.dll远程注入到游戏内存。IME32.dll载入后会修改游戏相关代码,挂钩特定函数。玩家正式登陆自己的游戏账号后,IME32.dll会根据登陆后的账号判断玩家是否注册了外挂,如果是已经注册的玩家,外挂会在游戏中创建一个辅助窗口供外挂参数调整。如果没有注册,就不会有界面弹出来让玩家使用外挂控制游戏。


最初的绕过思路是打算彻底摸清账号的验证方法,然后进行代码patch或者修改发送的验证包或者伪造服务器返回验证通过的数据包。GMK3.0和IME32.dll的外在表现形式不同,但和外挂服务器通信验证的协议是一样的。通过带有StrongOD的插件的OD避开检测,挂钩GMK的send和recv函数是可以看到验证数据的真身的。发送验证和服务器回复都是单次通信。想来即便不能解析其含义,用合法包替换也肯定能搞定。后来试了几次,发现即便相同包重放,返回结果都不一样,看来至少是加入了秒级别的时间戳,不分析算法是没法伪造了。本以为时至今日已经可以轻易干掉外挂的验证了。残酷事实是,逆向分析的能力确实相比那会儿的零有了提升,但外挂的保护方式也在生根发芽,直到今天我仍然不具备正面进攻的本事。

正面突破时最大的困难还是无法还原Themida虚拟机指令,所以外挂验证账号的数据包过程无从知晓。

虽然绕过验证是完美的解决方案,但想使用外挂功能也并不是就这么一条路。外挂启动游戏后除了注入了IME32.dll,还顺带注入了lua.dll用于执行自定义的lua脚本。外挂允许lua脚本可以调用的API多达150个,如:

void CallBoss(int nBoss)
功 能:呼叫商人
参 数:nBoss 商人编号
返回值:
描 述: 需在商人附近
--------------------------------------------------------------
void XiaoDian(int nMaster,int nKunfu,int nPoint)
功 能:消点
参 数:nMaster:师傅,nKunfu:武功,nPoint:所消学点
返回值:
描 述: 
--------------------------------------------------------------
void Buy(int nItem,int nPrice,int nCount)功 能:购买物品
参 数:nItem:物品编号 ,nPrice:物品价格 ,nCount:购买个数
返回值:
描 述:需先呼叫NPC,并正确填写购买价格
--------------------------------------------------------------
int BatConfig(int npc,int count,int delay,int grid,int speed,int timeout)
功 能:配置战斗信息
参 数:
返回值:
描 述: 
--------------------------------------------------------------
void BatInit()
功 能:初始化战场
参 数:
返回值:
描 述: 
--------------------------------------------------------------
void Log(char *str) 
功 能:输出文字
参 数:字符串
返回值:
描 述: 输出文字信息
--------------------------------------------------------------
void MoveTo(int map,int x,int y) 
功 能:移动到其他地图
参 数:map:目标地图,x:坐标y:坐标
返回值:直到移动完成
描 述: 角色移动到(其他图中)指定的坐标X,Y

自己写一段lua调用这些API就能利用外挂自动化完成很多操作,比如下面的代码就能自动练武功:

local XDEvt = 2; -- 消点事件
local XDMap = 1343; -- 消点地点
local XDX = 234;
local XDY = 278;
local XDMaster = 39778; -- 师傅ID
local XDKunfuID = 8002; -- 武功ID
function DoXiaoDian()
  if RoleXD() < 100 then
    return;
  end 
  if RoleXD() <= 10 then
    Log("Point is 10,n");
  return; 
  end
  if RoleJing() <= 10 then
    MoveTo(66,555,555)
  DoReset(3);  
  end 
  if RoleMap() ~= XDMap then
    MoveTo(XDMap,XDX,XDY); -- 移动到目标点
  end 
  EventID(XDEvt); -- 呼叫师傅事件
  Delay(1000);
  VerifyCode(); -- 效验码
  Delay(1000);
  XiaoDian(XDMaster,XDKunfuID,1); -- #8001:基本刀法100
  Delay(2000);
end
DoXiaoDian();

通过强制修改内存的方法做了一些初步的尝试,lua的接口无需外挂验证账号也是可以照常使用的。而且lua.dll并没有使用Themida进行虚拟机级别的保护,稍加分析可以发现这就是lua源码直接编译的结果,只不过版本号被删掉了。遂从lua的官网把所有版本的bin都下载下来,然后和外挂使用的lua.dll进行文件大小、输出函数、代码片段的比对,基本可以把范围缩小为VS2010编译的lua 5.2。下载了5.2的源码后,自行编译一个lua.dll进行替换,外挂运行正常。

IME32.dll中只有和账户验证相关的敏感代码进行了虚拟机保护,直接dump内存得到的bin中,即便没有修复地址,用IDA分析时仍然可以找到初始化lua库以及注册自定义API的过程:

int sub_774FA140()
{
  int i; // [sp+0h] [bp-4h]@1
  for ( i = 0; (&off_775CA7E8)[8 * i]; ++i ) // 循环注册lua脚本使用的API
    sub_77515540(0x2F57100, (int)(&off_775CA7E8)[8 * i], (int)*(&off_775CA7EC + 2 * i));
  return sub_774F6F50(sub_774D9950);
}

char __thiscall sub_77515540(int this, int a2, int a3)
{
  int v3; // ST0C_4@1
  v3 = this;
  // 注册自定义的API供lua调用
  lua_pushcclosure(*(_DWORD *)(this + 8), a3, 0); 
  lua_setglobal(*(_DWORD *)(v3 + 8), a2);
  return 1;
}

其中off_775CA7E8就是API的列表,每个API的名称和入口函数地址都清晰可见

CODE:775CA7E8 off_775CA7E8    dd offset aMainpath     ; "MainPath"
CODE:775CA7EC off_775CA7EC    dd offset sub_774F6FA0  ;
CODE:775CA7F0                 dd offset aIsrun        ; "IsRun"
CODE:775CA7F4                 dd offset sub_774F6F70
CODE:775CA7F8                 dd offset aMainloop     ; "MainLoop"
CODE:775CA7FC                 dd offset sub_774F6FC0
CODE:775CA800                 dd offset aDelay_0      ; "Delay"
CODE:775CA804                 dd offset sub_774F7030
CODE:775CA808                 dd offset aWaitforevent ; "WaitForEvent"
CODE:775CA80C                 dd offset sub_774F6FE0
CODE:775CA810                 dd offset aLog_1        ; "Log"
CODE:775CA814                 dd offset sub_774F7090
CODE:775CA818                 dd offset aSetpkstate   ; "SetPKState"
CODE:775CA81C                 dd offset sub_774F9670
CODE:775CA820                 dd offset aBatconfig    ; "BatConfig"
CODE:775CA824                 dd offset sub_774F96B0
CODE:775CA828                 dd offset aBatinit      ; "BatInit"
CODE:775CA82C                 dd offset sub_774F9910
CODE:775CA830                 dd offset aBatdircall   ; "BatDirCall"
CODE:775CA834                 dd offset sub_774F9990
CODE:775CA838                 dd offset aBatsetid     ; "BatSetID"
CODE:775CA83C                 dd offset sub_774F9830
CODE:775CA840                 dd offset aBatsetkunfu  ; "BatSetKunfu"
...

如果打算分析外挂的工作原理,这些API入口函数是很好的切入点。为节省体力,写个wrapper就能够方便的调用这些接口使用外挂功能就好。

调用lua相关函数全程都要维持一个实例指针lua_State *L,只有使用外挂初始化后的lua_State指针才能调用那些诱人的内建API。好在lua库创建都要通过luaL_openlibs (lua_State *L)完成,挂钩它就能得到这个实例指针。既然有了lua源代码,想动手脚真是手到擒来。

在lua源码中添加以下代码片段:

lua_State *Handle_L;
LUALIB_API lua_State *luaL_getL() 
{
  return Handle_L;
}

LUALIB_API void luaL_setL(lua_State *L) 
{
  Handle_L = L;
}

LUALIB_API void runCMD(char *luafile) 
{
  lua_State *L = luaL_getL();
  luaL_dofile(L, luafile);
}

LUALIB_API void luaL_openlibs (lua_State *L) {
  const luaL_Reg *lib;
  luaL_setL(L); //记录实例指针
...

这样通过调用runCMD()就能随时让外挂执行我们自行编写的lua脚本,并且lua脚本也可以使用外挂注册的API函数,尽享外挂的功能。另外再写一个图形界面的程序,根据自己预定的功能增加一些按钮,按钮事件仅仅是把每个功能用lua脚本完成,然后写入文件custom.lua。再通过CreateRemoteThread的方式调用游戏内存中的runCMD(“custom.lua”),就能避开验证使用外挂的功能了。