iOS逆向之给腾讯视频App添加快进手势

起因

起因是女朋友喜欢追综艺,现在的综艺节目中间有时候会插播很长的广告,正常速度看完的话太浪费时间了,直接拖进度条的话又容易拖过内容,只能点击右上角更多,切换到2x倍速播放,等播放完后,再点击更多,切换到1x倍速,操作比较繁琐,体验太差。而芒果tv有个很好的功能就是,长按直接切换到2x倍速,放开恢复1x倍速,大大提升了看剧体验,所以本文的主题就是,将这功能通过逆向注入,添加到腾讯视频App里。

其实逆向开发在iOS领域内已经有很完善的工具链了,本文主要是提供下分析思路。

脱壳

逆向App的前提是需要已脱壳的ipa包,我们从app store下的ipa包都是经过苹果加密的,可以从一些第三方市场比如PP助手之类的下载越狱版ipa,这种一般都是脱壳的,还有就是可以通过越狱手机运行App然后把内存dump出来重新打成ipa包。

这里我们用第二种方式,dump的方式也有好几种,比如越狱应用CrackerXI,可以直接把App dump到手机目录里,不过需要手动传输ipa包到电脑上。

早期的dump工具基本是用 dumpdecrypted [1] ,不过作者已经不维护了,这里推荐用Monkey大神的 frida-ios-dump [2] ,在越狱手机上装对应的frida插件,然后运行dump.py就可以一步导出脱壳的ipa包到电脑上:

安装流程上面地址已经注明,这里简单给大家列下:

  1. 在越狱手机上用Cydia商店搜索安装frida

  2. clone下上面的项目,使用命令安装相关python依赖库

    sudo pip install -r requirements.txt –upgrade

  3. 安装usbmuxd,用来把SSH转发到USB设备的手机上,安装后在终端输入iproxy 2222 22

  4. 终端运行python ./dump.py 对应App的bundleID 开始dump

    ShellCopy

腾讯视频的BundleID对应com.tencent.live4iphone,现在我们开始dump:

python /dump.py com.tencent.live4iphone

完成后就会在当前目录下生成一个已脱壳的ipa包了。

分析

下面分步骤分析怎么定位到快进的相关函数:

1.Debug View

新建MonkeyApp工程

MonkeyApp是一个可以直接运行脱壳ipa的项目,非常方便用于逆向调试,详情可以查看 Github [3]

除了用MonkeyApp外,还可以通过注入Reveal.dylib到App内然后用Reveal来查看,这里就暂时不扩展了。

项目名随便起个,我这里用TVHook,然后把我们上面脱壳的ipa包放入TargeApp目录中。

修改下BundelID和开发者账号后我们开始运行,第一次运行一般会失败,这时候我们可以把Targe.plist里的信息拷贝到Info.plist里,重新改下BundelID后我们继续运行

修改完毕后可以看到运行成功了,这里需要注意的是,上面这ipa包是通过哪个架构脱壳的,就只能在哪个架构的机子上运行,比如我的是在arm64上脱壳,就不能再模拟器上(x86_64)和armv7上运行。

我们直接去到视频播放页面,然后开启Xcode的Debug View功能:

接下来我们就可以直接看到视图结构了:

这里腾讯视频的开发小哥已经帮我们注释好这些View的作用了,省了不少力气~

2.定位手势View

QNBPlayerGestureView就是我们要的手势层,我们需要在这一层上面添加Long手势,但在这之前,我们可以先查看它有哪些方法,说不定里面已经有了Long手势了:

一开始我是用class-dump来获取QNBPlayerGestureView的.h文件来查看,但是class-dump后居然没发现这个文件,估计是打包在某个Framework里了,但是一个个Framework查又比较麻烦,于是我直接使用了LLDB命令:

image lookup -rn \[QNBPlayerGestureView\

这条命令就是查找QNBPlayerGestureView的所有方法,非常方便

这里我们可以看到QNBPlayerGestureView确实是打包在QNBAutomatic.framework里,而且我们还看到了-[QNBPlayerGestureView didLongPress:],这样的话我们就不需要自己添加long手势了,后面直接Method swizzle该方法就行。

3.定位2x速入口

手势的入口我们已经定位到了,接下来就是定位2x快进的方法了,还是和开始一样,在有2x播放的界面我们使用Debug View,来定位到相关View:

可以看到相关2x按钮所在是视图是QNBPlayerRateSelectPanel,继续用image lookup查看相关方法:

image lookup -rn \[QNBPlayerRateSelectPanel\

这里我们看到了关键入口[QNBPlayerRateSelectPanel btnPress:]。

结合[QNBPlayerRateSelectPanel btnArray]和[QNBPlayerRateSelectPanel rateArray]方法我们可以猜测大概逻辑就是,点击触发btnPress然后通过btnArray和rateArray的对应关系获取到相应的rate,也就是我们只要知道rateArray的内容就可以知道对应哪个Button了。

下面我们将打印[QNBPlayerRateSelectPanel rateArray]的值,先打印出QNBPlayerRateSelectPanel的地址:

这里获取到的是0x14541b400,接下来打印rateArray

可以看到2倍数就再最后一个,想就是相应的Button就是[btnArray lastObject],也就是我们只要调用btnPress:[btnArray lastObject],就能触发2x倍数的逻辑了。

到此基本也就分析完毕,我们只需要在-[QNBPlayerGestureView didLongPress:]里触发QNBPlayerRateSelectPanel的btnPress:[btnArray lastObject]就能实现长按切换2x速播放了。下面我们开始实现功能。

开发功能

MonkeyApp内部已经实现了注入功能,省去了我们自己注入的步骤,所以我们可以直接在它注入的dylib里编写我们的代码:

上面的NSObject+CPSwizzle是我自己简单封装的Method Swizzle方法,因为后续代码编写都需要用,大概代码如下:

+ (void)mothodSwizzleClass:(NSString *)className
old:(NSString *)oldMothod
new:(NSString *)newMothod
isClass:(BOOL)isClass {

if (isClass) {
Method oriSessionMethod = class_getClassMethod(NSClassFromString(className), NSSelectorFromString(oldMothod));
Method mySessionMethod = class_getClassMethod(NSClassFromString(className), NSSelectorFromString(newMothod));
method_exchangeImplementations(oriSessionMethod, mySessionMethod);
} else {
Method oldMothods = class_getInstanceMethod(NSClassFromString(className), NSSelectorFromString(oldMothod));
Method newMothods = class_getInstanceMethod(NSClassFromString(className), NSSelectorFromString(newMothod));
BOOL didAddMethod =
class_addMethod(NSClassFromString(className),
NSSelectorFromString(oldMothod),
method_getImplementation(newMothods),
method_getTypeEncoding(newMothods));
if (didAddMethod) {
class_replaceMethod(NSClassFromString(className),
NSSelectorFromString(newMothod),
method_getImplementation(oldMothods),
method_getTypeEncoding(oldMothods));
} else {
method_exchangeImplementations(oldMothods, newMothods);
}
}
};

当然我们也可以用MonkeyApp自己的Hook功能,不过还是Method Swizzle用的顺手点,代码入口在CHConstructor里,我们先实现Loog手势的Hook:

[NSObject mothodSwizzleClass:@"QNBPlayerGestureView" old:@"didLongPress:" new:@"cp_didLongPress:" isClass:NO];

然后添加对应的方法:

@implementation NSObject (Hook)

- (void)cp_didLongPress:(UILongPressGestureRecognizer *)avg1 {
[self cp_didLongPress:avg1];
//这里开始编写我们的代码
}

接下来我们需要获取到QNBPlayerRateSelectPanel的对象地址做方法调用,这里有两种方法,

第一种是通过Hook QNBPlayerRateSelectPanel的init方法,然后通过一个全局变量保存init后的对象,然后直接使用这个全局变量就行,但这有个弊端,就是QNBPlayerRateSelectPanel的retainCount会+1,导致不能正常释放,想正常释放还得Hook他父视图的dealloc方法,然后手动释放,计较麻烦。

第二种就是通过我们之前获取到的View结构,遍历查找到对应QNBPlayerRateSelectPanelclass

获取到QNBPlayerRateSelectPanel后我们就可以编写具体实现代码了,具体代码如下:

- (void)cp_didLongPress:(UILongPressGestureRecognizer *)avg1 {
[self cp_didLongPress:avg1];

//遍历获取到QNBPlayerRateSelectPanel
UIView *superView = [(UIView *)self superview];
id panle = [superView findQNBPlayerRateSelectPanel];

//获取到[QNBPlayerRateSelectPanel btnArray]
NSArray *btnArray = [panle valueForKey:@"btnArray"];
for (UIButton *btn in btnArray) {
btn.selected = NO;
}
if (avg1.state == UIGestureRecognizerStateBegan) {
//手势开始,取到2x的Button
UIButton *button = btnArray.lastObject;
button.selected = YES;
[panle performSelector:@selector(btnPress:) withObject:button];
} else if (avg1.state == UIGestureRecognizerStateEnded) {
//手势结束,取1x的Button
UIButton *button = btnArray[2];//通过之前的分析可以知道1x是数组的第2个下标
button.selected = YES;
[panle performSelector:@selector(btnPress:) withObject:button];
}
}

大概逻辑已经做了注释,到此已经实现了想要的功能,直接运行看效果!由于效果动图有点大,无法上传,所以可以点击“阅读原文”,查看实际效果!

结语

虽然到此已经实现了想要的功能,但是每次都会有个Toast提示,很影响观看,于是想通过函数调用逻辑查找到最终的实现函数,最终发现函数调用链如下:

直接调用[QNBPlayerInfo setRate:@2]就能实现效果,具体的查找过程等后续有时间在给大家补上。

最后感谢AloneMonkey大神开发了相关的逆向工具,简化了逆向的复杂度,也推荐大家去了解他写的逆向书籍~

参考

[1]https://github.com/stefanesser/dumpdecrypted
[2]https://github.com/AloneMonkey/frida-ios-dump
[3]https://github.com/AloneMonkey/MonkeyDev

看到这里费了很多脑细胞吧吃点水果补充下能量,推荐下我种的油桃嘎嘣脆且甜!如需要微信联系我如下: