某视频客户端逆向实践,达到视频随便下,vip随意当的效果

原文链接:http://esoftmobile.com/2014/04/06/video-app-reverse/如需转载,请注明: 本文来自 Esoft Mobile作者:TracyYih - 2014-04-06

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

$ class-dump -H xxxxVideo -o headers

用Xcode创建一个新工程,将头文件导入工程:

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

下面通过cycript开始找到该按钮所在类(如果你安装了Reveal,那下面这个查找步骤就可以直接用Reveal实现了)

$ ssh root@192.168.1.118  #通过ssh连接到手机
~ root# ps -A | grep xxxxVideo  #获取到该视频客户端进程ID
PID   TT  STAT      TIME COMMAND
1438   ??  Ss     0:02.19 /var/mobile/Applications/414D20A3-EA72-4AB4-87E4-5A209F648EAB/xxxxVideo.app/xxxxVideo
~ root# cycript -p 1438
# var tabBarController = [UIApp keyWindow].rootViewController
# "<SVTabBarController: 0x23da6a0>"
# var detailViewController = tabBarController.selectedViewController.visibleViewController
# "<VideoDetailViewController: 0x23eadc0>"

所以当前播放界面对应的视图控制器类是VideoDetailViewController,但是在该头文件中并未找到缓存按钮或者下面的bar,不过下面这个属性引起了我的注意:

@property(retain, nonatomic) VideoDetailBarController *videoDetailBarController; // @synthesize videoDetailBarController=_videoDetailBarController;

进入到该类对应的头文件,终于找到了下载按钮downloadBtn,并且很明显可以看出点击该按钮调用- (void)downloadWithButton:(id)arg1方法。因为缓存按钮有可以缓存和不能缓存两种状态,所以判断是否能缓存应该是在该方法里面实现的,由于没有其他成员变量或方法标记能否缓存,所以这个判断依据应该存在某个对象里面,这时可以注意到dataManager属性,它是一个数据管理对象,而管理的对象就是videoAlbum。

#import "BaseViewController.h"

@class AsynImageView, FollowButton, RequestItem, TTTAttributedLabel, UIButton, UILabel, VideoAlbumDataManager;

@interface VideoDetailBarController : BaseViewController
{
    //...
    FollowButton *_followBtn;
    UIButton *_downloadBtn;
}

@property(retain, nonatomic) UIButton *downloadBtn; // @synthesize downloadBtn=_downloadBtn;
@property(retain, nonatomic) FollowButton *followBtn; // @synthesize followBtn=_followBtn;
//...
@property(retain, nonatomic) VideoAlbumDataManager *dataManager; // @synthesize dataManager=_dataManager;
- (void)downloadWithButton:(id)arg1;
- (void)updateDownloadBarButtonItem;
- (id)videoAlbum;
- (id)initWithVideoDetailDataManager:(id)arg1;
//...
@end

进入VideoAlbum头文件,好家伙,一个模型类快300多行,而且方法远比属性多,不过眼尖的我一下就找到了我想要的东西——canBeDownloaded,这个就应该是判断视频能否被缓存的依据了。

@interface VideoAlbum : NSObject
//...
- (BOOL)canBeShared;
- (BOOL)canBeSubscribed;
- (BOOL)canBeDownLoaded;
- (BOOL)canBePlayed;
//..

马上验证,创建Theos Tweak工程,配置好其他信息:

%hook VideoAlbum

- (BOOL)canBeDownLoaded
{
    return YES;
}

%end

编译、打包、安装:

$ export THEOS_DEVICE_IP=192.168.1.118
$ make package install

再次运行该视频客户端,“缓存”按钮恢复正常了,点击后弹出了缓存界面,选择剧集后可以正常下载。


[hr]到了这里本文应该就要结束了,嘛,还没玩过瘾?那我们继续折腾吧,看到视频上面登录VIP 30吗?非VIP用户每个视频前面有30秒的广告,而且VIP专区的视频只能看前面的5分钟,浪费时间就是浪费生命,但是我等屌丝又买不起VIP好吗?那下面我们就告别广告,成为VIP。因为每次进到“个人资料”时客户端都会进行通讯,应该是获取用户信息,使用Charles抓取到以下信息:

