2007/06/24 | 以前做外挂留下的东西
类别(游戏开发) | 评论(5) | 阅读(1226) | 发表于 12:27

接了个做网游外挂的外包.这方面没有经验,不过应该不太难吧?
大家祝我早日搞定吧!

显血,显蓝,显经验
自动喝血,喝蓝实现了

说下心得:
1 打开进程空间:
没什么好说的,我也是抄的别人的代码,一下就搞定了
// 找进程
if( !m_bFoundGame ) {
 m_hGameWnd = FindWindow( NULL, "某游戏" );
 if( m_hGameWnd ) {
  DWORD PID;
  GetWindowThreadProcessId( m_hGameWnd->m_hWnd, &PID );
  m_hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, PID );
  if( m_hProcess ) {
   m_bFoundGame = true;
  }
 }
}

2 注册热键
这是为了在游戏中能呼出外挂窗口,进行设置;这个的实现也很简单
// 注册热键
if( !m_bHotKeySet ) {
 m_pHotKey = (CHotKeyCtrl*)GetDlgItem( IDC_HOTKEY1 );
 m_pHotKey->SetHotKey( VK_F12, 0 );
 DWORD wKeyAndShift = m_pHotKey->GetHotKey();
 this->SendMessage( WM_SETHOTKEY, wKeyAndShift );
}

3 监视动态内存
麻烦的地方来了
我下载了CheatEngine,然后搜索角色的HP信息(这个和单机游戏方法一样),很容易就搜索到地址.
恩,有了这个地址,我们就可以在外挂中监视角色的HP.(显然修改这个值是没有用的)
搞定了? 远远没有! 我发现在不同的机器上进入游戏, 或者同一台机器上多次进入游戏, 这个地址会动态变化!
上网查资料, 说是什么双线动态地址保护之类...
按照一些文档的做法, 逆向搜索基址(他们认为总能找到一个不变的基址,然后利用基址+偏移量找到动态地址),结果完全不可行(我已经逆向追踪了N层,很显然是追踪不到静态基址了,看来韩国人的网游技术是比国内强一些)
OK, 改变思路. 找到HP地址, 下断点, 每当有代码向这个地址写入, 就中断.
果然找到写入该地址的代码, 而且如预期一样, 发生在角色受伤或恢复生命时.
反编译这段代码, 看到类似如下的东西:
004aa480 ja 004aa4a1
004aa482 mov edx, [esp+0c]
004aa486 mov [ecx+00000124], edx   
004aa48c call 004aa010
004aa491 xor eax, eax
其中ecx+00000124就是HP的地址.
OK, 只要能够在游戏运行到这里时, 以某种方式获得此时ECX寄存器的值, 就可以追踪到这个讨厌的动态地址了!
我的思路是把ECX寄存器的值保存到某个无用地址去.
怎么获得无用地址呢? 大家知道VC编译的程序, 其代码段总是从0x00400000开始的, 于是我去这个地址浏览了一下, 发现有这样的代码:
this program can not be run in dos mode.
呵呵, 很熟悉的东西不是? 显然这个就是无用地址, OK, 得到0x00400070.
所以汇编代码为:
mov [0x00400070], ecx
查Intel opcodes, 求得其机器码为: 89 0D 70 00 40 00
我们只要把这段代码, 写入刚才004aa486 mov [ecx+00000124], edx这句话上面的某处, 就可以了
检查一下反汇编代码, 发现在上面不远处0x004aa47E有这样的代码
TEST EDX, EDX
JA 0x4aa4a1
这样的语句
显然这是垃圾语句, JA永远不会执行, 把我们的作弊代码贴到这里最合适不过了.
可是, 这里只有4个字节的空间, 而我们需要6个字节 (该死的MOV指令...)
没办法, 做了一件肮脏的事情, 把0x004aa47C的那句jne也覆盖了... 貌似这样很危险, 但是实际测试游戏并未崩溃, 那么就这样吧 -_________________-!
于是把89 0D 70 00 40 00写入0x004aa47C, 我们终于完成了动态地址的追踪.
写入方法为调用WriteProcessMemory.
代码如下:
// 修改游戏代码
if( m_bFoundGame && !m_bModified ) {
 BYTE mycode[6]; // mov [0x00400070], ecx
 mycode[0] = 0x89;
 mycode[1] = 0x0D;
 mycode[2] = 0x70;
 mycode[3] = 0x00;
 mycode[4] = 0x40;
 mycode[5] = 0x00;
 WriteProcessMemory( m_hProcess, (LPVOID)0x004AA47C, &mycode, 6, NULL );

 BYTE mydata[4];
 mydata[0] = 00;
 mydata[1] = 00;
 mydata[2] = 00;
 mydata[3] = 00;
 WriteProcessMemory( m_hProcess, (LPVOID)0x00400070, &mydata, 4, NULL );

 m_bModified = true;
}
大家可能会觉得向0x00400070写入0比较奇怪, 其实这只是我判断0x00400070是否已经获取动态地址的方法.
我下了计时器, 每隔500ms读这个地址, 发现不为0就保存这个动态地址.
后来又发现一个有趣的事情, 004aa486这里不仅HP要走这段代码, MP和EXP也要走. 呵呵, 一举三得, 最后一个写入的是MP. 从0x00400070得到MP地址后, -0x250就是HP地址, +0x250就是EXP地址, 爽啊!
代码如下:
// 找基址
if( m_bModified && !m_bGotAddress ) {
 DWORD addr;
 if( ReadProcessMemory( m_hProcess, (LPCVOID)0x00400070, &addr, 4, NULL ) ) {
  if( addr != 0 ) {
   m_bGotAddress = true;
   addr += 0x124;
   m_pHP  = addr - 0x250;
   m_pMP  = addr;
   m_pEXP = addr + 0x250;
  }
 }
}
至此监控HP, MP, EXP完全搞定

