这篇文章算是学习逆向时的总结,文章中涉及到分析一个 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>"
推荐阅读:
超越技术
