本文记录一次完整的 iOS tweak 静态分析过程。
使用工具
这次分析使用的主要工具是 IDA Pro 9.x。
一、问题现象
拿到的是一个微信界面美化插件:WXLiquidGlass.dylib。插件能正常注入微信,设置页也能打开,但六个开关里只有最上面的“透明模式”可以使用,下面五个开关点开后会马上恢复关闭:
设置页中无法开启的五个开关:
受影响的五项分别是:
- 气泡开关
- 底栏总开关
- 渐变顶部
- 顶部导航按钮
- 顶部标题
一开始看起来像普通的 NSUserDefaults 写入失败,后面才发现实际原因是插件做了授权门控,而且代码经过了比较重的控制流混淆。
二、先确定插件到底做了什么
从 Objective-C 元数据中可以看到三个主要类:
WXLGNavBarBackgroundView
WXLSettingsViewController
CCLiquidGlassView
几个比较关键的方法:
0x005568 -[WXLGNavBarBackgroundView updateForHostView:topExtension:visibleHeight:]
0x1E15D4 -[WXLSettingsViewController loadSettingsData]
0x1FF3D8 -[WXLSettingsViewController wxl_boolValueForKey:]
0x207710 -[WXLSettingsViewController wxl_setBoolValue:forKey:]
0x2527BC -[WXLSettingsViewController wxl_switchValueChanged:]
0x3E4D7C -[CCLiquidGlassView cc_installBackdropFiltersForStyle:]
0x3F10D8 -[CCLiquidGlassView updateForHostView:preferredStyle:sourceView:]
设置项字符串也很直观:
bubblesEnabled
transparentMode
tabBarEnabled
tabBarYOffset
tabBarWidth
tabBarHeight
navBarGradientEnabled
navBarButtonsEnabled
navBarTitleEnabled
__ustring 里还能直接还原出中文标题,例如“气泡开关”“四色渐变”“底栏总开关”“顶部导航按钮”等。到这里基本可以确定,它主要负责聊天气泡、底部 TabBar 和顶部导航栏的玻璃/渐变效果。
三、第一条线索:ECDSA 授权验证
导入表里出现了:
SecKeyCreateWithData
SecKeyVerifySignature
kSecKeyAlgorithmECDSASignatureMessageX962SHA256
CC_SHA256
CCHmacInit / CCHmacUpdate / CCHmacFinal
同时还发现两个明显与授权有关的标记:
WXLPUBK1
WXLGLAS1-
公钥在 0x2930D8 附近通过 SecKeyCreateWithData 创建,最后保存在 qword_612E70。
签名验证调用位于:
0x28BC68 BL _SecKeyVerifySignature
0x28BCD4 CMP W0, #0
0x28BCE0 CSET W8, EQ
0x28BCEC STRB W8, [X25,#0x89]
SecKeyVerifySignature 返回非零表示验证成功。这里又执行了一次 CSET EQ,所以保存下来的实际是一个“验证失败标志”:
invalidFlag = (SecKeyVerifySignature(...) == 0);
之后在 0x28D5E8 读取这个标志:
0x28D5E8 LDRB W8, [X27,#0x89]
0x28D5EC CMP W8, #0
0x28D5F0 MOV W8, #0x5F69BF3D
0x28D5F8 MOV W9, #0xCDB45803
0x28D600 CSEL W8, W9, W8, NE
两条状态分别是:
0x5F69BF3D:签名通过
0xCDB45803:签名失败
第一个补丁
把 0x28D600 的条件选择改成 NOP,让 W8 始终保留前面装入的成功状态:
原指令:CSEL W8, W9, W8, NE
新指令:NOP
对应机器码:
文件偏移:0x28D600
原字节:28 11 88 1A
新字节:1F 20 03 D5
但是实际测试后,五个开关仍然无法开启。这说明 ECDSA 只是授权链中的一层,不是功能代码最终读取的授权状态。
四、走过的一条弯路:直接读取 NSUserDefaults
开关事件链比较清楚:
0x253D04 读取 UISwitch.isOn
0x253000 / 0x254C0C 调用 wxl_setBoolValue:forKey:
0x253358 / 0x253B00 再调用 wxl_boolValueForKey:
0x253358 / 0x253B00 根据回读结果重新 setOn:
也就是说,开关不是 UIKit 层面被禁用,而是点击后写入配置,再被 getter 回读成 false,于是界面马上恢复关闭。
当时尝试把 wxl_boolValueForKey: 改成:
[[NSUserDefaults standardUserDefaults] boolForKey:key];
结果六个开关全部失效。原因是传给设置控制器的这些字符串只是逻辑键,插件内部还会把它们映射到混淆后的存储键。绕过整个 getter 后,反而读不到真正的配置。
这一步很重要:不要看到 NSUserDefaults 就直接重写 getter。先确认插件有没有做键名映射、默认值转换或二次缓存。
五、定位最终公共授权状态
恢复原始 getter 后,继续追踪五个受限键的读取路径。在多个分支中反复出现同一个无参数函数:
sub_15A6C4
它的特征非常明显:
- 不接收参数,最终返回一个布尔值。
- 被
wxl_boolValueForKey: 多次调用。
- 交叉引用约 40 处,调用者覆盖气泡、TabBar、导航栏和玻璃视图代码。
- “透明模式”走的是另一条路径,不依赖这个返回值。
- 原函数内部会读取授权相关全局状态并构造多个临时对象,最后从栈上取一个字节作为返回值。
它原来的返回尾部位于 0x15C644 附近:
0x15C644 LDURB W19, [X29,#-0x9D]
...
0x15C668 MOV X0, X19
0x15C688 RETAB
结合调用范围和现象,可以确认 sub_15A6C4 才是功能模块共同使用的最终授权查询函数。ECDSA 验证只是给它提供状态的一部分。
六、最终补丁
最干净的做法是在 sub_15A6C4 入口直接返回 true:
0x15A6C4 MOV W0, #1
0x15A6C8 RET
为什么这里使用普通 RET 而不是 RETAB?因为我们把函数入口处原本的 PACIBSP 也覆盖掉了,函数没有执行返回地址签名,自然应该用普通 RET。
机器码如下:
文件偏移:0x15A6C4
原字节:
7F 23 03 D5 FC 6F BA A9
新字节:
20 00 80 52 C0 03 5F D6
最终版本一共保留两个补丁:
| 文件偏移 |
原字节 |
新字节 |
作用 |
0x15A6C4 |
7F 23 03 D5 FC 6F BA A9 |
20 00 80 52 C0 03 5F D6 |
公共授权查询固定返回 true |
0x28D600 |
28 11 88 1A |
1F 20 03 D5 |
ECDSA 失败状态固定走成功分支 |
原始 getter 和 setter 都不需要修改,保留插件自身的键名映射、配置同步和界面刷新逻辑。
七、替换与测试
把补丁后的 dylib 替换到 tweak 原来的安装位置,保持文件名、权限和配套 plist 不变。之后彻底结束微信进程,再重新启动。
最终结果是六个开关都可以正常操作,五个原本受限的功能也能实际生效。