4 自动吃红/蓝! 按键消息?
自动吃红的逻辑简直太容易了, 只要把监控到的HP, 与用户设置的保护值比较, 如果低于就吃红.
怎么吃红呢? 这个游戏把红药拖到快捷栏之后, 就有快捷键对应, 这个键并不确定, 但是可以让外挂用户在外挂界面中手工指定, 比如A键就是吃红.
然后我们只需要模拟一次按键就行了, 这太简单了.
PostMessage...
? 怎么不行 ? OK, 用全局的keybd_event...
还是不行? 奇怪了...
原因是该程序使用了DirectInput直接响应硬件, 饶过了windows的键盘消息, 所以我的做法当然无效, 真狡猾..
上网, 查资料, 找到一个winio库, 据说可以从设备驱动程序的级别模拟按键操作.
该库有完善文档, 这里推荐一下...
总之果然游戏乖乖就范, 当然这里面细节问题还是很多的, 比如key_down和key_up的节奏问题, 就不多扯了.
封装了winio的按键模拟代码如下:
const int KBC_KEY_CMD = 0x64;//键盘命令端口
const int KBC_KEY_DATA = 0x60;//键盘数据端口


void KBCWait4IBE()
{
 DWORD dwVal;
 do {
  GetPortVal( KBC_KEY_CMD, &dwVal, 1 );
 } while( dwVal & 0x2 );
};

void VxdKeyDown( UINT vKey )
{
 UINT iScancode = MapVirtualKey( vKey, 0 );
 KBCWait4IBE();
 SetPortVal( KBC_KEY_CMD, 0xD2, 1 );
 KBCWait4IBE();
 SetPortVal( KBC_KEY_DATA, iScancode, 1 );
}

void VxdKeyUp( UINT vKey )
{
 UINT iScancode = MapVirtualKey( vKey, 0 );
 KBCWait4IBE();
 SetPortVal( KBC_KEY_CMD, 0xD2, 1 );
 KBCWait4IBE();
 SetPortVal( KBC_KEY_DATA, iScancode|0x80, 1 );
}

void VxdKeyClick( UINT vKey )
{
 VxdKeyDown( vKey );
 Sleep(100);
 VxdKeyUp( vKey );
}
至于自动吃红的代码... 呵呵, 这个就算了, 没啥值得看的...

