阅读视图

发现新文章,点击刷新页面。

iOS逆向-哔哩哔哩增加3倍速播放(2)-[横屏视频-半屏播放]增加3倍速播放

前言

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

修改前效果: Screenshot 2025-12-11 at 07.26.05.png

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

修改后效果: Screenshot 2025-12-11 at 07.22.57.png

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

场景

[横屏视频-半屏播放]的播放页面

CE1C32DB-8B78-4543-844C-5283FA858E86.png

开发环境

  • 哔哩哔哩版本:8.41.0

  • MonkeyDev

  • IDA Professional 9.0

  • 安装IDA插件:patching

  • Lookin

目标

[横屏视频-半屏播放]增加三倍速播放

分析

  • Lookin可以知道,播放速度组件叫做VKSettingView.SelectContent

1D8085C7-9797-435A-A5E2-3D748FE9B097.png

  • Mach-O文件导出的VKSettingView.SelectContentswift文件可以知道,它的model叫做VKSettingView.SelectModel
class VKSettingView.SelectContent: VKSettingView.TitleBaseContent {
  /* fields */
    var model: VKSettingView.SelectModel ?
    var lazy selecter: VKSettingView.VKSelectControl ?
}
  • VKSettingView.SelectModel有个items属性,有可能是播放速度数组。我们从IDA依次查看方法的实现,找到itemssetter方法叫做sub_10D8ACB88
import Foundation

class VKSettingView.SelectModel: VKSettingView.BaseModel {
  /* fields */
    var icon: String
    var items: [String]
    var reports: [String]
    var selectedIndex: Int
    var dynamicSelectedString: String?
    var enableRepeatSelect: Swift.Bool
    var selectChangeCallback: ((_:_:))?
    var preferScrollPosition: VKSettingView.VKSelectControlScrollPosition
  /* methods */
    func sub_10d8aca08 // getter (instance)
    func sub_10d8acac4 // setter (instance)
    func sub_10d8acb20 // modify (instance)
    func sub_10d8acb70 // getter (instance)
    func sub_10d8acb88 // setter (instance)
    func sub_10d8acb94 // modify (instance)
    func sub_10d8acc48 // getter (instance)
    func sub_10d8acd10 // setter (instance)
    func sub_10d8acd68 // modify (instance)
    func sub_10d8acf6c // getter (instance)
    func sub_10d8acff8 // setter (instance)
    func sub_10d8ad040 // modify (instance)
    func sub_10d8ad138 // getter (instance)
    func sub_10d8ad234 // setter (instance)
    func sub_10d8ad2a0 // modify (instance)
    func sub_10d8ad328 // getter (instance)
    func sub_10d8ad3b4 // setter (instance)
    func sub_10d8ad3fc // modify (instance)
}

5C5C3435-8A3B-47BB-8689-E56D31E2617E.png

  • 我们在Xcode添加符号断点sub_10D8ACB88,看到底谁设置了items的值

E7D376B3-3226-4C6E-AFA4-F9F94058CE32.png

  • sub_10D8ACB88断点触发,我们打印参数的值,证明items确实是播放速度数组
(lldb) p (id)$x0
(_TtGCs23_ContiguousArrayStorageSS_$ *) 0x00000001179c8370
(lldb) expr -l Swift -- unsafeBitCast(0x00000001179c8370, to: Array<String>.self)
([String]) $R4 = 6 values {
  [0] = "0.5"
  [1] = "0.75"
  [2] = "1.0"
  [3] = "1.25"
  [4] = "1.5"
  [5] = "2.0"
}
  • 我们打印方法的调用堆栈,发现是sub_10A993E14修改了items的值
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x000000010de78b88 bili-universal`sub_10D8ACB88
    frame #1: 0x000000010af5fea0 bili-universal`sub_10A993E14 + 140
    frame #2: 0x000000010af5f15c bili-universal`sub_10A992320 + 3644
    frame #3: 0x000000010af5db20 bili-universal`sub_10A9916B4 + 1132
    frame #4: 0x000000010af6714c bili-universal`sub_10A99B130 + 28
    frame #5: 0x000000010af6859c bili-universal`sub_10A99C1A0 + 1020
    frame #6: 0x000000010af67128 bili-universal`sub_10A99B118 + 16
...
  • 我们从IDA看下sub_10A993E14的伪代码实现
