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文件导出的IJKFFMoviePlayerController的OC头文件可以知道,它有一个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 maxPlaybackRate的getter方法,看能不能修改最大播放速度?
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文件导出的IJKFFMoviePlayerControllerFFPlay的OC头文件可以知道,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_117084280的16进制值

      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
    • ARM64、Mach-O 中 double 存储方式都是 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

相关推荐
老李IT笔记16 小时前
设备指纹由哪几个字段拼出来:换一块主板会变成新设备
ios·智能手机
andyweike17 小时前
iOS App 测试
ios·测试·ad hoc·分发
用户2181697049301 天前
iOS 逆向 汇编代码和frida的使用
逆向
ControlM1 天前
从官方 CDN 里扒出 TRAE (TraeCode) 历史版本安装包
python·逆向·trae
茶底世界之下1 天前
花瓣要更柔和,还是更有反差?用 Awaking 做一次对比度取舍
ios
Anhty2 天前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频
2501_915909062 天前
iOS应用从开发到上架App Store的完整发布流程与步骤指南
android·ios·小程序·https·uni-app·webview
初雪云2 天前
UniApp 云打包拿到 IPA 后,真正的发布工作才刚开始
ios·自动化·编辑器·产品经理·iphone
Lancker2 天前
没有 Mac 也能上架 iOS:GitHub Actions 全自动构建 + 签名 + TestFlight 送审实录
macos·ios·github
茶底世界之下2 天前
晚霞要多暖?用 Awaking 只改色温做一次取舍
ios