自带工具,支持强制窗口模式运行游戏!

模拟式外挂,安全可靠(可以设置安全级别!)

后台运行,热键启动/关闭

自动喝红(大红/小红两级)
自动喝蓝
自动回城
自动拣物(不会影响坐下休息!)(目前所有这个游戏的外挂开启拣物,都会导致无法坐下休息)
自动挖矿(有单独的热键启动/关闭)
自动战斗(可以输入挂机坐标范围,在任何地方都可以挂机!可以开启超级随机选项,使你的挂机更难被察觉!)

自动整理内存,使外挂更稳定!

================================

这些功能还存在难点。本次制作外挂完全没涉及数据包分析,全靠分析客户端数据加按键模拟实现,相当简单的做法。
可能会觉得某些功能,只靠模拟按键难以实现,其实总会有办法的。
此外用到了Hook。

内存整理是一个小发现,用到了 SetProcessWorkingSetSize。
以下抄自MSDN:
The SetProcessWorkingSetSize function sets the minimum and maximum working set sizes for the specified process.

BOOL SetProcessWorkingSetSize(
  HANDLE hProcess,
  SIZE_T dwMinimumWorkingSetSize,
  SIZE_T dwMaximumWorkingSetSize
);

If both dwMinimumWorkingSetSize and dwMaximumWorkingSetSize have the value -1, the function temporarily trims the working set of the specified process to zero. This essentially swaps the process out of physical RAM memory.

看到这里,学习过操作系统的同学就会有所觉悟了。进程页面必然会重新载入,而这实际上成为了内存整理的手段!
不光是外挂,而且还可以对游戏进行内存整理。从这一点来说,应算是绿色外挂吧?--!

下面的分析,很清楚地说明了这个外挂最核心的思路:

========================================================

人物坐标:

用CE搜索到数值写入点:0050D5EC MOV [ESI+0x344], EAX
所以需要保存ESI的值

从 0050D641 开始注入 MOV [0x400094], ESI
0050D641 pop edi 5f
0050D642 pop esi 5e
0050D643 pop ebx 5b
0050D644 add esp, 10 83 c4 10
0050D647 ret 0004 c2 04 00

之后读 [0x400094] 即可
+0x344 为X坐标
+0x348 为Y坐标(猜的,验证正确 ^^)

=============================================================

人物是否正坐下休息:

用CE搜索到数值写入点:00414806  MOV [ESI+0x40], EAX
所以需要保存ESI的值

从 0041482E 开始注入 MOV [0x400098], ESI
0041482E pop edi 5f
0041482F pop ebx 5b
00414830 pop esi 5e
00414831 ret 0004 c2 04 00

之后读 [0x400098] 即可

=================================================

搜人物是否坐下的标志时极为痛苦(花了N小时才定位到),因为它是一个BOOL值,而不是数值!!!
而且我也不确定这样的信息是否会保存为BOOL值!!!只是根据自己写游戏代码的经验,认为如果这样重要的标志都不保存的话,韩国的程序员就是猪了!!!
这太痛苦了!
我大量的时间在 Search Changed 及 Search Unchanged 之间渡过。。。
最终定位到几千个地址。。。
想想这样的值应该不会超过255,狠狠心 Search Smaller than 255,还剩几百个,还是不行。
妈的豁出去了Search Smaller than 2 ! BOOL值应该只有0和1两种状态!如果失败这几个小时就白费了!
还剩几个地址了。。。随便跑几步,再切换地图,继续Search Smaller than 2。。。
哈哈,只有一个地址了!
成功了!
坐下,该值为1;站起来,变为0;反复测试数次均正确!
接着就是Find out who writes to this address,定位反汇编代码,这一步利用了CE的强大功能。
下面就是自己的事了:
1 反汇编代码分析,同样利用CE,还可以调试,寄存器这些都拿得到!
2 寻找漏洞,其实就是上下文搜索无用代码,尤其是int 3(机器码为0xCC),很奇怪大部分反汇编代码中都充斥着大量的0xCC,这不是自己暴露破绽么?
3 注入自己的代码,先草拟汇编代码,然后翻译成机器码。
于是一只寄生虫就诞生了。
注入自己的代码时,太复杂的代码可能找不到漏洞,那么可以放到0x00400000去,然后用CALL指令执行。
实际测试发现从0x00400050开始使用比较安全,否则宿主程序容易崩溃。

