叉叉助手是目前做得比较好一款手机游戏辅助的工具.(官网地址:http://www.xxzhushou.cn),里面集成了不少的游戏辅助,包括微信游戏的天天系列,还有COC,我叫MT等等一些游戏的辅助. 其实这些游戏辅助实现的方式,都大同小异,都是挂钩了几个关键函数,然后做一些适当的修改和记录,来达到辅助的目的.
最近分析了一下叉叉助手中的QQ欢乐斗地主的记牌器,抛砖引玉,和大家一块探讨下IOS/Android游戏辅助的分析和实现.
1. Cydia Substrate
使用 TheOS 做越狱开发的同学,肯定都知道 Cydia Substrate 这个东西,其实所谓的 Substrate 就是类似Windows上的Hook框架(Detours,MHook),提供了一套标准的Hook函数: MSHookFunction,MSHookMessage,MSFindSymbol 等等给大家使用. 根据官网的介绍,这套框架不仅支持IOS,而且还支持Android平台. 游戏辅助的开发者,也是期望能迅速开发,所以也是会利用Substrate来实现的. 所以我们分析游戏辅助插件的时候,只需要紧紧盯着这几个 MSHook** 的函数,就能迅速定位到关键所在.
2. 获取二进制文件
AppStore下载QQ欢乐斗地主,使用iTools,将名为QQHLDDZ的文件导入到电脑上. 同样下载好叉叉助手,安装好欢乐斗地主记牌器插件,然后使用itools,从 MobileSubstrate\DynamicLibrary 目录下获取 xxHLDDZPlugin.dylib.
3. 反汇编 xxHLDDZPlugin.dylib
使用IDA Pro 或者 Hopper 打开 xxHLDDZPlugin.dylib. 对 MSHookFunction 函数查找引用,我们会发现在一个名为 sethook()的函数里面会调用MSHookFunction(), 使用Hopper的伪代码功能可以看到:
提醒下Hopper的伪代码功能不是特别好用,仅供参考(还有就是Hopper的反汇编识别函数的能力也比IDA Pro 弱太多了). 所以这里只看到 MSHookFunction() 只调用了一次,看看sethook()的汇编,这里应该是调用了两次 MSHookFunction().
所以 sethook()里面的实现应该是:
MSHookFunction(_offset_new_add + module_base ,func_hook_xx_new_add,&func_orig_xx_new_add)
MSHookFunction(_offset_new_start + module_base,func_hook_xx_new_start,&func_orig_xx_new_start)
根据 MSHookFunction() 函数的定义:
void MSHookFunction(void*function,void* replacement,void** p_original);
所以第一个参数就是我们要hook的函数.找到第一个参数 _offset_new_add和 _offset_new_start定义的地方,我们会看到:
_offset_new_add = 0x004b2b68;
_offset_new_start = 0x004f0978;
这里记牌器插件使用的 Offset + 基地址 来动态获取函数的地址,然后再进行Hook.
4. 反汇编 QQHLDDZ
使用IDA Pro 或者 Hopper 打开 QQHLDDZ,然后跳转到那两个offset.
对于 _offset_new_add = 0x004b2b68; 我们能看到一个名为: “__ZN12XOutCardCtrl14AddNormalCardsEjPhjj”的函数,Hopper 伪代码如下,从函数名猜测下这个 XOutCardCtrl::AddNormalCards,应该是记录了每个玩家的出牌.
对于 _offset_new_start = 0x004f0978; 我们也能看到一个名为: “ __ZN8XGameMgr11OnGameStartEv “的函数, Hopper伪代码如下,同样从函数名看到,这个函数 XGameMgr::OnGameStart,应该是表示新一轮游戏的开始.
5. 分析待Hook函数的定义
从上面我们可以看到函数的定义,XOutCardCtrl::AddNormalCards函数有4个参数,但是无法得知每个参数的具体用途,这里就需要自己去分析和调试了,过程比较琐碎和需要耐心,这里就不仔细叙述了.
具体定义如下:
int AddNormalCards(void* self, int player,unsigned char* cards,int count);
第一个参数 self,是 self 指针.
第二个参数 player,是int 类型,表示的每个玩家的编号. 范围是 0 - 2 (实际调试发现,我自己是2,左玩家是 0 ,右玩家是 1)
第三个参数 cards, 是个 unsigned char类型数组,用来记录玩家每次出的牌.
第四个参数 count, 用来表示 参数3 cards 数组的大小,也就是出牌的数量.
int XGameMgr::OnGameStart()
无参数,用来表示每次牌局的开始.
6. 实现记牌器
使用TheOS 创建一个 Tweak 工程. 在Tweak.xm 里面添加我们自己的代码.
%ctor
{
initPokerTable();
MSHookFunction((void*)MSFindSymbol(NULL,"__ZN12XOutCardCtrl14AddNormalCardsEjPhjj"),(void*)my_newadd,(void **)&orig_newadd);
MSHookFunction((void*)MSFindSymbol(NULL,"__ZN8XGameMgr11OnGameStartEv"),(void*)my_newstart,(void **)&orig_newstart);
}
initPokerTable(); 初始化了一个NSMutableArray的全局变量,名为PokerTable,里面存储了从 黑桃A 到 大王 的 54 张扑克牌,方便我们查看调试信息.
MSHookFunction(),会调用MSFindSymbol()函数来查找原始函数的地址.
为什么使用MSFindSymbol(),而不是使用xx记牌器里面的 module_base + offset 的方式,其实尝试了使用 offset 这种硬编码的方式,但是很容易造成程序崩溃,多次尝试下,还是改为MSFindSymbol()函数,这个函数的实现其实是查找Mach-o文件格式的symbol表,然后获得函数地址. 这个和Windows 里面查找PE文件的 Import(Export) Address Table 函数的方式类似. 所以这种方式更加稳定可靠.
my_newadd()函数只是打印了每一轮牌局(GameRound),每个玩家(Player) 出的牌. (也就是实现了记牌器的功能.)
int my_newadd(void* self, int player,unsigned char* cards,int count)
{
for(int i = 0; i < count; i++)
{
NSLog(@"GameRound%d:player%d:%@",Round,player,[PokerTable objectAtIndex:cards[i] - 1]);
}
NSLog(@"GameRound ---------------------------------------");
return orig_newadd(self,player,cards,count);
}
my_newstart()函数,会对 Round 变量递增,然后打印信息,表示新一轮游戏开始了.
int my_newstart()
{
Round++;
NSLog(@"GameRound%d begin!",Round);
return orig_newstart();
}
7. 查看调试输出
在 terminal 输入 make package install,对工程编译打包,并发送到设备上.
再 ssh root@你的IP. 到设备上.
然后你开始玩一局QQ欢乐斗地主,
然后在teminal上输入 grep GameRound /var/log/syslog (注意,请安装好syslogd插件) 会看到如下的调试信息:
这里,你能清楚的看到每个玩家所出的牌. 这样我们就实现了一个记牌器所需要的核心功能.
8. 总结
其实上面的调试输出只是一个简单的演示罢了,要做成产品给用户使用,还需要像叉叉助手一样,在QQ欢乐斗地主那,添加自己的subview,然后再绘制控件,再将信息展现给用户,还有很多事情需要完善.
再说说其他游戏辅助插件的分析原理,其实分析和这个类似.按照这个思路一步一步耐心分析,就能实现同样的功能了.
再补充一下Android平台,叉叉助手的Android平台其实也是使用同样的技术,和上面的分析大同小异.
最后,此文抛砖引玉,还希望各位朋友对此不吝赐教! ( 欢迎各位同仁交流和认识)
Author: coltor
Email(QQ): coltor#qq.com
交流群: 12399218 (欢迎各位童鞋加入讨论)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)