我是如何利用Xcode调试开发微信消息预览插件的

作者 Corbin Chen 日期 2016-02-26
我是如何利用Xcode调试开发微信消息预览插件的

平时在微信使用时,经常出现这样的场景:阅读公众号文章时,突然一条好友消息来了。这时一直很纠结,我该停止阅读还是继续阅读。假如我可以预览消息的话,甚至回复消息后快速回来继续阅读。那太好不过了。
学习过iOS逆向开发的话,利用theos在越狱机器上实现还是可以的。但由于日常使用的是非越狱机器,鱼和熊掌都想要,只好在万能的google上寻找资料,终于找到了insert_dylib工具和念茜的博客上动态库注入相关知识。OK,开工!

效果图

最终效果图

需求分析

  1. 公众号文章界面(网页):收到消息后,显示消息内容
  2. 公众号文章界面(网页):点击消息内容进入对应聊天界面
  3. 聊天界面:点击网页标志,跳回公众号文章界面(网页)

代码分析

结合需求,需要hook的主要是微信消息通知Method,聊天界面ViewController,网页ViewController。利用工具class-dump, Hopper Disassembler很快定位出需要hook的微信代码,-[CMessageMgr AsyncOnAddMsg:MsgWrap:] -[BaseMsgContentViewController viewDidLoad] -[MMWebViewController viewDidLoad]

磨刀霍霍

定位出hook代码段,接下来要做的就是写代码了。

  1. Xcode现在支持建立动态库工程,但生成的是framework,可以通过修改工程文件下的project.pbxproj productType = "com.apple.product-type.framework"; => productType = "com.apple.product-type.library.dynamic"
    xcode建立framework图
  2. 利用iOSOpenDev也可以快速生成动态库工程。
    iosopendev建立dylib图

这里注意要设置好签名证书,后续可能因为证书问题导致失败。

- (void)cb_AsyncOnAddMsg:(NSString *)msg MsgWrap:(CMessageWrap *)wrap {
[self cb_AsyncOnAddMsg:msg MsgWrap:wrap];
[CBNewestMsgManager sharedInstance].username = msg;
[CBNewestMsgManager sharedInstance].content = wrap.m_nsContent;
[[NSNotificationCenter defaultCenter] postNotificationName:CBWeChatNewMessageNotification object:nil];
}
- (void)cb_msgContentViewControllerViewDidLoad {
[self cb_msgContentViewControllerViewDidLoad];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(self.view.frame.size.width - 40, 74, 40, 40)];
UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"WeChatMsgPreview_safari@2x" ofType:@"png"]];
[button setImage:image forState:UIControlStateNormal];
[button addTarget:self action:@selector(backToWebViewController) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
- (void)cb_webViewControllerViewDidLoad {
[self cb_webViewControllerViewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cb_didReceiveNewMessage) name:CBWeChatNewMessageNotification object:nil];
}
- (void)cb_didReceiveNewMessage {
NSString *username = [CBNewestMsgManager sharedInstance].username;
NSString *content = [CBNewestMsgManager sharedInstance].content;
CContactMgr *contactMgr = [[objc_getClass("MMServiceCenter") defaultCenter] getService:[objc_getClass("CContactMgr") class]];
CContact *contact = [contactMgr getContactByName:username];
dispatch_async(dispatch_get_main_queue(), ^{
NSString *text = [NSString stringWithFormat:@" %@: %@ ", contact.m_nsNickName, content];
[CBMessageHud showHUDInView:self.view text:text target:self action:@selector(backToMsgContentViewController)];
});
}
- (void)backToWebViewController {
NSArray *webViewViewControllers = [CBNewestMsgManager sharedInstance].webViewViewControllers;
if (webViewViewControllers) {
[[objc_getClass("CAppViewControllerManager") getCurrentNavigationController] setViewControllers:webViewViewControllers animated:YES];
}
}
- (void)backToMsgContentViewController {
// 返回聊天界面ViewController前记录当前navigationController的VC堆栈,以便快速返回
NSArray *webViewViewControllers = [objc_getClass("CAppViewControllerManager") getCurrentNavigationController].viewControllers;
[CBNewestMsgManager sharedInstance].webViewViewControllers = webViewViewControllers;
// 返回rootViewController
UINavigationController *navVC = [objc_getClass("CAppViewControllerManager") getCurrentNavigationController];
[navVC popToRootViewControllerAnimated:NO];
// 进入聊天界面ViewController
NSString *username = [CBNewestMsgManager sharedInstance].username;
CContactMgr *contactMgr = [[objc_getClass("MMServiceCenter") defaultCenter] getService:[objc_getClass("CContactMgr") class]];
CContact *contact = [contactMgr getContactByName:username];
MMMsgLogicManager *logicMgr = [[objc_getClass("MMServiceCenter") defaultCenter] getService:[objc_getClass("MMMsgLogicManager") class]];
[logicMgr PushOtherBaseMsgControllerByContact:contact navigationController:navVC animated:YES];
}

最后关键的一步~

