平时在微信使用时,经常出现这样的场景:阅读公众号文章时,突然一条好友消息来了。这时一直很纠结,我该停止阅读还是继续阅读。假如我可以预览消息的话,甚至回复消息后快速回来继续阅读。那太好不过了。
学习过 iOS 逆向开发的话,利用 theos 在越狱机器上实现还是可以的。但由于日常使用的是非越狱机器,鱼和熊掌都想要,只好在万能的 google 上寻找资料,终于找到了 insert_dylib 工具和念茜的博客上动态库注入相关知识。OK,开工!
效果图 
需求分析 
公众号文章界面(网页):收到消息后,显示消息内容 
公众号文章界面(网页):点击消息内容进入对应聊天界面 
聊天界面:点击网页标志,跳回公众号文章界面(网页) 
 
代码分析 结合需求,需要 hook 的主要是微信消息通知 Method,聊天界面 ViewController,网页 ViewController。利用工具 class-dump, Hopper Disassembler 很快定位出需要 hook 的微信代码,-[CMessageMgr AsyncOnAddMsg:MsgWrap:] -[BaseMsgContentViewController viewDidLoad] -[MMWebViewController viewDidLoad]
磨刀霍霍 
定位出 hook 代码段,接下来要做的就是写代码了。 
 
Xcode 现在支持建立动态库工程,但生成的是 framework,可以通过修改工程文件下的 project.pbxproj productType = "com.apple.product-type.framework"; => productType = "com.apple.product-type.library.dynamic"
利用 iOSOpenDev 也可以快速生成动态库工程。 
 
这里注意要设置好签名证书,后续可能因为证书问题导致失败。
- (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,安装到手机。
注意注意,博文的标题里有「调试」,调试!!!怎么做呢?
偷天换日 细心观察可以发现
任意一个 app 工程,run 后在 Derived Data 文件夹都有对应的。app 文件 
在 Build Phases 中增加 Run Script,可以在编译工程后执行自定义脚本。 
 
于是,一招偷天换日招数就想出来了(通过脚本,在编译工程后,利用新生成的动态库生成 WeChat.app, 替换原有目录下的 app 文件)
在原有工程中增加 Application Target 
在 Build Phases 中设置 Target Dependencies,增加 dylib,确保每次 run app 都会编译最新的 dylib 
 
然后增加 Run Script(修改微信可执行文件,重签名,生成新的 app)接下来的事情(安装 app,打开手机 app,lldb 调试)就交给 Xcode 做了。 
 
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 进行重签名–解密 
来源:我是如何利用Xcode调试开发微信消息预览插件的 
The Why·Liam·Blog  by WhyLiam  is licensed under a Creative Commons BY-NC-ND 4.0 International License .
由WhyLiam 创作并维护的Why·Liam·Blog 采用创作共用保留署名-非商业-禁止演绎4.0国际许可证 。
本文首发于Why·Liam·Blog (https://blog.naaln.com) ,版权所有,侵权必究。
本文永久链接:https://blog.naaln.com/2016/03/i-is-how-to-use-xcode-debugging-development-wechat-news-preview-of-the-plugin/