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

前言

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

修改前效果:

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

修改后效果:

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

场景

**横屏视频-半屏播放**的播放页面

开发环境

  • 哔哩哔哩版本:8.41.0

  • MonkeyDev

  • IDA Professional 9.0

  • 安装IDA插件:patching

  • Lookin

目标

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

分析

  • Lookin可以知道,播放速度组件叫做VKSettingView.SelectContent
  • Mach-O文件导出的VKSettingView.SelectContentswift文件可以知道,它的model叫做VKSettingView.SelectModel
swift 复制代码
class VKSettingView.SelectContent: VKSettingView.TitleBaseContent {
  /* fields */
    var model: VKSettingView.SelectModel ?
    var lazy selecter: VKSettingView.VKSelectControl ?
}
  • VKSettingView.SelectModel有个items属性,有可能是播放速度数组。我们从IDA依次查看方法的实现,找到itemssetter方法叫做sub_10D8ACB88
swift 复制代码
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)
}
  • 我们在Xcode添加符号断点sub_10D8ACB88,看到底谁设置了items的值
  • sub_10D8ACB88断点触发,我们打印参数的值,证明items确实是播放速度数组
objc 复制代码
(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的值
objc 复制代码
(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的伪代码实现
objc 复制代码
_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
objc 复制代码
(lldb) p (id)$x0
(BAPIPlayersharedSettingItem *) 0x0000000282c0f880
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
  • 我们打印x1(VKSettingView.SelectModel)(0x0000000283b51790)的items的值,发现是个空数组
objc 复制代码
(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的值
objc 复制代码
(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通常拿来存放函数的返回值。
objc 复制代码
(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的值,再看汇编代码对应的伪代码是怎样的。
objc 复制代码
检查 inited = swift_initStaticObject(...) 对象
inited 很可能是 SelectModel 或其内部配置对象(例如某个静态配置结构体或 Swift 字典/数组)被初始化。你可以在反汇编中查看 swift_initStaticObject 的参数 &unk_116E60370 看看该静态对象是什么,它可能携带 items 的初始数据。若你在数据段或只读段中找到与 "items" 相关的字符串数组、常量字符串列表、NSStringPointer 等,那可能就是 items 的来源。
  • 查看&unk_116E60370的值,发现是在数据段(__data)中
  • 查看&unk_116E60370的值的16进制视图,发现它旁边的地址存放着播放速度,所以&unk_116E60370保存着播放速度数组
  • 我们知道数据段(__data)存放着全局变量,所以播放速度数组应该是放在一个全局变量里面,类似:
objc 复制代码
var playbackRates = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"]

说明

比如0000000116E789B0,保存的值是0.75

objc 复制代码
0000000116E789B0  30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4  0.75............

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

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

越狱解决方案

  • 修改下面地址存储的值
objc 复制代码
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.............
  • 具体代码
objc 复制代码
/// 将速度写入到内存地址
/// - 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文件的汇编指令

目标

  • 修改下面地址存储的值
objc 复制代码
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

objc 复制代码
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
  • 显示Patch Bytes弹框
  • 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,真正修改

修改结果

  • 当前的值:
objc 复制代码
0.5    → 30 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
0.75   → 30 2E 37 35 00 00 00 00  00 00 00 00 00 00 00 E4
1.0    → 31 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
1.25   → 31 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4
1.5    → 31 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
2.0    → 32 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
  • 新的播放速度对应的值:
objc 复制代码
0.5    → 30 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
1.0    → 31 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
1.25   → 31 2E 32 35 00 00 00 00  00 00 00 00 00 00 00 E4
1.5    → 31 2E 35 00 00 00 00 00  00 00 00 00 00 00 00 E3
2.0    → 32 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
3.0    → 33 2E 30 00 00 00 00 00  00 00 00 00 00 00 00 E3
  • 全部修改完后
objc 复制代码
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
  • 保存后,底部会显示log

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

效果

代码

BiliBiliMApp-无广告版哔哩哔哩

相关推荐
sakiko_17 小时前
Swift学习笔记42-SwiftUI的属性包装器(讲解+面试)
笔记·学习·ios·swiftui·swift
Patrick_Wilson1 天前
iOS Safari 按钮点击无响应问题修复指南
前端·ios·safari
随遇丿而安1 天前
第 3 周:UIButton 点了没反应,因为它默认就没有按压态
ios
思盛iOS签名上架2 天前
App苹果签名有什么作用?
macos·ios·cocoa
CocoaKier2 天前
Facebook广告后台收不到Adjust回传数据,我们排查了一个月终于找到了根因
ios·facebook
weixin_531670893 天前
Interlude起来:动作动画和文字通知哪个更适合休息提醒?Mac上有没有用桌宠做动作引导的软件?
人工智能·macos·mac·swift
FairGuard手游加固3 天前
iOS 游戏加固技术解析:代码混淆、资源加密、内存保护与重签名检测
安全·游戏·macos·unity·ios·objective-c
思盛iOS签名上架3 天前
iOS企业签名原理是什么?
macos·ios·cocoa
Daniel_Coder3 天前
我用 SwiftUI + GRDB 做了一个「按时吃药·服药提醒」App
ios·swiftui·swift·widgetkit