最初,我采用SendMessage WM_SETHOTKEY的方法,来实现在游戏中呼出外挂并激活。
这种做法确实简单(仅仅一个标准控件加几行代码),但存在非常大的弊端:
1 使用Hotkey呼出外挂将导致外挂窗口被强行带到前端,此时游戏窗口被最小化,非常麻烦而且游戏人物很危险。
注:WM_HOTKEY与WM_SETHOTKEY不是同一个事件,对于后者,MSDN说:
An application sends a WM_SETHOTKEY message to a window to associate a hot key with the window. When the user presses the hot key, the system activates the window.
可见这个问题基本上无解。
2 对于一个窗口来说,Hotkey只能有一个,而我们需要 挖矿开关,开启外挂功能,关闭外挂功能 三个热键。

=====================

怎么办?当然我一下就想到了大家耳熟能详的名词--钩子。
虽然钩子这个名词我非常熟悉,可是真正自己编写一个钩子程序,那还是第一次。没什么,承认自己的不足是进步的第一步,下次不就知道了么?
OK! 查资料,得到这样的描述:

Windows系统是建立在事件驱动的机制上的,说穿了就是整个系统都是通过消息的传递来实现的。而钩子是Windows系统中非常重要的系统接口,用它可以截获并处理送给其他应用程序的消息,来完成普通应用程序难以实现的功能。钩子的种类很多,每种钩子可以截获并处理相应的消息,如键盘钩子可以截获键盘消息,外壳钩子可以截取、启动和关闭应用程序的消息等。
钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。对每种类型的钩子由系统来维护一个钩子链,最近安装的钩子放在链的开始,而最先安装的钩子放在最后,也就是后加入的先获得控制权。要实现Win32的系统钩子,必须调用SDK中的API函数SetWindowsHookEx来安装这个钩子函数,这个函数的原型是
HHOOK SetWindowsHookEx(int idHook,HOOKPROC lpfn,HINSTANCE hMod,DWORD dwThreadId);
其中,第一个参数是钩子的类型;第二个参数是钩子函数的地址;第三个参数是包含钩子函数的模块句柄;第四个参数指定监视的线程。如果指定确定的线程,即为线程专用钩子;如果指定为空,即为全局钩子。其中,全局钩子函数必须包含在DLL(动态链接库)中,而线程专用钩子还可以包含在可执行文件中。得到控制权的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外一个SDK中的API函数CallNextHookEx来传递它。钩子函数也可以通过直接返回TRUE来丢弃该消息,并阻止该消息的传递。

==========================

看起来线程钩子比较简单,于是我选择实现它。
在程序中添加
m_hHook = SetWindowsHookEx( WH_KEYBOARD, KeyboardProc, NULL, PID );
其中PID是这个网络游戏的进程号。
看上去很好,可是实际测试无法工作,一检查,原来这里m_hHook返回NULL,根本就没成功。。。
再翻MSDN:
The hMod parameter(就是参数三) must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within the code associated with the current process.
原来线程钩子必须是当前进程(也就是外挂本身)创建的,不能用线程钩子去钩其他进程!汗!

========================

没办法,必须做一个全局钩子
建一个DLL工程,导出DLL和LIB;
然后在外挂中引用它。
核心代码:
在DLL中:
BOOL CKeyboardHook::StartHook()
{
 BOOL bResult=FALSE;
 glhHook=SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,glhInstance,0);
 if(glhHook!=NULL)
  bResult=TRUE;
 return bResult;
}
在外挂中:
CKeyboardHook m_hook;
m_hook.StartHook();

================================

