叉叉助手是目前做得比较好一款手机游戏辅助的工具.(官网地址:a7aK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4S2^5P5X3S2#2M7$3S2G2N6g2)9J5k6h3y4F1i4K6t1&6i4K6u0o6i4@1f1&6i4K6R3%4i4K6S2o6i4@1f1&6i4K6W2p5i4@1p5J5i4@1f1&6i4K6W2n7i4K6R3$3i4@1f1$3i4K6R3^5i4K6V1H3i4@1f1@1i4@1u0m8i4K6R3$3i4@1f1@1i4@1t1^5i4K6S2p5i4@1f1#2i4@1t1H3i4K6V1I4i4@1f1%4i4K6W2m8i4K6R3@1i4@1f1$3i4@1t1^5i4@1t1^5i4@1f1$3i4K6R3^5i4K6S2r3i4@1f1^5i4@1u0q4i4K6R3#2i4@1f1#2i4K6S2m8i4@1p5&6i4K6u0o6i4@1f1#2i4K6S2o6i4K6R3#2i4@1f1$3i4K6S2n7i4@1q4o6i4@1f1#2i4@1u0q4i4@1q4q4i4@1f1@1i4@1u0r3i4@1p5I4i4@1f1$3i4@1t1^5i4@1t1^5i4@1f1$3i4K6R3^5i4K6S2r3i4@1f1%4i4K6W2m8i4K6R3@1i4@1f1#2i4@1p5@1i4@1p5&6i4@1f1#2i4@1p5@1i4@1p5&6i4@1f1%4i4@1t1K6i4@1u0n7i4@1f1#2i4K6R3^5i4K6V1%4i4K6u0o6i4@1f1^5i4@1u0r3i4K6V1^5i4@1f1$3i4K6W2o6i4K6R3&6b7@1!0o6i4K6u0o6i4@1f1$3i4K6R3^5i4K6V1I4i4@1f1#2i4K6S2r3i4@1q4n7e0g2c8Q4c8e0N6Q4b7f1c8Q4z5o6W2Q4c8e0N6Q4b7f1c8Q4z5o6W2Q4c8e0c8Q4b7U0S2Q4z5o6m8Q4c8e0c8Q4b7V1q4Q4z5f1u0Q4c8e0k6Q4b7U0S2Q4b7U0S2Q4c8e0k6Q4z5o6S2Q4z5p5k6Q4c8e0N6Q4z5f1q4Q4z5o6c8Q4c8e0S2Q4b7V1g2Q4z5o6g2Q4c8e0g2Q4z5p5q4Q4b7e0W2Q4x3X3f1`. 其实这些游戏辅助实现的方式,都大同小异,都是挂钩了几个关键函数,然后做一些适当的修改和记录,来达到辅助的目的.
最近分析了一下叉叉助手中的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 (欢迎各位童鞋加入讨论)
[培训]科锐逆向工程师培训第53期2025年7月8日开班!