最近看完了《iOS应用逆向工程分析与实战》,当你手里拿着锤子的时候,整个世界都成了钉子,所以迫不及待的想练练手。正好最近在某视频客户端上跟美剧,有时候想缓存下来离线看,但是由于版权原因,很多视频都不能缓存,所以今天逆向实践的主要目标就是能够缓存有版权的视频。
有人可能会问:你怎么就知道一定能够实现这个目标,万一带版权的视频压根儿就没有提供下载地址你怎么缓存啊?问得好,其实在拿到逆向这把锤子之前,我靠着纯体力已经能够下载到追的美剧了。使用Charles抓包工具获取到剧集的信息(关于Charles的使用可以看这里),即使有版权的视频也会有download_url
字段,然后将每个下载地址复制到迅雷里面下载。还好有了这段痛苦的经历,让我知道今天逆向的目标是可实现的。
使用工具
某助手软件、class-dump、cycript、Charles、IDA、Theos、已越狱iPhone。
分析与实践过程
通过某助手软件下载要进行逆向的视频客户端xxxxVideo.ipa(省去AppStore下载后还需破壳的过程),解压后将二进制文件复制出来,使用class-dump导出头文件:
1
| $ class-dump -H xxxxVideo -o headers
|
用Xcode创建一个新工程,将头文件导入工程:

在越狱的手机上安装该视频客户端,进入到视频播放界面,可以看到下面“缓存”按钮为灰色,很自然的会想到以该按钮为出发点开始分析。

下面通过cycript开始找到该按钮所在类(如果你安装了Reveal,那下面这个查找步骤就可以直接用Reveal实现了)
1 2 3 4 5 6 7 8 9
| $ ssh root@192.168.1.118 ~ root PID TT STAT TIME COMMAND 1438 ?? Ss 0:02.19 /var/mobile/Applications/414D20A3-EA72-4AB4-87E4-5A209F648EAB/xxxxVideo.app/xxxxVideo ~ root
|
所以当前播放界面对应的视图控制器类是VideoDetailViewController
,但是在该头文件中并未找到缓存按钮或者下面的bar,不过下面这个属性引起了我的注意:
1
| @property(retain, nonatomic) VideoDetailBarController *videoDetailBarController;
|
进入到该类对应的头文件,终于找到了下载按钮downloadBtn
,并且很明显可以看出点击该按钮调用- (void)downloadWithButton:(id)arg1
方法。因为缓存按钮有可以缓存和不能缓存两种状态,所以判断是否能缓存应该是在该方法里面实现的,由于没有其他成员变量或方法标记能否缓存,所以这个判断依据应该存在某个对象里面,这时可以注意到dataManager
属性,它是一个数据管理对象,而管理的对象就是videoAlbum
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #import "BaseViewController.h" @class AsynImageView, FollowButton, RequestItem, TTTAttributedLabel, UIButton, UILabel, VideoAlbumDataManager; @interface VideoDetailBarController : BaseViewController { FollowButton *_followBtn; UIButton *_downloadBtn; } @property(retain, nonatomic) UIButton *downloadBtn; @property(retain, nonatomic) FollowButton *followBtn; @property(retain, nonatomic) VideoAlbumDataManager *dataManager; - (void)downloadWithButton:(id)arg1; - (void)updateDownloadBarButtonItem; - (id)videoAlbum; - (id)initWithVideoDetailDataManager:(id)arg1; @end
|
进入VideoAlbum
头文件,好家伙,一个模型类快300多行,而且方法远比属性多,不过眼尖的我一下就找到了我想要的东西——canBeDownloaded
,这个就应该是判断视频能否被缓存的依据了。
1 2 3 4 5 6 7
| @interface VideoAlbum : NSObject - (BOOL)canBeShared; - (BOOL)canBeSubscribed; - (BOOL)canBeDownLoaded; - (BOOL)canBePlayed;
|
马上验证,创建Theos Tweak工程,配置好其他信息:
1 2 3 4 5 6 7 8
| %hook VideoAlbum - (BOOL)canBeDownLoaded { return YES; } %end
|
编译、打包、安装:
1 2
| $ export THEOS_DEVICE_IP=192.168.1.118 $ make package install
|
再次运行该视频客户端,“缓存”按钮恢复正常了,点击后弹出了缓存界面,选择剧集后可以正常下载。

