前言
基础知识都学完了,我们来实战一下微信自动抢红包的功能,最终实现的效果就是给微信增加一个开关,用来控制自动抢红包功能的开启,并且安装到非越狱手机上。
方法二用的Hook语法是:CaptainHook语法,因为这种语法可以让我们直接在Xcode中打断点,动态调试起来非常方便
方法二用的主要工具是MonkeyDev、Hopper Disassembler、Xcode的LLDB
项目代码已经放到github上了,点击查看WXTweak
一、逆向的整个流程
-
- 工具准备
硬件设备:一台Mac电脑、一台越狱手机 (我准备的是iPhone7,iOS13.3的系统)
查看UI层级工具:
FLEXLoader
,FLEXLoader
在Cydia
中直接搜索安装即可,需要在设置里开启(或者Cycript、Reveal)脱壳工具:frida-ios-dump一键脱壳
导出头文件工具:MonkeyDev,在
Build Settings
中设置MONKEYDEV_CLASS_DUMP
为YES,然后编译即可反编译工具(静态分析):Hopper Disassembler (或者 MachOView、IDA)
动态调试工具:
Xcode的LLDB + DebugServer
开发工具:
Xcode
+ MonkeyDev签名工具:MonkeyDev
辅助工具:
iFunBox、OpenSSH、Cydia
-
- 分析微信界面,使用
FLEXLoader
分析微信的界面层级(或者用Cycript、Reveal
,Cycript
可以以文字的形式查看层级结构,Reveal
可以以3D的视角查看层级结构,FLEXLoader
可以在手机上查看UI层级,如何使用这些工具请看这里)
- 分析微信界面,使用
-
- 脱壳,使用frida-ios-dump一键脱壳,拿到脱壳后的微信可执行文件(可执行文件是什么,内部放了什么,请看这里)
-
- 导出头文件,脱壳之后,使用
MonkeyDev
导出微信的头文件,以便后续分析,MonkeyDev对class-dump
进行了封装,只需要在Build settings
中设置MONKEYDEV_CLASS_DUMP
为YES
,然后编译即可
- 导出头文件,脱壳之后,使用
-
- 静态分析,脱壳之后,使用 Hopper Disassembler分析微信的可执行文件 (或者使用MachOView、IDA )
-
- 动态调试,通过分析微信界面,找到聊天界面的类,使用
MonkeyDev
提供的OCMethodTrace
代替繁琐的logify.pl
,让此类的所有方法都打印参数和返回值,根据打印结果,找出与红包相关的类,在Xcode上打断点,在Xcode的LLDB中看函数调用栈,逐步找出收到红包消息、打开红包的整个流程 (如何动态调试,请看这里;Xcode的打断点和平常开发项目打断点一样,不需要找虚拟内存的函数地址了)
- 动态调试,通过分析微信界面,找到聊天界面的类,使用
-
- 编写逆向代码,根据动态调试的结果,使用 CaptainHook语法,在
WXTweakDylib.m
中去hook
消息类和红包类,当收到红包消息时,自动调用抢红包的方法,以达到自动抢红包的目的
- 编写逆向代码,根据动态调试的结果,使用 CaptainHook语法,在
-
- 签名并安装到非越狱手机,使用
Xcode
+ MonkeyDev ,给Framework、dylib、AppExtension、WatchApp、WeChat.app包
重新签名,并安装到非越狱的手机上,此过程MonkeyDev
已经帮我们封装好了,和Xcode
正常开发项目基本一致,详情可以查看MonkeyDev官方文档
- 签名并安装到非越狱手机,使用
我们把主要精力放在动态调试和编写逆向代码即可,其他步骤都是固定死的,按部就班来即可。
二、动态调试微信,分析收到红包消息的流程
下面的过程其实非常简单,就是找到聊天界面对应的类,打印出此类的函数调用流程,找到与消息处理有关的类,分析出处理消息的函数,找到红包消息的消息类型
- 1 .在越狱手机上安装
FLEXLoader
,查看微信聊天界面的类名BaseMsgContentViewController
,在WXTweakDylib.m
中,通过OCMethodTrace,让BaseMsgContentViewController
类的所有方法打印参数和返回值
[[MDMethodTrace sharedInstance] addClassTrace:@"BaseMsgContentViewController"];
-
2 .打开微信聊天界面,接受一个红包消息,在Xcode上的控制台,查看
BaseMsgContentViewController
类的打印结果,发现每次接受消息都会走- (void)addMessageNode:(id)arg1 layout:(_Bool)arg2 addMoreMsg:(_Bool)arg3
方法,使用 CaptainHook语法Hook这个方法,并且给这个方法打断点,分析函数调用栈,如下所示:
3 .打完断点之后,发送一条微信消息,就会进入Xcode断点,使用
bt
命令,查看addMessageNode
函数调用栈,如上图所示,这里只会显示部分方法名称,剩下的需要我们自己找,我们拿到函数的基地址,才能去Hopper中按G键
跳转到函数名,而这里的函数地址都是虚拟内存中的真正的函数地址,减去ASLR偏移地址,才是函数的基地址,
拿到函数的ASLR偏移量
(lldb) image list -o -f | grep WeChat
[ 0] 0x0000000002e10000 /Users/songpeng/Library/Developer/Xcode/DerivedData/TestMonkey-frfqpwylhhialbcoykahavrqcdza/Build/Products/Debug-iphoneos/TestMonkey.app/WeChat
'函数基地址 = 虚拟内存中的函数地址 - ASLR偏移地址
frame #1: 0x000000010324c3f8 WeChat`___lldb_unnamed_symbol15009$$WeChat + 460
frame #2: 0x0000000104d0c228 WeChat`___lldb_unnamed_symbol100217$$WeChat + 356
frame #3: 0x0000000102e2a538 WeChat`___lldb_unnamed_symbol239$$WeChat + 476
frame #4: 0x0000000102ecf75c WeChat`___lldb_unnamed_symbol1834$$WeChat + 552
’在Hopper中,按G键,跳转到指定地址,查看这个地址对应的函数
frame #0:[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:]
frame #1:[BaseMsgContentLogicController DidAddMsg:]
frame #2:[BaseMsgContentLogicController OnAddMsg:MsgWrap:]
frame #3:MMCommon`_callExtension + 480 //扩展函数,排除
frame #4:[CMessageMgr MainThreadNotifyToExt:]
- 4 .每次收到微信消息,就会调用
(void)addMessageNode:(id)arg1 layout:(_Bool)arg2 addMoreMsg:(_Bool)arg3
方法,而这个方法又会调用[CMessageMgr MainThreadNotifyToExt:]
方法,通过观察类名,我们可以判断出,CMessageMgr
类就是处理消息的类,使用OCMethodTrace,让CMessageMgr
类的所有方法打印参数和返回值,进行观察
[[MDMethodTrace sharedInstance] addClassTrace:@"CMessageMgr"];
- 5 .经过观察,我们发现,
CMessageMgr
类最终又会去调用本类中的-(void)AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2
方法去处理消息,我们对此方法进行hook
,如下所示,经过多次发送消息,可以得知,红包消息的Type=49
//-(void)AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2
CHDeclareClass(CMessageMgr)
CHOptimizedMethod2(self,void,CMessageMgr,AsyncOnAddMsg,id,arg1,MsgWrap,id,arg2){
CHSuper2(CMessageMgr, AsyncOnAddMsg, arg1, MsgWrap, arg2);
NSLog(@"CMessageMgr-hook-成功");
NSLog(@"AsyncOnAddMsg的第一个参数 = %@ , 第二个参数 = %@", arg1, arg2);
NSLog(@"AsyncOnAddMsg的第一个参数的类型 = %@ , 第二个参数的类型 = %@", [arg1 class], [arg2 class]);
}
CHConstructor{
CHLoadLateClass(CMessageMgr);
CHClassHook2(CMessageMgr,AsyncOnAddMsg,MsgWrap);
}
打印结果是:
AsyncOnAddMsg的第一个参数 = 17385678347@chatroom , 第二个参数 = {m_uiMesLocalID=2066, m_ui64MesSvrID=7603262124305168898, m_nsFromUsr=17385678347@chatroom, m_nsToUsr=wxi*l21~19, m_uiStatus=3, type=47, createTime=1590045768 msgSource="695455296
1
484
"}
AsyncOnAddMsg的第一个参数的类型 = __NSCFString , 第二个参数的类型 = CMessageWrap
总结一下:上述过程其实非常简单,就是找到聊天界面对应的类BaseMsgContentViewController
,打印出此类的函数调用流程,发现发送消息时,会调用- (void)addMessageNode:(id)arg1 layout:(_Bool)arg2 addMoreMsg:(_Bool)arg3
方法,而addMessageNode
是被[CMessageMgr MainThreadNotifyToExt:]
方法调用的,通过打印追踪,又发现MainThreadNotifyToExt
被[CMessageMgr AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2]
调用的 ,hook此方法,找出红包消息类型,调用流程如下所示:
微信发送/接受消息时:
先调用这个:[CMessageMgr AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2]
->
再调用这个:[CMessageMgr MainThreadNotifyToExt:]
->
再调用这个:[BaseMsgContentViewController addMessageNode:(id)arg1 layout:(_Bool)arg2 addMoreMsg:(_Bool)arg3:]
三、动态调试微信,分析抢红包的流程
接收消息的方法已经找到了,接下来只需要在接受消息的方法里,调用抢红包的方法,就可以实现自动抢红包了
1 .使用
FLEXLoader
,找到抢红包的View是WCRedEnvelopesReceiveHomeView
,使用logify.pl
,打印函数调用流程,发现会调用OnOpenRedEnvelopes
函数-
2 .那么
OnOpenRedEnvelopes
函数内部究竟做了什么呢?我们就需要查看汇编代码了,使用Hopper
打开WeChat可执行文件,搜索函数[WCRedEnvelopesReceiveHomeView OnOpenRedEnvelopes]
,就可以看到如下汇编代码,发现内部调用了WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes
方法,给这三个方法都打上断点,调试发现其调用了
[WCRedEnvelopesReceiveControlLogic WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes]
方法
-
3 .通过
Hopper
查看[WCRedEnvelopesReceiveControlLogic WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes]
方法的伪代码,如下所示,可以得知,这个方法拼接了抢红包所需的参数,并且通过[[MMServiceCenter defaultCenter] getService: "WCPayLogicMgr"]
,获取了WXPayLogicMgr
的实例对象,并调用[WCPayLogicMgr checkHongbaoOpenLicense:acceptCallback:denyCallback:]
方法
-
3 .我们继续用Hopper查看
[WCPayLogicMgr的 checkHongbaoOpenLicense:acceptCallback:denyCallback:]
方法的汇编代码,并对此方法进行hook
,发现微信为了防止逆向,特意做了消息转发,在acceptCallback
中,把消息转发给了[WASearchFromGlobalProxy的 onOpenWeAppPage:]
方法,然后在这个方法内部又调用了[WCRedEnvelopesLogicMgr的 OpenRedEnvelopesRequest:]
方法
-
4 .从上面的分析,我们知道了抢红包最终会调用
[WCRedEnvelopesLogicMgr的 OpenRedEnvelopesRequest:]
方法,那么可不可以在接收到红包消息后,直接调用这个方法呢,答案是不可以,因为通过打印了这个方法的参数,我们可以得知,想要安全的调用这个参数必须凑齐以下参数,而在收到消息的[CMessageMgr AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2]
方法里,是无法获取到timingIdentifier
参数的,所以我们得想办法获取到timingIdentifier
,然后在调用[WCRedEnvelopesLogicMgr的 OpenRedEnvelopesRequest:]
5 .我们通过
OCMethodTrace
打印出来WCRedEnvelopesLogicMgr
所有方法的调用,发现每次抢红包时,都会走以下流程,通过观察类名,我们大致可以判断出,每次抢红包,会先发起查询红包请求,然后得到红包返回的CommonResponse,然后再去发起打开红包请求,所以可以判断出timingIdentifier
参数应该在OnWCToHongbaoCommonResponse
返回过来的
抢红包时,WCRedEnvelopesLogicMgr类的调用顺序:
ReceiverQueryRedEnvelopesRequest
GetHongbaoBusinessRequest
OnWCToHongbaoCommonResponse
OpenRedEnvelopesRequest
GetHongbaoBusinessRequest
OnWCToHongbaoCommonResponse
addReceiveSystemMsgWithDic
-
6 .通过上述动态分析,我们大致可以得出以下逆向逻辑,分为两步:
第一步:首先在收到红包消息的
[CMessageMgr的 AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2]
方法中,调用[WCRedEnvelopesLogicMgr的 ReceiverQueryRedEnvelopesRequest]
去发起查询红包的请求第二步:然后在
[WCRedEnvelopesLogicMgr的 OnWCToHongbaoCommonResponse]
回调方法中,拿到timingIdentifier
参数,与其他参数拼接在一起后,再调用[WCRedEnvelopesLogicMgr的 OpenRedEnvelopesRequest]
方法
总结一下:逆向工程的最核心的地方就是动态调试分析逻辑的过程,这一步做完,剩下写代码反而是最简单的一个环节了
四、根据动态调试的结果,编写逆向代码
-
- 安装好MonkeyDev后,使用
Xcode
新建一个MonkeyApp
项目,项目名称叫做:WXTweak
,将脱壳后的微信ipa
文件,拖入TargetApp
文件夹
- 安装好MonkeyDev后,使用
-
- 在
WXTweakDylib.m
中使用 CaptainHook语法,对接受消息的方法和得到红包的通用响应的方法进行Hook
- 在
-
- 仿照
[WCRedEnvelopesReceiveControlLogic的 WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes]
中获取参数的伪代码,在接收消息的[CMessageMgr的 AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2]
中进行拼接,然后调用[WCRedEnvelopesLogicMgr的 ReceiverQueryRedEnvelopesRequest]
去发起查询红包的请求,如下所示:
- 仿照
//-(void)AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2
CHDeclareClass(CMessageMgr)
CHOptimizedMethod2(self, void, CMessageMgr, AsyncOnAddMsg, NSString *, msg, MsgWrap, CMessageWrap *, wrap){
CHSuper2(CMessageMgr, AsyncOnAddMsg, msg, MsgWrap, wrap);
NSLog(@"CMessageMgr-hook-成功");
NSInteger uiMessageType = [wrap m_uiMessageType];
BOOL isAutoRed = [SP_Defaults boolForKey:SP_AutoKey];
if (uiMessageType == 49 && isAutoRed){
//收到红包消息
NSString *nsFromUsr = [wrap m_nsFromUsr];
WCPayInfoItem *payInfoItem = [wrap m_oWCPayInfoItem];
if (payInfoItem == nil){
return;
}
NSString * m_c2cNativeUrl = [payInfoItem m_c2cNativeUrl];
if (m_c2cNativeUrl == nil){
NSLog(@"m_c2cNativeUrl是nil !!!!!!!!!");
return;
}
NSInteger length = [@"wxpay://c2cbizmessagehandler/hongbao/receivehongbao?" length];
NSString *subString = [m_c2cNativeUrl substringFromIndex: length];
NSDictionary *dict = [objc_getClass("WCBizUtil") dictionaryWithDecodedComponets:subString separator:@"&"];
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionary];
[mutableDict setObject:@"1" forKey:@"msgType"];
NSString *sendId = dict[@"sendid"];
[mutableDict safeSetObject:sendId forKey:@"sendId"];
NSString *channelId = dict[@"channelid"];
[mutableDict safeSetObject:channelId forKey:@"channelId"];
Class contactMgrClass = [objc_getClass("CContactMgr") class];
CContactMgr *service = [[objc_getClass("MMServiceCenter") defaultCenter] getService:contactMgrClass];
CContact *contact = [service getSelfContact];
NSString *displayName = [contact getContactDisplayName];
[mutableDict safeSetObject:displayName forKey:@"nickName"];
NSString *headerImg = [contact m_nsHeadImgUrl];
[mutableDict safeSetObject:headerImg forKey:@"headImg"];
id nativeUrl = [payInfoItem m_c2cNativeUrl];
[mutableDict safeSetObject:nativeUrl forKey:@"nativeUrl"];
BOOL (^isSender)() = ^BOOL() {
return [wrap.m_nsFromUsr isEqualToString:contact.m_nsUsrName];
};
/** 是否自己在群聊中发消息 */
BOOL (^isGroupSender)() = ^BOOL() {
return isSender() && [wrap.m_nsToUsr rangeOfString:@"chatroom"].location != NSNotFound;
};
NSString * sessionUserName = isGroupSender() ? wrap.m_nsToUsr : wrap.m_nsFromUsr;
[mutableDict safeSetObject:sessionUserName forKey:@"sessionUserName"];
if ([nsFromUsr hasSuffix:@"@chatroom"]){
//群红包
[mutableDict safeSetObject:@"0" forKey:@"inWay"]; //0:群聊,1:单聊
}else {
//个人红包
[mutableDict safeSetObject:@"1" forKey:@"inWay"]; //0:群聊,1:单聊
}
[mutableDict safeSetObject:@"0" forKey:@"agreeDuty"];
if (sendId.length > 0) {
SPRedParameter *redParameter = [[SPRedParameter alloc] init];
redParameter.params = mutableDict;
[[SPRedManager sharedInstance] addParams:redParameter];
}
NSLog(@"SPRedManager------mutableDict=%@",mutableDict);
Class redEnvelopesClass = [objc_getClass("WCRedEnvelopesLogicMgr") class];
WCRedEnvelopesLogicMgr *redEnvelopesLogicMgr = [[objc_getClass("MMServiceCenter") defaultCenter] getService: redEnvelopesClass];
[redEnvelopesLogicMgr ReceiverQueryRedEnvelopesRequest:mutableDict];
}
}
CHConstructor{
CHLoadLateClass(CMessageMgr);
CHClassHook2(CMessageMgr,AsyncOnAddMsg,MsgWrap);
}
-
- 接下来Hook
WCRedEnvelopesLogicMgr类的 OnWCToHongbaoCommonResponse:(HongBaoRes *)hongBaoRes Request:(HongBaoReq *)hongBaoReq
方法,由于前边发起了查询红包请求,所以一定会回调红包通用响应这个方法,我们在这个方法里拼接好红包参数,然后调用打开红包请求的方法[WCRedEnvelopesLogicMgr类的 OpenRedEnvelopesRequest:redParameter.params]
方法,如下所示:
- 接下来Hook
//- (void)OnWCToHongbaoCommonResponse:(HongBaoRes *)hongBaoRes Request:(HongBaoReq *)hongBaoReq{
CHDeclareClass(WCRedEnvelopesLogicMgr)
CHOptimizedMethod2(self, void, WCRedEnvelopesLogicMgr, OnWCToHongbaoCommonResponse, HongBaoRes*, arg1, Request, HongBaoReq *, arg2){
CHSuper2(WCRedEnvelopesLogicMgr, OnWCToHongbaoCommonResponse, arg1, Request, arg2);
NSDictionary *bufferDic = [[[NSString alloc] initWithData:arg1.retText.buffer encoding:NSUTF8StringEncoding] JSONDictionary];
if (arg1 == nil || bufferDic == nil){
return;
}
if (arg2 == nil){
return;
}
if (arg2.cgiCmd == 3){
int receiveStatus = [bufferDic[@"receiveStatus"] intValue];
int hbStatus = [bufferDic[@"hbStatus"] intValue];
if (receiveStatus == 0 && hbStatus == 2){
// 没有timingIdentifier字段会被判定为使用外挂
NSString *timingIdentifier = bufferDic[@"timingIdentifier"];
NSString *sendId = bufferDic[@"sendId"];
if (sendId.length > 0 && timingIdentifier.length > 0){
SPRedParameter *redParameter = [[SPRedManager sharedInstance] getParams:sendId];
if (redParameter != nil){
redParameter.timingIdentifier = timingIdentifier;
// 抢的太快也会被判定为使用外挂
sleep(1);
Class redLogicMgrClass = [objc_getClass("WCRedEnvelopesLogicMgr") class];
WCRedEnvelopesLogicMgr *redEnvelopesLogicMgr = [[[objc_getClass("MMServiceCenter") class] defaultCenter] getService: redLogicMgrClass];
if (nil != redEnvelopesLogicMgr){
[redEnvelopesLogicMgr OpenRedEnvelopesRequest:redParameter.params];
}
}
}
}
}
}
CHConstructor{
CHLoadLateClass(WCRedEnvelopesLogicMgr);
CHClassHook2(WCRedEnvelopesLogicMgr,OnWCToHongbaoCommonResponse,Request);
}
-
- 到此整个自动抢红包的核心代码都写完了,我们还可以加上一个开关,控制是否开启自动抢红包,这就非常简单了,这里就不贴出来了,全部代码都放在
github
上了,点击这里查看WXTweak
- 到此整个自动抢红包的核心代码都写完了,我们还可以加上一个开关,控制是否开启自动抢红包,这就非常简单了,这里就不贴出来了,全部代码都放在
五、将动态库注入到微信,并重签名,安装到非越狱手机
因为MonkeyDev已经帮我们封装好了codesign命令重签名
,所以我们只需要选择好证书就可以了,过程如下:
在Xcode修改
WXTweak
项目的Bundle ID为com.tencent.xin
,选择好自己的付费开发证书,在Edit Scheme
中设置为Release
模式,Command+R
安装到手机即可效果图: