分析一个App需要的技术手段

2,144 阅读9分钟
原文链接: mp.weixin.qq.com

这篇文章算是学习逆向时的总结,文章中涉及到分析一个 App需要的技术手段。以下内容是 iWeChat 项目的摘录,省略了一大部分内容,https://github.com/lefex/iWeChat/blob/master/README.md。下面是完整目录:

APP信息

1.砸壳 - ipa 获取

首先第一步是获取一个破解的 ipa 包,我们可以通过下面这几种方式获取:方式一:iTunes苹果既然在高版本的 iTunes 取消了获取 ipa 包的入口,那我们就想办法降级处理。需要下载低版本的 iTunes。下载。下载完后,安装,第一次启动的时候按住 option 键,这样才不会报错,安装完成后,即可下载应用的 ipa 包。下载完成后,在应用的图标上按右键,show in finder 即可找到 ipa 包。

方式二:pp助手

电脑安装一个 pp助手客户端,直接下载越狱应用,下载完成后,即可在“本地应用”中找打 APP 的 ipa 包。需要强调一点,这种方式下载的应用是解密后的 ipa。

方式三:抓包

在 Mac 中的 iTunes 中下载应用,通过 Charles 抓包获取到 ipa 包的下载地址,直接在浏览器中下载,下载地址是在 p52-buy.itunes 这个域名下。获取到 ipa 包后就需要砸壳。如果从越狱助手上下载的则不需要砸壳。

2.手机越狱我使用的是叉叉越狱助手,按照他的流程,即可完成越狱。安装完成会出现 Cydia 这个应用,你可以理解成它是越狱应用商店。有时候比较坑,越狱时可能会出现黑屏。需要安装 Cydia Impactor ,它用来安装软件,下载完成后,需要把你所需要的安装包拖到 Cydia Impactor,安装过程中如果没有出现错误,说明安装成功。应用会保存到这个目录下

/var/mobile/Containers/Data/Application/

登录

安装 OpenSSL(在Cydia中安装)sudo ssh root@172.24.94.140<!--修改密码默认为 alpine 为 (q--4)-->TCZYde-iPhone:~ root# passwd

3.头文件导出

class-dump 这个工具用来查看某个APP的头文件。只需要找到第三方APP的 xxx.app 文件,然后执行 class-dump 命令即可。不过在执行 class-dump 命令前,需要确保 xxx.app 是砸过壳的,从 APPStore下载的 xxx.app 文件是经过加密处理的,可以直接从各大越狱市场上下载第三方 xxx.app 文件,从越狱市场下载的 xxx.app 已被破解。可以直接使用 class-dump 导出头文件。下载 class-dump 后把 class-dump 导入 /usr/local/bi 目录下,并执行下列命令:

sudo chmod 777/usr/local/bin/class-dump

执行 class-dump 命令:

class-dump -H [xxx.app所在的位置] -o [头文件导出的位置]比如:class-dump -H Lefex.app -o lefexheaderclass-dump -H /Users/daredos/Desktop/微信-6.3.23\(越狱应用\)/Payload/WeChat.app  -o /Users/daredos/Desktop/w

使用 class-dump 命令导出头文件有以下特点:1.不管 .h 还是 .m 文件中的属性和方法都会被导出;2.某个类的类别中的方法也会被导出,导出到源文件中,比如 ViewController (Navigation) 中的方法被导出到 ViewController 中;3.实现的协议也会被导出,比如 ViewControllerDelegate 的方法被导出到 ViewController 中,如果 ViewController 不实现 ViewControllerDelegate 协议讲不会被导出; 

4.协议中定义的方法不会被导出,只会导出到实现协议的类中;

4.MonkeyDev自动导出头文件工程已集成class-dump导出可执行文件OC头文件的功能,可在build settings最下面开启该功能,在 User-Defined 下添加 MONKEYDEV_CLASS_DUMP 值为 YES。

5.第三方库

如果APP使用了三方库,可以输入PodsDummy来快速找到使用的第三方库和私有库;

6. UI

除了头文件外,研究第三方 APP 另一个比较重要的点就是查看 UI。可以使用 Reveal 查看视图层级。使用 MonkeyDev 可以在非越狱的手机上运行 Reveal。MonkeyDev 默认集成是最新版本,需要把自己的 RevealServer.framework放到/opt/MonkeyDev/frameworks下(打开 Reveal,点击 reveal - help - show reveal in finder 即可找到 RevealServer.framework),这样就可以查看时图层级。如果 Reveal 过期了,直接修改电脑时间为可以继续使用。

7.沙盒目录

(1)直接运行App通过Xcode 导出

沙盒目录结构是什么,每个文件夹下面保存了那些数据。非越狱手机,通过Xcode直接导出(windows->devices and simulators - 设置按钮 - Download Container ...),这个时间比较长,需要耐心等待。

(2)使用iFunBox工具:

这个工具比较强大,可以直接把越狱设备的内容拷贝到电脑上,一图胜千言,直接看图吧。

8.Pod 集成

如果想使用第三方库咋么办?直接通过 pod init,然后在 Podfile 里添加你想使用的第三方库即可。

target 'xxxDylib' do  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks  # use_frameworks!  # Pods for KuaiXuoyeDylib  pod 'xxx'end

9.查看网络请求数据

有时候想快速地查看某个 APP 中的网络请求结果,抓包可能是一个不错的选择,但是遇到HTTPS的请求,就比较费事。其实我们可以通过逆向来查看网络请求,主要有两种方式:(1)找到某个应用中网络请求的统一出口,然后 Hook 掉这个方法,直接拿到数据。不过找到 APP 网络请求封装的类有时候比较难。教你一招,非常容易,找到某个页面中含有网络请求的类,使用 Hopper 工具查看伪代码,非常容易定位具体的网络请求类;(2)使用网络工具直接集成到第三方APP中,通过工具查看网络请求,推荐Flex,它可以查看网络请求;

10.数据库

直接跑项目,导出沙盒目录。

工具使用说明

1.CaptainHook Hook 代码

// 无参数CHDeclareClass(WCActionSheet)CHOptimizedMethod0(self, NSArray *, WCActionSheet, buttonTitleList){    NSArray *titles = CHSuper0(WCActionSheet, buttonTitleList);    return titles;}CHConstructor{    CHLoadLateClass(WCActionSheet);    CHClassHook0(WCActionSheet, buttonTitleList);}
// 一个参数CHDeclareClass(MMUIImageView);CHOptimizedMethod1(self, void, MMUIImageView, setImage, NSString *, imageurl){    NSLog(@"setImage: %@", imageurl);    CHSuper1(MMUIImageView, setImage, imageurl);}CHConstructor{        CHLoadLateClass(MMUIImageView);    CHClassHook1(MMUIImageView, setImage);}
@interface Lefex : NSObject@property (nonatomic, copy) NSString *city;+ (Lefex *)empty;- (void)updateNickName:(NSString *)nickName;- (void)updateNickName:(NSString *)nickName age:(int)age;- (void)requestNickNameForId:(NSString *)userId completion:(void(^)(NSString *))block;@end
@synthesize city = _city;@implementation Lefex+ (Lefex *)empty {    NSLog(@"orgin - %@", NSStringFromSelector(_cmd));    return [Lefex new];}- (void)updateNickName:(NSString *)nickName {    NSLog(@"orgin - %@", NSStringFromSelector(_cmd));}- (void)updateNickName:(NSString *)nickName age:(int)age {    NSLog(@"orgin - %@", NSStringFromSelector(_cmd));}- (void)requestNickNameForId:(NSString *)userId completion:(void(^)(NSString *))block {    NSLog(@"orgin - %@", NSStringFromSelector(_cmd));    if (block) {        block(@"lefe_x");    }}- (void)setCity:(NSString *)city {    _city = city;}- (NSString *)city {    return _city;}@end
typedef void(^RequestBlock)(NSString *);// hook某个类时需要先声明CHDeclareClass(Lefex)// Hook 类方法CHOptimizedClassMethod0(self, Lefex *, Lefex, empty){    Lefex *me = CHSuper0(Lefex, empty);    NSLog(@"Hook empty");    return me;}// Hook 一个参数的方法CHOptimizedMethod1(self, void, Lefex, updateNickName, NSString *, name) {    CHSuper1(Lefex, updateNickName, name);    NSLog(@"hook updateNickName");}// Hook属性,hook对应的get和set方法// hook set 方法CHOptimizedMethod1(self, void, Lefex, setCity, NSString *, city) {    CHSuper1(Lefex, setCity, @"BeiJing");    NSLog(@"hook setCity");}// hook get 方法CHOptimizedMethod0(self, void, Lefex, city) {    NSLog(@"hook city");    return CHSuper0(Lefex, city);}// Hook 两个参数的方法CHOptimizedMethod2(self, void, Lefex, updateNickName, NSString *, name, age, int, age) {    CHSuper2(Lefex, updateNickName, name, age, age);    NSLog(@"hook updateNickName: age");}// Hook 带有block的方法,需要知道 block 中的参数个数CHOptimizedMethod2(self, void, Lefex, requestNickNameForId, NSString *, userId, completion, RequestBlock, block) {    RequestBlock nicknameBlock = ^(NSString *nickname) {        NSLog(@"hook callback :%@", nickname);        if (block) {            block(nickname);        }    };        CHSuper2(Lefex, requestNickNameForId, userId, completion, nicknameBlock);    NSLog(@"hook requestNickNameForId: age");}// Hook 参数是二级指针的方法CHOptimizedMethod1(self, void, Lefex, queryLocation, NSString **, name) {    CHSuper1(Lefex, queryLocation, name);    NSLog(@"hook queryLocation: %@", *name);}CHConstructor {    // 导入类才能够使用    // linkable    CHLoadClass(Lefex);    // un linkable//    CHLoadLateClass(Lefex);    CHClassHook1(Lefex, setCity);    CHClassHook0(Lefex, city);    CHClassHook0(Lefex, empty);    CHClassHook1(Lefex, updateNickName);    CHClassHook2(Lefex, updateNickName, age);    CHClassHook2(Lefex, requestNickNameForId, completion);    CHClassHook1(Lefex, queryLocation);}

