iOS逆向-哔哩哔哩增加3倍速播放(2)-[横屏视频-半屏播放]增加3倍速播放
前言
作为一名 哔哩哔哩的重度用户,我一直期待官方推出 3 倍速播放 功能。然而等了许久,这个功能始终没有上线 😮💨。
修改前效果:
刚好我自己熟悉 iOS 逆向工程,于是决定 亲自动手,为 B 站加入 3 倍速播放 😆。
修改后效果:
由于整个过程涉及 多处逻辑修改与多个模块的反汇编分析,为了让内容更加清晰易读,我将会分成多篇文章,逐步拆解 如何为 B 站增加 3 倍速播放能力。
场景
[横屏视频-半屏播放]的播放页面
开发环境
-
哔哩哔哩版本:
8.41.0 -
IDA Professional 9.0 -
安装IDA插件:
patching -
Lookin
目标
[横屏视频-半屏播放]增加三倍速播放
分析
- 从
Lookin可以知道,播放速度组件叫做VKSettingView.SelectContent
- 从
Mach-O文件导出的VKSettingView.SelectContent的swift文件可以知道,它的model叫做VKSettingView.SelectModel
class VKSettingView.SelectContent: VKSettingView.TitleBaseContent {
/* fields */
var model: VKSettingView.SelectModel ?
var lazy selecter: VKSettingView.VKSelectControl ?
}
-
VKSettingView.SelectModel有个items属性,有可能是播放速度数组。我们从IDA依次查看方法的实现,找到items的setter方法叫做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)
}
- 我们在
Xcode添加符号断点sub_10D8ACB88,看到底谁设置了items的值
-
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的值
(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.SelectModel的items的值。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的类型是BAPIPlayersharedSettingItem,a2的类型是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)中
- 查看
&unk_116E60370的值的16进制视图,发现它旁边的地址存放着播放速度,所以&unk_116E60370保存着播放速度数组
- 我们知道数据段(
__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
- 显示
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,真正修改
修改结果
- 当前的值:
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
- 新的播放速度对应的值:
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
- 全部修改完后
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:Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal