iOS逆向-哔哩哔哩增加3倍速(1)-最大播放速度

前言

作为一名 哔哩哔哩的重度用户 ,我一直期待官方推出 3 倍速播放 功能。然而等了许久,这个功能始终没有上线 😮‍💨。

修改前效果:

刚好我自己熟悉 iOS 逆向工程,于是决定 亲自动手,为 B 站加入 3 倍速播放 😆。

修改后效果:

由于整个过程涉及 多处逻辑修改与多个模块的反汇编分析 ,为了让内容更加清晰易读,我将会分成多篇文章,逐步拆解 如何为 B 站增加 3 倍速播放能力

场景

哔哩哔哩的视频播放页面

开发环境

  • 哔哩哔哩版本:8.41.0
  • MonkeyDev
  • IDA Professional 9.0
  • 安装IDA插件:patching
  • Lookin

目标

视频最大播放速度改为4倍速播放

分析

  • 我们知道哔哩哔哩开源了他们的视频播放器ijkplayer,我们可以从中了解到设置播放速度的方法,是IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate
objc 复制代码
@interface IJKFFMoviePlayerController : NSObject 

@property (nonatomic) float playbackRate;

@end
  • Mach-O文件导出的IJKFFMoviePlayerControllerOC头文件可以知道,它有一个maxPlaybackRate属性,应该是最大播放速度
objc 复制代码
// IJKFFMoviePlayerController.h
@interface IJKFFMoviePlayerController : NSObject  {
    /* instance variables */
    id  _player;
}

...
@property (readonly, nonatomic) double realCurrentPlaybackTime;
@property (readonly, nonatomic) float realPlaybackRate;
@property (readonly, nonatomic) float maxPlaybackRate;
@property (readonly, nonatomic) IJKMediaPlayerItem *currentItem;
...
  • 我们hook IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate,添加断点并打印一些日志,其中就有打印maxPlaybackRate
    • inputPlaybackRate:要设置的播放速度
    • changedPlaybackRate:更改后的播放速度
    • realPlaybackRate:真实播放速度
    • maxPlaybackRate:最大播放速度
objc 复制代码
%hook IJKFFMoviePlayerController

- (void)setPlaybackRate:(float)playbackRate {
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

%end
  • 每次设置播放速度,IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate的断点都会触发,我们从日志中可以看到,最大播放速度为3.0
objc 复制代码
cxzcxz:IJKFFMoviePlayerController-0x280519220-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:1.500000-changedPlaybackRate:1.500000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
  • 因为倍速面板无法选择超过2.0的速度,所以我们就设置,如果倍速面板选择的是2.0,我们就改成4.0,看是否会限制播放速度到3.0
objc 复制代码
%hook IJKFFMoviePlayerController

- (void)setPlaybackRate:(float)playbackRate {
    playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

%end
  • 倍速面板选择2.0,从日志中可以看到,播放速度(changedPlaybackRate)改成了3.0而不是4.0,这也证明maxPlaybackRate就是最大播放速度
objc 复制代码
cxzcxz:IJKFFMoviePlayerController-0x281304380-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
  • 我们尝试hook maxPlaybackRategetter方法,看能不能修改最大播放速度?
objc 复制代码
%hook IJKFFMoviePlayerController

- (void)setPlaybackRate:(float)playbackRate {
    playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
    %orig(playbackRate);
    NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

- (float)maxPlaybackRate {
    return 4.0;
}

%end
  • 从日志可以看到,虽然maxPlaybackRate的输出值改成了4.0,但是播放速度(changedPlaybackRate)还是3.0,所以修改无效
objc 复制代码
cxzcxz:IJKFFMoviePlayerController-0x2808e2a40-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:4.000000
  • 我们从IDA中查看IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate伪代码实现,发现调用的是[self->_player setPlaybackRate:]方法
objc 复制代码
void __cdecl -[IJKFFMoviePlayerController setPlaybackRate:](IJKFFMoviePlayerController *self, SEL a2, float a3)
{
  -[IJKMediaPlayback setPlaybackRate:](self->_player, "setPlaybackRate:");
}
  • 我们给IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate,添加断点,看看self->_player的值是什么?
  • 断点触发,发现self->_player的类型是IJKFFMoviePlayerControllerFFPlay
objc 复制代码
(lldb) po self->_player
  • Mach-O文件导出的IJKFFMoviePlayerControllerFFPlayOC头文件可以知道,IJKFFMoviePlayerControllerFFPlay是一个视频播放器,看来IJKFFMoviePlayerController是基于IJKFFMoviePlayerControllerFFPlay实现的
objc 复制代码
@interface IJKFFMoviePlayerControllerFFPlay : NSObject  {
    /* instance variables */
    struct IjkMediaPlayer * _mediaPlayer;
...
@property (weak, nonatomic) IJKMediaPlayerItem *item;
@property (retain, nonatomic) id  fileOpenDelegate;
@property (retain, nonatomic) id  segmentOpenDelegate;
@property (readonly, nonatomic) double fpsInMeta;
@property (readonly, nonatomic) double fpsAtOutput;
@property (nonatomic) _Bool shouldShowHudView;
@property (readonly, nonatomic) long long numberOfBytesTransferred;
@property (nonatomic) _Bool allowsMediaAirPlay;
@property (nonatomic) _Bool isDanmakuMediaAirPlay;
@property (readonly, nonatomic) _Bool airPlayMediaActive;
@property (readonly, nonatomic) int isSeekBuffering;
@property (readonly, nonatomic) int isAudioSync;
@property (readonly, nonatomic) int isVideoSync;
@property (readonly, nonatomic) int currentVideoIdentifier;
@property (readonly, nonatomic) int currentAudioIdentifier;
@property (readonly, nonatomic) double realCurrentPlaybackTime;
@property (readonly, nonatomic) float realPlaybackRate;
@property (readonly, nonatomic) float maxPlaybackRate;
@property (readonly, nonatomic) IJKMediaPlayerItem *currentItem;
...
  • 我们从IDA中查看IJKFFMoviePlayerControllerFFPlay- (void)setPlaybackRate:(float)playbackRate伪代码实现,发现跟开源版本的IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate是一致的
objc 复制代码
// 哔哩哔哩版本
void __cdecl -[IJKFFMoviePlayerControllerFFPlay setPlaybackRate:](
        IJKFFMoviePlayerControllerFFPlay *self,
        SEL a2,
        float a3)
{
  sub_10F3F5768(
    0LL,
    32LL,
    "IJKFFMoviePlayerControllerFFPlay: setPlaybackRate ts = %lld, playbackRate = %f\n",
    +[IJKFFUtils getIjkTickHR](&OBJC_CLASS___IJKFFUtils, "getIjkTickHR"),
    a3);
  if ( self->_mediaPlayer )
    sub_10F0BAFD4(a3);
}
objc 复制代码
// 开源版本
- (void)setPlaybackRate:(float)playbackRate
{
    if (!_mediaPlayer)
        return;

    return ijkmp_set_playback_rate(_mediaPlayer, playbackRate);
}
  • 我们从IDA中查看sub_10F0BAFD4的伪代码实现,发现跟开源版本的ijkmp_set_playback_rate的实现是一致的
objc 复制代码
// 哔哩哔哩版本
__int64 __fastcall sub_10F0BAFD4(__int64 a1, float a2)
{
  printf("%s(%f)\n", "ijkmp_set_playback_rate", a2);
  pthread_mutex_lock((pthread_mutex_t *)(a1 + 8));
  sub_10F0A70B4(*(_QWORD *)(a1 + 136), a2);
  pthread_mutex_unlock((pthread_mutex_t *)(a1 + 8));
  return printf("%s()=void\n", "ijkmp_set_playback_rate");
}
objc 复制代码
// 开源版本
void ijkmp_set_playback_rate(IjkMediaPlayer *mp, float rate)
{
    assert(mp);

    MPTRACE("%s(%f)\n", __func__, rate);
    pthread_mutex_lock(&mp->mutex);
    ffp_set_playback_rate(mp->ffplayer, rate);
    pthread_mutex_unlock(&mp->mutex);
    MPTRACE("%s()=void\n", __func__);
}
  • 我们从IDA中查看sub_10F0A70B4的伪代码实现,可以知道是从sub_10F101034获取最大播放速度的
objc 复制代码
__int64 __fastcall sub_10F0A70B4(__int64 result, float a2)
{
  __int64 v3; // x19
  float v4; // s0
  __int64 v5; // x8
  float v6; // s1
  float v7; // [xsp+1Ch] [xbp-24h] BYREF

  if ( result )
  {
    v3 = result;
    v7 = 0.0;
    sub_10F101034(10LL, &v7, 4LL);
    if ( v7 < a2 )
    {
      sub_10F3F5768(0LL, 32LL, &#34;%s: origin %f, new %f \n&#34;, &#34;adjust_playback_rate&#34;, a2, v7);
      a2 = v7;
    }
    sub_10F3F5768((__int64 *)v3, 32LL, &#34;Playback rate: %f\n&#34;, a2);
    if ( a2 == 0.0 )
      v4 = 1.0;
    else
      v4 = a2;
    if ( v4 != 1.0 )
      *(_DWORD *)(v3 + 884) = 1;
    v5 = *(_QWORD *)(v3 + 8);
    if ( v5 )
    {
      v6 = *(float *)(v3 + 876);
      if ( v6 != v4 )
        *(double *)(v5 + 8040) = vabds_f32(v6, v4);
    }
    if ( v4 > *(float *)(v3 + 6724) )
      *(float *)(v3 + 6724) = v4;
    *(float *)(v3 + 876) = v4;
    *(_DWORD *)(v3 + 880) = 1;
    return sub_10F100F98(8LL, v3 + 9272);
  }
  return result;
}
  • 我们从IDA中查看sub_10F101034的伪代码实现,再根据sub_10F101034(10LL, &v7, 4LL); 知道参数a1=10,a3=4,将伪代码和参数交给chatgpt分析,得出下面几个结论:
    • sub_10F10449C(a9) 计算最大播放速度
    • 该函数内部会将计算结果放入某个寄存器(例如 d0
    • 解码器将其反编译成 v14
    • LABEL_38 → v13 = v14
    • 最终写入:*(float*)a2 = v13
objc 复制代码
void __fastcall sub_10F101034(
        int a1,
        double *a2,
        __int64 a3,
        __int64 a4,
        __int64 a5,
        __int64 a6,
        __int64 a7,
        __int64 a8,
        _QWORD *a9,
        __int64 a10,
        unsigned int a11,
        __int64 a12)
{
  float v13; // s0
  double v14; // d0
  __int64 v15; // x20
  int v16; // w0
  __int64 *v17; // [xsp+18h] [xbp-68h] BYREF
  __int64 v18; // [xsp+20h] [xbp-60h]
  __int64 v19; // [xsp+30h] [xbp-50h]
  unsigned int *v20; // [xsp+58h] [xbp-28h]

  if ( a2 && a3 )
  {
    switch ( a1 )
    {
...
      case 10:
        if ( a3 == 4 )
        {
          v17 = &a10;
          sub_10F10449C((unsigned int)a9);
          goto LABEL_38;
        }
        break;
...
LABEL_38:
            v13 = v14;
          }
LABEL_44:
          *(float *)a2 = v13;
        }
        break;
...
  • 我们从IDA中查看sub_10F10449C的伪代码实现,发现有可能返回两个值,一个是2.0,一个是全局变量qword_117084280的值
objc 复制代码
double __fastcall sub_10F10449C(__int64 a1)
{
  __int64 v2; // x0
  double result; // d0
  double v4[6]; // [xsp+0h] [xbp-40h] BYREF

  v2 = sub_10F104600();
  sub_10F104B7C(v4, v2, a1);
  result = 2.0;
  if ( v4[0] < 50.0 )
    return *(double *)&qword_117084280;
  return result;
}
  • 我们查看全局变量qword_117084280的值,发现是3.0,而3.0就是最大播放速度

    • qword_11708428016进制值

      objc 复制代码
      0000000117084280  00 00 00 00 00 00 08 40  00 00 00 00 00 00 00 00
    • 关键是前 8 字节:

      objc 复制代码
      00 00 00 00 00 00 08 40
    • ARM64Mach-Odouble 存储方式都是 little-endian,所以按小端序解析:

      objc 复制代码
      40 08 00 00 00 00 00 00
    • IEEE-754 双精度格式:

      objc 复制代码
      0x4008000000000000 → 3.0(double)
    • 所以qword_117084280 = 3.0

  • 我们添加符号断点sub_10F10449C,看它的返回值多少

  • sub_10F10449C断点触发,返回值发现是3.0,这样就验证了全局变量qword_117084280的值是3.0
objc 复制代码
(lldb) register read d0
      d0 = 3
  • 总结 :哔哩哔哩的最大播放速度方法是sub_10F10449C,最大播放速度有可能是2倍速,也可能是3倍速,猜测跟视频有关。

越狱解决方案

我们hook sub_10F10449C,将最大值改为4.0

swift 复制代码
// 视频最大播放速度
public let maxPlaybackRateValue = 4.0

// 声明原函数类型
public typealias orig_get_max_playback_rate_type = @convention(c) (_ a1: Int64) -> Double

// 定义全局函数指针变量,并绑定一个 C 名字
@_silgen_name(&#34;orig_get_max_playback_rate&#34;)
nonisolated(unsafe) public var orig_get_max_playback_rate: orig_get_max_playback_rate_type? = nil

// 获取最大播放速度方法
@_cdecl(&#34;my_get_max_playback_rate&#34;)
func my_get_max_playback_rate(a1: Int64) -> Double {
    return maxPlaybackRateValue
}
objc 复制代码
// 获取最大播放速度方法
long long get_max_playback_rate_address = g_slide+0x10F10449C;
NSLog(@&#34;[%@] cal func get_max_playback_rate address:0x%llx&#34;, nj_logPrefix, get_max_playback_rate_address);
MSHookFunction((void *)get_max_playback_rate_address,
			   (void*)my_get_max_playback_rate,
			   (void**)&orig_get_max_playback_rate);
  • 我们再hook IJKFFMoviePlayerControllerFFPlay- (void)setPlaybackRate:(float)playbackRate方法,用以打印日志
objc 复制代码
%hook IJKFFMoviePlayerControllerFFPlay

- (void)setPlaybackRate:(float)playbackRate {
    playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
    %orig(playbackRate);
    NSLog(@&#34;%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf&#34;, nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}

%end
  • 倍速面板选择2.0,从日志中可以看到,最大播放速度(maxPlaybackRate)改成了4.0,播放速度(changedPlaybackRate)改成了4.0,👍
objc 复制代码
cxzcxz:IJKFFMoviePlayerControllerFFPlay-0x136428000-_logos_method$App$IJKFFMoviePlayerControllerFFPlay$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:4.000000-realPlaybackRate0.000000-maxPlaybackRate:4.000000

非越狱解决方案

修改Mach-O文件的汇编指令

  • sub_10F10449C方法的伪代码可知,sub_10F10449C方法返回的就是最大播放速度
objc 复制代码
double __fastcall sub_10F10449C(__int64 a1)
{
  __int64 v2; // x0
  double result; // d0
  double v4[6]; // [xsp+0h] [xbp-40h] BYREF

  v2 = sub_10F104600();
  sub_10F104B7C(v4, v2, a1);
  result = 2.0;
  if ( v4[0] < 50.0 )
    return *(double *)&qword_117084280;
  return result;
}
  • 修改sub_10F10449C方法的实现,改为类似下面的伪代码

    objc 复制代码
    return 4.0;
    • 对应的汇编指令就是

      objc 复制代码
      FMOV            D0, #4.0
      RET
  • 我们修改sub_10F10449C方法的前两条汇编指令,改为下面

objc 复制代码
FMOV            D0, #4.0
RET
  • 示例

    修改第一条汇编指令

    • 鼠标点击000000010F10449C

      objc 复制代码
      __text:000000010F10449C                 SUB             SP, SP, #0x50
    • 右键选择Assemble

    • 改成

      objc 复制代码
      FMOV    D0, #4.0
    • 点击enter
  • 全部修改结果

  • 保存到Mach-O文件中

    • IDA->Edit->Patch program->Apply patches to input file->Apply patches
  • 保存后,底部会显示log

objc 复制代码
Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal

代码

BiliBiliMApp-NJPlaybackRate.xm

相关链接

哔哩哔哩的视频播放器:ijkplayer

相关推荐
RollingPin10 小时前
React Native与Flutter的对比
android·flutter·react native·ios·js·移动端·跨平台开发
2501_9160088910 小时前
iOS 能耗检测的工程化方法,构建多工具协同的电量分析与性能能效体系
android·ios·小程序·https·uni-app·iphone·webview
long_run10 小时前
Objective-C 类与对象详细入门
ios
美狐美颜SDK开放平台11 小时前
跨平台直播美颜SDK开发:iOS/Android/WebGL实现要点
android·人工智能·ios·美颜sdk·第三方美颜sdk·视频美颜sdk·美狐美颜sdk
2501_9159214311 小时前
重新理解 iOS 的 Bundle Id 从创建、管理到协作的工程策略
android·ios·小程序·https·uni-app·iphone·webview
2501_9151063211 小时前
当 altool 退出历史舞台,iOS 上传链路的演变与替代方案的工程实践
android·ios·小程序·https·uni-app·iphone·webview
前端不太难11 小时前
RN 版本升级、第三方库兼容、Android/iOS 崩溃(实战博文 — 从 0.63 升到 0.72)
android·ios·react
00后程序员张11 小时前
Transporter 的局限与替代路径,iOS 上传流程在多平台团队中的演进
android·ios·小程序·https·uni-app·iphone·webview
00后程序员张11 小时前
Python 抓包工具全面解析,从网络监听、协议解析到底层数据流捕获的多层调试方案
开发语言·网络·python·ios·小程序·uni-app·iphone