_QWORD *__fastcall sub_10A993E14(void *a1, id a2)
{
...

  v3 = a2;
  if ( a2 && (v4 = v2, v6 = type metadata accessor for SelectModel(0LL), (v7 = swift_dynamicCastClass(v3, v6)) != 0) )
  {
    v9 = (_QWORD *)v7;
    v10 = sub_107C8B79C(&unk_116BB42E8, v8);
    inited = swift_initStaticObject(v10, &unk_116E60370);
    v12 = *(void (__fastcall **)(__int64))((swift_isaMask & *v9) + 0x1C0LL);
    v13 = objc_retain(v3);
...
  • 我们直接搜索sub_10A993E14的伪代码,看是否有直接调用sub_10D8ACB88,很遗憾并没有
  • 我们添加sub_10A993E14符号断点,断点触发后打印方法的参数,发现x1的值是_TtC13VKSettingView11SelectModel,也就是VKSettingView.SelectModel
(lldb) p (id)$x0
(BAPIPlayersharedSettingItem *) 0x0000000282c0f880
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
  • 我们打印x1(VKSettingView.SelectModel)(0x0000000283b51790)的items的值,发现是个空数组
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
(lldb) p [(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790 items]
(_TtCs19__EmptyArrayStorage *) 0x00000001dd92e978
(lldb) expr -l Swift -- unsafeBitCast(0x00000001dd92e978, to: Array<String>.self)
([String]) $R2 = 0 values {}
  • 我们在sub_10A993E14方法返回之前添加一个断点,看下x1(VKSettingView.SelectModel)(0x0000000283b51790)的items的值

16093E5E-4BD2-4281-9945-297047044F27.png

(lldb) register read 
General Purpose Registers:
        x0 = 0x0000000283b51790
        x1 = 0x00000002819eb700
        x2 = 0x0000000000000003
...
       x23 = 0x0000000283b51790
       x24 = 0x0000000283b51790
       x25 = 0x0000000116e17f28  (void *)0x00000001173e6b88: OBJC_METACLASS_$__TtC16BBUGCVideoDetail13VDUGCMoreBloc
       x26 = 0x00000001142906d8  bili-universal`type_metadata_for_ToolCell + 784
       x27 = 0x000000010a552534  bili-universal`sub_109F86534
       x28 = 0x0000000116718000  "badge_control"
        fp = 0x000000016f832610
        lr = 0x000000010af5f15c  bili-universal`sub_10A992320 + 3644
        sp = 0x000000016f832510
        pc = 0x000000010af5ffe4  bili-universal`sub_10A993E14 + 464
      cpsr = 0x60000000
  • 因为x0的值是0x0000000283b51790,所以打印x0的值,看到x0(VKSettingView.SelectModel)(0x0000000283b51790)的items有值了,就是播放速度数组,这也证明sub_10A993E14修改了VKSettingView.SelectModelitems的值。x0通常拿来存放函数的返回值。
(lldb) p (id)$x0
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
(lldb) p [(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790 items]
(_TtCs22__SwiftDeferredNSArray *) 0x0000000280743120 6 values
(lldb) po 0x0000000280743120
<Swift.__SwiftDeferredNSArray 0x280743120>(
0.5,
0.75,
1.0,
1.25,
1.5,
2.0
)
  • 我们将sub_10A993E14的伪代码,参数a1的类型是BAPIPlayersharedSettingItema2的类型是VKSettingView.SelectModel一起给chatgpt分析,chatgpt叫我们查看 swift_initStaticObject 的参数 &unk_116E60370的值是什么。
    • 如果chatgpt的分析结果没用,我们就自己打断点,看是哪些汇编代码更改了items的值,再看汇编代码对应的伪代码是怎样的。
检查 inited = swift_initStaticObject(...) 对象
inited 很可能是 SelectModel 或其内部配置对象(例如某个静态配置结构体或 Swift 字典/数组)被初始化。你可以在反汇编中查看 swift_initStaticObject 的参数 &unk_116E60370 看看该静态对象是什么,它可能携带 items 的初始数据。若你在数据段或只读段中找到与 “items” 相关的字符串数组、常量字符串列表、NSStringPointer 等,那可能就是 items 的来源。
  • 查看&unk_116E60370的值,发现是在数据段(__data)中

AD08C629-FC16-405A-B8EC-D4F866C62775.png

  • 查看&unk_116E60370的值的16进制视图,发现它旁边的地址存放着播放速度,所以&unk_116E60370保存着播放速度数组

0DF1A4B8-7DCF-4F83-A4DB-6C484DF53598.png

  • 我们知道数据段(__data)存放着全局变量,所以播放速度数组应该是放在一个全局变量里面,类似:
var playbackRates = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"]

说明

比如0000000116E789B0,保存的值是0.75

0000000116E789B0  30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4  0.75............

各个字节的解析如下,特别是最后一个字节E4,代表要读取4个字节的数据,如果是E3代表要读取3个字节的数据

30 : 0
2E : .
37 : 7
35 : 5
E4 : 读取四个字节的数据

越狱解决方案

  • 修改下面地址存储的值
0000000116E60390  30 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  0.5.............
0000000116E603A0  30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4  0.75............
0000000116E603B0  31 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.0.............
0000000116E603C0  31 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4  1.25............
0000000116E603D0  31 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.5.............
0000000116E603E0  32 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  2.0.............
  • 具体代码
/// 将速度写入到内存地址
/// - Parameters:
///   - dest_addr: 目标内存地址
///   - str: 速度字符串,比如"1.0"
static int write_rate_string_to_address(uintptr_t dest_addr, NSString *str) {
    if (str == nil) {
        return -1;
    }

    // UTF8 字符串
    const char *utf8Str = [str UTF8String];
    size_t strLength = strlen(utf8Str);   // 字符数(不含 \0)

    if (strLength > (NJ_RATE_BLOCK_SIZE - 1)) {
        // 只能容纳前15字节 + 最后一字节用于 E0+strLength
        strLength = NJ_RATE_BLOCK_SIZE - 1;
    }

    uint8_t block[NJ_RATE_BLOCK_SIZE];
    memset(block, 0, NJ_RATE_BLOCK_SIZE);

    // 前 strLength 字节写入字符串
    memcpy(block, utf8Str, strLength);

    // 最后一个字节写入:E0 + 长度
    block[NJ_RATE_BLOCK_SIZE - 1] = 0xE0 + (uint8_t)strLength;

    // 将 block 写到目标地址
    memcpy((void *)dest_addr, block, NJ_RATE_BLOCK_SIZE);

    return 0;
}


/// 将速度写入到内存地址
/// - Parameter baseAddress: 起始内存地址
static void write_rate_to_address(uintptr_t baseAddress) {
    NSArray<NSString *> *playbackRates = @[@"0.5", @"1.0", @"1.25", @"1.5", @"2.0", @"3.0"];
    NSInteger count = playbackRates.count;
    for (NSInteger i = 0; i < count; i++) {
        uintptr_t currentAddress = baseAddress + i * NJ_RATE_BLOCK_SIZE;
        write_rate_string_to_address(currentAddress, playbackRates[i]);
    }
}

// [横屏视频-半屏播放]的播放速度
static void changePlaybackRates_LandscapeVideo_HalfScreenPlayback() {
    /*
     0000000116E60390  30 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  0.5.............
     0000000116E603A0  30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4  0.75............
     0000000116E603B0  31 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.0.............
     0000000116E603C0  31 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4  1.25............
     0000000116E603D0  31 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.5.............
     0000000116E603E0  32 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  2.0.............
     */
    uintptr_t baseAddress = g_slide + 0x116E60390;
    write_rate_to_address(baseAddress);
}

非越狱解决方案

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

目标

  • 修改下面地址存储的值
0000000116E60390  30 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  0.5.............
0000000116E603A0  30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4  0.75............
0000000116E603B0  31 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.0.............
0000000116E603C0  31 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4  1.25............
0000000116E603D0  31 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.5.............
0000000116E603E0  32 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  2.0.............

示例

比如修改0000000116E603A0

0000000116E603A0  30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4  0.75............
  • 鼠标点击0000000116E603A0
  • IDA->Edit->Patch program->Change byte

9FFF3DBB-69F3-49EB-9A32-8CA94207D159.png

  • 显示Patch Bytes弹框

D3949C2A-9560-4D76-A710-ADAAE4E75A81.png

  • Origin value
    • 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4
  • 修改 Values 为:
    • 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
  • 点击OK,真正修改

修改结果

  • 当前的值:
0.530 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
0.7530 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4
1.031 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
1.2531 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4
1.531 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
2.032 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
  • 新的播放速度对应的值:
0.530 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
1.031 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
1.2531 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4
1.531 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
2.032 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
3.033 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
  • 全部修改完后
0000000116E60390  30 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  0.5.............
0000000116E603A0  31 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.0.............
0000000116E603B0  31 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4  1.25............
0000000116E603C0  31 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3  1.5.............
0000000116E603D0  32 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  2.0.............
0000000116E603E0  33 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3  3.0.............

保存

保存到Mach-O文件

  • IDA->Edit->Patch program->Apply patches to input file->OK

9D1A3899-9871-4DF3-BC43-6D3109085192.png

29737F6A-DF9E-48AB-A46A-77363CA8546B.png

  • 保存后,底部会显示log

    Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal
    

    F1E26F23-6F74-4DEC-B1CD-1256372F5FBE.png

效果

73D09925-1B81-42A9-8392-8EFB88B9884D.png

代码

BiliBiliMApp-无广告版哔哩哔哩

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

前言

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

修改前效果: Screenshot 2025-12-11 at 07.26.05.png

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

修改后效果: Screenshot 2025-12-11 at 07.22.57.png

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

场景

哔哩哔哩的视频播放页面

8C0CA5C5-C2D9-4E60-9BB0-0FF3B83A03DA-5346442.png

开发环境

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

目标

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

分析

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

@property (nonatomic) float playbackRate;

@end

  • Mach-O文件导出的IJKFFMoviePlayerControllerOC头文件可以知道,它有一个maxPlaybackRate属性,应该是最大播放速度
// 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:最大播放速度
%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
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
%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就是最大播放速度
cxzcxz:IJKFFMoviePlayerController-0x281304380-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
  • 我们尝试hook maxPlaybackRategetter方法,看能不能修改最大播放速度?
%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,所以修改无效
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:]方法
void __cdecl -[IJKFFMoviePlayerController setPlaybackRate:](IJKFFMoviePlayerController *self, SEL a2, float a3)
{
  -[IJKMediaPlayback setPlaybackRate:](self->_player, "setPlaybackRate:");
}
  • 我们给IJKFFMoviePlayerController- (void)setPlaybackRate:(float)playbackRate,添加断点,看看self->_player的值是什么?

C72ACE3B-EA9B-4684-A7AB-71C162A27F1E.png

  • 断点触发,发现self->_player的类型是IJKFFMoviePlayerControllerFFPlay
(lldb) po self->_player

  • Mach-O文件导出的IJKFFMoviePlayerControllerFFPlayOC头文件可以知道,IJKFFMoviePlayerControllerFFPlay是一个视频播放器,看来IJKFFMoviePlayerController是基于IJKFFMoviePlayerControllerFFPlay实现的
@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是一致的
// 哔哩哔哩版本
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);
}
// 开源版本
- (void)setPlaybackRate:(float)playbackRate
{
    if (!_mediaPlayer)
        return;

    return ijkmp_set_playback_rate(_mediaPlayer, playbackRate);
}
  • 我们从IDA中查看sub_10F0BAFD4的伪代码实现,发现跟开源版本的ijkmp_set_playback_rate的实现是一致的
// 哔哩哔哩版本
__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");
}
// 开源版本
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获取最大播放速度的
__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, "%s: origin %f, new %f \n", "adjust_playback_rate", a2, v7);
      a2 = v7;
    }
    sub_10F3F5768((__int64 *)v3, 32LL, "Playback rate: %f\n", 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
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的值
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进制值

      0000000117084280  00 00 00 00 00 00 08 40  00 00 00 00 00 00 00 00
      
    • 关键是前 8 字节:

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

      40 08 00 00 00 00 00 00
      
    • IEEE-754 双精度格式:

      0x40080000000000003.0double
    • 所以qword_117084280 = 3.0

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

C291B52E-537E-494A-A8C4-8332634DEC40.png

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

越狱解决方案

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

// 视频最大播放速度
public let maxPlaybackRateValue = 4.0

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

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

// 获取最大播放速度方法
@_cdecl("my_get_max_playback_rate")
func my_get_max_playback_rate(a1: Int64) -> Double {
    return maxPlaybackRateValue
}
// 获取最大播放速度方法
long long get_max_playback_rate_address = g_slide+0x10F10449C;
NSLog(@"[%@] cal func get_max_playback_rate address:0x%llx", 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方法,用以打印日志
%hook IJKFFMoviePlayerControllerFFPlay

- (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,从日志中可以看到,最大播放速度(maxPlaybackRate)改成了4.0,播放速度(changedPlaybackRate)改成了4.0,👍
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方法返回的就是最大播放速度
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方法的实现,改为类似下面的伪代码

    return 4.0;
    
    • 对应的汇编指令就是

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

FMOV            D0, #4.0
RET
  • 示例

    修改第一条汇编指令

    • 鼠标点击000000010F10449C

      __text:000000010F10449C                 SUB             SP, SP, #0x50
      
    • 右键选择Assemble F5144010-91BD-48E8-8E73-75223FF2C980.png

    • 改成

      FMOV    D0, #4.0
      

    3428FB04-36CC-43E8-B362-89CF02D7FFC6.png

    • 点击enter
  • 全部修改结果

F808A0ED-9C78-4589-80A0-76BD2FCF1F8A.png

  • 保存到Mach-O文件中

    • IDA->Edit->Patch program->Apply patches to input file->Apply patches

    542DF73E-FA15-41B7-A251-31E30F37EB38.png

    0D71933B-BB78-48A6-AEA2-A12AC7261586.png

  • 保存后,底部会显示log

Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal

F1E26F23-6F74-4DEC-B1CD-1256372F5FBE.png

代码

BiliBiliMApp-无广告版哔哩哔哩

相关链接

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

❌