#define CBHookInstanceMethod(classname, ori_sel, new_sel) \
\
{ \
Class class = objc_getClass(#classname); \
Method ori_method = class_getInstanceMethod(class, ori_sel); \
Method new_method = class_getInstanceMethod(class, new_sel); \
method_exchangeImplementations(ori_method, new_method); \
} \
\
static void __attribute__((constructor)) initialize(void) {
CBHookInstanceMethod(CMessageMgr, @selector(AsyncOnAddMsg:MsgWrap:), @selector(cb_AsyncOnAddMsg:MsgWrap:));
CBHookInstanceMethod(BaseMsgContentViewController, @selector(viewDidLoad), @selector(cb_msgContentViewControllerViewDidLoad));
CBHookInstanceMethod(MMWebViewController, @selector(viewDidLoad), @selector(cb_webViewControllerViewDidLoad));
}

好了,command+B 成功生成动态库文件,下一步,利用insert_dylib修改微信可执行文件,重签名,生成新的微信app,安装到手机。嗯嗯,这样文章到这里就结束了~~
慢着,真正开发时哪会这么简单,代码一次成功。一旦代码出现问题,我们需要一直手动重复这样的工作:修改代码,生成dylib,修改微信可执行文件,重签名,生成新的app,安装到手机。
注意注意,博文的标题里有“调试”,调试!!!怎么做呢?

偷天换日

细心观察可以发现

  1. 任意一个app工程,run后在Derived Data文件夹都有对应的.app文件
  2. 在Build Phases中增加Run Script,可以在编译工程后执行自定义脚本。

于是,一招偷天换日招数就想出来了(通过脚本,在编译工程后,利用新生成的动态库生成WeChat.app, 替换原有目录下的app文件)

  1. 在原有工程中增加Application Target
  2. 在Build Phases中设置Target Dependencies,增加dylib,确保每次run app都会编译最新的dylib
    工程设置图
  3. 然后增加Run Script(修改微信可执行文件,重签名,生成新的app)
    接下来的事情(安装app,打开手机app,lldb调试)就交给Xcode做了。
#!/bin/bash
BUNDLEIDENTIFIER=com.tencent.xin
APPLICATIONIDENTIFIER=***.${BUNDLEIDENTIFIER}
WECHATFILEPATH=***/apps/WeChat
LIBNAME=$(find *.dylib)
TEMPDIR=$(mktemp -d)
ORIGINDIR=$(pwd)
# 0.get argv
if [ x$1 != x ]
then
BUNDLEIDENTIFIER=$1
fi
# 1.unzip ipa
if [ $arch == "arm64" ]
then
unzip -qo ${WECHATFILEPATH}/WeChat-dump-arm64.ipa -d $TEMPDIR
else
unzip -qo ${WECHATFILEPATH}/WeChat-dump-armv7.ipa -d $TEMPDIR
fi
# 2.copy files
cp ${WECHATFILEPATH}/embedded.mobileprovision $TEMPDIR/
cp ${WECHATFILEPATH}/entitlements.plist $TEMPDIR/
cp ${LIBNAME} $TEMPDIR/
# 3.resign
cd $TEMPDIR
plutil -replace application-identifier -string ${APPLICATIONIDENTIFIER} entitlements.plist
plutil -replace CFBundleIdentifier -string ${BUNDLEIDENTIFIER} Payload/WeChat.app/Info.plist
mv ${LIBNAME} Payload/WeChat.app/
insert_dylib --all-yes @executable_path/${LIBNAME} Payload/WeChat.app/WeChat
mv Payload/WeChat.app/WeChat_patched Payload/WeChat.app/WeChat
chmod +x Payload/WeChat.app/WeChat
rm -rf Payload/WeChat.app/_CodeSignature
rm -rf Payload/WeChat.app/PlugIns
rm -rf Payload/WeChat.app/Watch
cp embedded.mobileprovision Payload/WeChat.app/
codesign -fs "iPhone Developer: *** (***)" --no-strict --entitlements=entitlements.plist Payload/WeChat.app/${LIBNAME}
codesign -fs "iPhone Developer: *** (***)" --no-strict --entitlements=entitlements.plist Payload/WeChat.app
# 4.end
mv Payload/WeChat.app ${ORIGINDIR}
rm -rf ${TEMPDIR}

调试效果图

常见问题

dyld: Library not loaded: @executable_path/libWeChatMsgPreview.dylib
Referenced from: /var/mobile/Containers/Bundle/Application/55148CD1-0D6E-4F6B-B55C-08261695B408/WeChat.app/WeChat
Reason: image not found

原因:没拷贝libWeChatMsgPreview.dylib到WeChat.app目录下

dyld: Library not loaded: @executable_path/libWeChatMsgPreview.dylib
Referenced from: /var/mobile/Containers/Bundle/Application/F62EF4DE-7A8E-4564-8839-7FED32FB0927/WeChat.app/WeChat
Reason: no suitable image found. Did find:
/var/mobile/Containers/Bundle/Application/F62EF4DE-7A8E-4564-8839-7FED32FB0927/WeChat.app/libWeChatMsgPreview.dylib: mmap() errno=1 validating first page of '/var/mobile/Containers/Bundle/Application/F62EF4DE-7A8E-4564-8839-7FED32FB0927/WeChat.app/libWeChatMsgPreview.dylib'
/private/var/mobile/Containers/Bundle/Application/F62EF4DE-7A8E-4564-8839-7FED32FB0927/WeChat.app/libWeChatMsgPreview.dylib: mmap() errno=1 validating first page of '/private/var/mobile/Containers/Bundle/Application/F62EF4DE-7A8E-4564-8839-7FED32FB0927/WeChat.app/libWeChatMsgPreview.dylib'

原因:签名不对,需保持重签名时codesign -fs "iPhone Developer: *** (***)" --no-strict --entitlements=entitlements.plist Payload/WeChat.app/${LIBNAME} codesign -fs "iPhone Developer: *** (***)" --no-strict --entitlements=entitlements.plist Payload/WeChat.app证书一致

脚本中涉及到的WeChat-dump-arm64.ipa需要从越狱机器中提取。对App Store App进行重签名–解密[上]

相关资料

insert_dylib
iOS安全攻防(十二):iOS7的动态库注入