2.Logos 语法

// 要 hook 的类%hook ClassName// hook 类方法+ (id)sharedInstance{  %log;  return %orig; // 调用原实现}- (void)messageWithNoReturnAndOneArgument:(id)originalArgument{  %log;    // 调用原实现,可以修改参数  %orig(originalArgument);}- (id)messageWithReturnAndNoArguments{  %log;    // 调用原实现,查看返回值,修改返回值  id originalReturnOfMessage = %orig;  return originalReturnOfMessage;}// 要有结束标签%end

3.调用带有参数为block的方法

@interface NetworkTask : NSObject- (void)requestComplete:(void(^)(NSString *name))completion;- (void)requestID:(NSString *)docId domplete:(void(^)(NSString *name))completion failed:(void(^)(NSError *error))failed;@end// 方法一:只支持一个参数- (void)runTaskForPerform {    NSObject *task = [objc_getClass("NetworkTask") new];    void(^completeBlock)(NSString *) = ^(NSString *name) {        NSLog(@"block: %@", name);    };    [task performSelector:@selector(requestComplete:) withObject:completeBlock];}// 方法二:支持多个参数- (void)runTask {    NSObject *task = [objc_getClass("NetworkTask") new];    SEL selector = NSSelectorFromString(@"requestID:domplete:failed:");    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[objc_getClass("NetworkTask") instanceMethodSignatureForSelector:selector]];    [invocation setSelector:selector];    [invocation setTarget:task];    NSString *docId = @"123";    [invocation setArgument:&docId atIndex:2];    void(^completeBlock)(NSString *) = ^(NSString *name) {        NSLog(@"block invocation: %@", name);    };    [invocation setArgument:&completeBlock atIndex:3];    void(^fliedBlock)(NSError *) = ^(NSError *error) {        NSLog(@"block invocation error: %@", error);    };    [invocation setArgument:&fliedBlock atIndex:4];    [invocation invoke];}// 方法三:支持多个参数- (void)runTaskForMsgSend {    NSString *docId = @"123";        void(^completeBlock)(NSString *) = ^(NSString *name) {        NSLog(@"block objc_msgSend: %@", name);    };        void(^filedBlock)(NSError *) = ^(NSError *error) {        NSLog(@"block objc_msgSend error: %@", error);    };    NSObject *task = [objc_getClass("NetworkTask") new];    SEL selector = NSSelectorFromString(@"requestID:domplete:failed:");    ((void (*)(id, SEL, id, id, id, id))objc_msgSend)(task, selector, docId, completeBlock, filedBlock, NULL);}

4.查找可执行文件技巧

显示包内容后,按 size 大小排列文件,可执行文件一般比较大,名字和app包名字一致(有时候不一致);

5.Cycript调试程序

试想一种场景,我想知道某个第三方 APP 当前页面对应的是哪个 VC,想让某个实例执行某个函数后的效果,打印当前的视图层级,咋么办?其实使用 Cycript 即可解决这几个问题,Cycript是一门脚本语言,可以把某段代码注入到某个进程中。比如我可以把用 Cycript 编写的代码植入到一个运行的 APP 中,这样 APP 就可以执行注入的代码。下面的测试需要安装 MonkeyDev。安装 Cycript 非常简单,直接下载 Cycript,并进入 Cycript 目录下,执行:

./cycript -r 192.168.10.111:6666

192.168.10.111:6666 是手机ip地址,6666是默认的端口。连接成功后,控制台会有:cy#。需要注意手机和电脑需要使用同一Wifi。

当前页面对应的是哪个 VC?获取当前页面是哪个页面时,可以用到响应链的知识。假如SubjectViewController有一个 UITableView, 它的内存地址是 0x106a05c00 ,那么我可以通过下列命令找到当前的VC。

cy# [#0x106a05c00 nextResponder]#"<UIView: 0x105d839d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x1c0635460>>"cy# [#0x105d839d0 nextResponder]#"<SubjectViewController: 0x106a0a200>"

推荐阅读:

图解红黑树的 5 大特征

开启我的前端之路

超越技术