我是如何利用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]

磨刀霍霍

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

Xcode现在支持建立动态库工程,但生成的是framework,可以通过修改工程文件下的project.pbxproj productType = "com.apple.product-type.framework"; => productType = "com.apple.product-type.library.dynamic"

  1. 利用iOSOpenDev也可以快速生成动态库工程。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
- (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];

}

最后关键的一步~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#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

  1. 然后增加Run Script(修改微信可执行文件,重签名,生成新的app)接下来的事情(安装app,打开手机app,lldb调试)就交给Xcode做了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

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}

常见问题

1
2
3
4
5
6
7

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目录下

1
2
3
4
5
6
7
8
9
10
11

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/