到了这里本文应该就要结束了,嘛,还没玩过瘾?那我们继续折腾吧,看到视频上面登录VIP 30
吗?非VIP用户每个视频前面有30秒的广告,而且VIP专区的视频只能看前面的5分钟,浪费时间就是浪费生命,但是我等屌丝又买不起VIP好吗?那下面我们就告别广告,成为VIP。
因为每次进到“个人资料”时客户端都会进行通讯,应该是获取用户信息,使用Charles抓取到以下信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "attachment": { "status": 0, "msg": "ok", "jifen": 0, "dengji": 0, "uid": xxxxxxxxx, "passport": "yyyyyyyyyy@sina.sohu.com", "nickname": "TracyYih", "smallimg": "http://tp3.sinaimg.cn/1342106870/50/5664617611/1", "mobile": "", "email": "", "birthday": "", "gender": 1, "utype": 31, "token": "1ee4863ec29030730e624afdb401a3e6", "isVip": "0", "vipexpire": "" }, "message": "成功", "debug": null, "status": 200 }
|
看到了吗?isVip
字段为0,在头文件中搜索“isVip”,找到两个类:UserDataModel
和 UserInterface
,将属性与通讯返回的JSON格式对比可以看出,UserDataModel
和 JSON数据一一对应,即为用户信息模型类,而在UserInterface
类中有一个UserDataModel
实例作为属性,还有一些其他的方法。
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
| @interface UserDataModel : NSObject { BOOL isVip; NSString *passport; NSString *password; NSString *nickname; NSString *profileImage; NSString *mobile; NSString *email; NSString *birthday; NSString *requestToken; NSString *vipExpire; NSString *score; NSString *grade; NSString *uid; int gender; int loginTpye; } @property int loginTpye; @property int gender; @property(copy, nonatomic) NSString *uid; @property(copy) NSString *grade; @property(copy) NSString *score; @property(copy) NSString *vipExpire; @property BOOL isVip; @property(copy) NSString *requestToken; @property(copy) NSString *birthday; @property(copy) NSString *email; @property(copy) NSString *mobile; @property(copy) NSString *profileImage; @property(copy) NSString *nickname; @property(copy) NSString *password; @property(copy) NSString *passport; @end
|
所以我们应该关注最基础的模型(UserDataModel
),不管接口返回的用户信息中是否为VIP,我们统一设置他为VIP:
1 2 3 4 5 6 7 8
| %hook UserDataModel - (BOOL)isVip { return YES; } %end
|
编译、打包、安装,重新运行该视频客户端,没有任何变化,应该是哪里出问题。再仔细看一遍UserDataModel
类,发现vipExpire
字段,而接口返回的该字段为空字符串,所以应该是和这个字段有关系,而且我们只知道vipExpire
是一个字符串(应该是个时间),不知道它具体格式,总不能一个个试吧,上IDA。
还是搜索“isVip”,在UserDataModel
的isVip方法中只是简单的存储,没有其他逻辑判断:

再看UserInterface
中的getModelIsVip(isVip)方法,非常有料:

首先获取属性dataModel的isVip属性,如果为NO,直接返回NO,否则进入下面的判断,对应的代码如下:
1 2 3 4 5 6 7
| - (BOOL)getModelIsVip { if (self.dataModel.isVip) { } retrun NO; }
|

接着就判断vipExpire
字段是否为空,这个和我们之前的设想一致,对应代码如下:
1 2 3 4 5 6 7 8 9 10
| - (BOOL)getModelIsVip { if (self.dataModel.isVip) { NSString *vipExpire = self.dataModel.vipExpire; if (vipExpire && vipExpire.length) { } } retrun NO; }
|


判断dateFormatter
属性是否为空,如果为空,创建该对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - (BOOL)getModelIsVip { if (self.dataModel.isVip) { NSString *vipExpire = self.dataModel.vipExpire; if (vipExpire && vipExpire.length) { if (!self.dateFormatter) { self.dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; [self.dateFormatter setLocale:locale]; } } } retrun NO; }
|

比较VIP有效期和当前时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| - (BOOL)getModelIsVip { if (self.dataModel.isVip) { NSString *vipExpire = self.dataModel.vipExpire; if (vipExpire && vipExpire.length) { if (!self.dateFormatter) { self.dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; [self.dateFormatter setLocale:locale]; } NSDate *vipExpireDate = [self.dateFormatter dateFormString:vipExpire]; if ([vipExpireDate compare:[NSDate date]]) { return YES; } } } retrun NO; }
|
到这里我们基本还原了判断是否为VIP的方法,逻辑清楚了,下面我们要实现VIP身份就简单了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #define kYearInterval 31536000.0 %hook UserDataModel - (BOOL)isVip { return YES; } - (NSString *)vipExpire { NSDate *date = [[NSDate date] dateByAddingTimeInterval:kYearInterval]; NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; return [dateFormatter stringFromDate:date]; } %end
|
编译、打包、安装,重新运行客户端,高大上的VIP有木有?看视频没有广告了有木有?VIP视频随便看有木有?

总结
至此,整个逆向过程就结束了,授人以鱼不如授人以渔,所以我详细的描述了分析的过程而不是结果。此外,本文内容仅供个人学习交流,所以即使你知道我所逆向的是哪个客户端,也不要将本文内容用于有损该客户端利益的目的。