{
    "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实例作为属性,还有一些其他的方法。

@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; // @synthesize loginTpye;
@property int gender; // @synthesize gender;
@property(copy, nonatomic) NSString *uid; // @synthesize uid;
@property(copy) NSString *grade; // @synthesize grade;
@property(copy) NSString *score; // @synthesize score;
@property(copy) NSString *vipExpire; // @synthesize vipExpire;
@property BOOL isVip; // @synthesize isVip;
@property(copy) NSString *requestToken; // @synthesize requestToken;
@property(copy) NSString *birthday; // @synthesize birthday;
@property(copy) NSString *email; // @synthesize email;
@property(copy) NSString *mobile; // @synthesize mobile;
@property(copy) NSString *profileImage; // @synthesize profileImage;
@property(copy) NSString *nickname; // @synthesize nickname;
@property(copy) NSString *password; // @synthesize password;
@property(copy) NSString *passport; // @synthesize passport;
//...
@end

所以我们应该关注最基础的模型(UserDataModel),不管接口返回的用户信息中是否为VIP,我们统一设置他为VIP:

%hook UserDataModel

- (BOOL)isVip
{
    return YES;
}

%end

编译、打包、安装,重新运行该视频客户端,没有任何变化,应该是哪里出问题。再仔细看一遍UserDataModel类,发现vipExpire字段,而接口返回的该字段为空字符串,所以应该是和这个字段有关系,而且我们只知道vipExpire是一个字符串(应该是个时间),不知道它具体格式,总不能一个个试吧,上IDA。还是搜索“isVip”,在UserDataModel的isVip方法中只是简单的存储,没有其他逻辑判断:

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

首先获取属性dataModel的isVip属性,如果为NO,直接返回NO,否则进入下面的判断,对应的代码如下:

- (BOOL)getModelIsVip
{
    if (self.dataModel.isVip) {
        //继续判断
    }
    retrun NO;
}


接着就判断vipExpire字段是否为空,这个和我们之前的设想一致,对应代码如下:

- (BOOL)getModelIsVip
{
    if (self.dataModel.isVip) {
        NSString *vipExpire = self.dataModel.vipExpire;
        if (vipExpire && vipExpire.length) {
            //继续判断
        }
    }
    retrun NO;
}




判断dateFormatter属性是否为空,如果为空,创建该对象。

- (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];
                //[locale release]; 汇编中没见到这句,难道内存泄露了?
            }
            //继续判断
        }
    }
    retrun NO;
}


比较VIP有效期和当前时间:

- (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];
                //[locale release]; 汇编中没见到这句,难道内存泄露了?
            }
            NSDate *vipExpireDate = [self.dateFormatter dateFormString:vipExpire];
            if ([vipExpireDate compare:[NSDate date]]) {
                return YES;
            }
        }
    }
    retrun NO;
}

到这里我们基本还原了判断是否为VIP的方法,逻辑清楚了,下面我们要实现VIP身份就简单了:

#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视频随便看有木有?


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

3 个赞

直接hook - (BOOL)getModelIsVip 这个方法返回YES,不就破解VIP了?

可以试试,不过个人资料详情里面 有效期字段就应该显示有问题了

大哥,我遇到一个错误,想向你请教一下, make package install 报错如下:

No matching processes were found
make: *** [after-install] Error 1

这是我的tweak 工程各个文件如下:

文件 makefile :
export THEOS_DEVICE_IP=192.168.2.40
include theos/makefiles/common.mk

TWEAK_NAME = sohu
sohu_FILES = Tweak.xm

include $(THEOS_MAKE_PATH)/tweak.mk

after-install::
install.exec “killall -9 com.sohu.iPhoneVideo”

文件control:
Package: com.iboxpay.sohu
Name: sohu
Depends: mobilesubstrate
Version: 0.0.1
Architecture: iphoneos-arm
Description: An awesome MobileSubstrate tweak!
Maintainer: chenping
Author: chenping
Section: Tweaks

文件 plist:
{ Filter = { Bundles = ( “com.sohu.iPhoneVideo” ); }; }

Tweak.xm文件 :
%hook VideoAlbum

  • (BOOL)canBeDownLoaded
    {
    return YES;
    }

%end

请大家帮我看一下

错误的提示是“No matching processes were found”,也就是Makefile中的这一句:

after-install::
        install.exec "killall -9 com.sohu.iPhoneVideo"

执行失败,原因是找不到要kill的进程。这里要把com.sohu.iPhoneVideo改成进程名,是书上的一个错误,勘误在了这里

1 个赞

恩,是的

按照楼主的方法,完全实现了上面的功能,只是ida那一块还是不明白,继续学习~感谢楼主~

先mark 一下

请问这种插件可以发布到cydia么?

可以的

几个月前干了一件跟楼主完全一样的事情,思路几乎一致,甚至入口都是一样的:”很自然的会想到以该按钮为出发点开始分析“。不过我是用FLEXLoader找到这个按钮的类以及显示VIP剩余时间的view的,FLEX这货太强大,直接在iPad就差不多完成了■■思路,然后写tweak验证。
(看了一下帖子的日期,我应该是在你之后,嘿嘿)

1 个赞

厉害,帮顶一下。