继续,为了实现那几个开关的功能,我的按键回调这样写:
//键盘钩子函数
LRESULT CALLBACK KeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)
{
 if( ((DWORD)lParam&0x40000000) && (HC_ACTION==nCode) ) {// 有键按下
  // F9 = 自动挖矿开关
  // F10 = 开启外挂功能
  // F12 = 禁用外挂功能
  if( wParam == VK_F9 ) {
   g_bStartMine = !g_bStartMine;
  }
  if( wParam == VK_F10 ) {
   g_bStartAll = TRUE;
  }
  if( wParam == VK_F12 ) {
   g_bStartAll = FALSE;
   g_bStartMine = FALSE;
  }
 }
    return CallNextHookEx( glhHook, nCode, wParam, lParam );
}

这里问题就来了,g_bStartMine,g_bStartAll这两个变量的实例,放在哪里?
显然不能放在外挂代码里,因为这个DLL根本不知道外挂的存在,无从拿到这两个数据。
而放在钩子代码里,外挂又如何获取呢?

=================================

最初我用了非常愚昧的做法,大家莫耻笑:
我在钩子CPP中写:BOOL g_bStartMine;
然后在钩子H中写:extern BOOL g_bStartMine;
最后在外挂代码中包含钩子的H。
哈哈编译通过。。。恩?怎么LINK出错?
仔细想想才发觉自己昏了头,钩子中定义的变量,并没有在外挂中定义过啊,当然LINK不了。
回忆一下基本概念,然后:
钩子H改为:__declspec(dllexport) BOOL g_bStartMine;
外挂H添加:__declspec(dllimport) BOOL g_bStartMine;
哈哈LINK通过。。。恩?怎么按下热键根本无效?
看起来,外挂中根本没拿到g_bStartMine的值的变化!
这又是怎么回事?!

=============================================

感谢互联网,查资料得到:
在Win16环境下,所有应用程序都在同一地址空间;而在Win32环境下,所有应用程序都有自己的私有空间,每个进程的空间都是相互独立的,这减少了应用程序间的相互影响,但同时也增加了编程的难度。大家知道,在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,当进程在载入DLL时,系统自动把DLL地址映射到该进程的私有空间,而且也复制该DLL的全局数据的一份拷贝到该进程空间,也就是说每个进程所拥有的相同的DLL的全局数据其值却并不一定是相同的。因此,在Win32环境下要想在多个进程中共享数据,就必须进行必要的设置。亦即把这些需要共享的数据分离出来,放置在一个独立的数据段里,并把该段的属性设置为共享。

原来如此。。。

可惜这篇文章给出的共享数据段方法,完全错误,后来又查了大量资料才得出正确做法。
略过痛苦的搜索过程,把结果公布如下:

第一步:在钩子CPP中这样定义变量:
#pragma data_seg("mydata")//mydata这个名字可以任意
 BOOL g_bStartMine = FALSE;//必须初始化!
#pragma data_seg()
这是为了把g_bStartMine放入一个被命名的数据段。

第二步:在工程设置的LINK命令行中添加:
/SECTION:mydata,RWS
这是为了把mydata数据段设为共享数据段,权限为读/写。

第三步:为钩子增加:
BOOL GetStartMine();
void SetStartMine( BOOL b );
两个接口,因为只有这样才能读写共享数据。

第四步:在外挂程序中调用GetStartMine与SetStartMine,读写共享数据。

==========================================

呼~ 终于搞定了这一切,怀着紧张的心情测试了一下。。。。。。
OH YEAH! 一切正常!

事实上,在进程间共享数据还有 CreatFileMapping 和 SendMessage WM_COPYDATA 两种方法。
前者是创建共享内存区,类似于UNIX中的管程操作。对大规模数据效率很高(很多共享数据放在数据段中会导致DLL尺寸显著变大)。
但是我这里只需要共享两个BOOL而已,杀鸡怎用牛刀?而且本方法实现更为复杂!
后者是通过WINDOWS消息发送共享数据,这个方法也很简单,但发送数据方在接受数据方处理数据之前,处于被阻塞的状态,这可能会导致玩家在游戏中按热键出现问题,所以也没有采用。

《完》

0

评论Comments

日志分类
首页[118]
游戏开发[40]
绝命都市[6]
文章[47]
朝花夕拾[10]
诗歌[6]
相簿[9]