iOS 7 之后的 Safari 提供了远程调试设备上网页的功能。在设备和 mac 端的 Safari 上均开启开发者功能之后,可以用 USB 连接手机,然后在 Develop 菜单中选择对应的页面打开 WebInspector:
但是这个调试功能只对 Xcode 真机调试的 App 和 MobileSafari 开启了。
如果基于某些原因想要调试其他厂商的 App 应该怎么办?
已知的一种方式是重签名,获取到目标 App -> 砸壳 -> MonkeyDev 重打包。这种方法只能针对某一个应用,而且重签名后很多应用的行为会出现异常甚至闪退,总结起来就是,限制较多,过程繁琐,无法通用。
那么问题来了,是否有办法写一个 Tweak,像 SSL Kill Switch 2 一样,让越狱设备上的所有 App 都开启 WebView 调试功能呢?
App 是否支持 WebInspector 是通过 entitlement 控制的。已知将 com.apple.security.get-task-allow
设置为 true
之后会允许调试 WebView。Xcode 编译出来的调试版本 App 都会带上这个 entitlement,这也是 lldb 真机调试必须的配置。
MobileSafari 肯定不允许 lldb 调试,不过可以看到(iOS 11.1.2)它注册了一个这样 entitlement:
在 iOS 设备上启用了 WebInspector 之后会出现一个 webinspectord 的守护进程。关于远程调试实现的一些技术细节可以参考 WebKit 远程调试协议实战。
上面提到的文章同样也没有解决 entitlement 的条件,还是需要自己逆向一下。这个进程的代码只有一点点:
其实是放在链接库里了。(注:不同版本的 iOS 会有一些差异,文末会提到)
把 dyld_shared_cache 拖回来分析。
➜ /tmp scp ios:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 ./
Warning: Permanently added '[127.0.0.1]:2222' (RSA) to the list of known hosts.
dyld_shared_cache_arm64 100% 1023MB 33.6MB/s 00:30
之前提到两个 entitlement 是很明显的特征。
- com.apple.private.webinspector.allow-remote-inspection
- com.apple.security.get-task-allow
很快定位到字符串表
通过交叉引用来到如下函数:
bool __cdecl -[RWIRelayDelegateIOS _allowApplication:bundleIdentifier:](id a1, SEL a2, struct {unsigned int var0[8];} *a3, id a4)
{
__int128 *v4; // x21
id v5; // x20
__int64 v6; // x19
char v7; // w20
__int128 v9; // [xsp+0h] [xbp-80h]
__int128 v10; // [xsp+10h] [xbp-70h]
__int128 v11; // [xsp+20h] [xbp-60h]
__int128 v12; // [xsp+30h] [xbp-50h]
__int128 v13; // [xsp+40h] [xbp-40h]
__int128 v14; // [xsp+50h] [xbp-30h]
v4 = (__int128 *)a3;
v5 = a1;
v6 = MEMORY[0x18F5A5488](a4, a2);
if ( qword_1B0981AD0 != -1 )
dispatch_once(&qword_1B0981AD0, &unk_1AC56C870);
if ( byte_1B0981AC8 )
goto LABEL_14;
v14 = v4[1];
v13 = *v4;
if ( MEMORY[0x18F5A547C](v5, selRef__hasRemoteInspectorEntitlement_[0], &v13) & 1 ) // 开启了 allow-remote-inspection
goto LABEL_14;
if ( qword_1B0981AE0 != -1 )
dispatch_once(&qword_1B0981AE0, &unk_1AC56C8B0);
if ( byte_1B0981AD8
&& (v12 = v4[1], v11 = *v4, MEMORY[0x18F5A547C](v5, selRef__hasCarrierRemoteInspectorEntitlement_[0], &v11) & 1) )
{ // 特定条件下检查的是 com.apple.private.webinspector.allow-carrier-remote-inspection
LABEL_14:
v7 = 1;
}
else
{
v10 = v4[1];
v9 = *v4;
v7 = MEMORY[0x18F5A547C](v5, selRef__usedDevelopmentProvisioningProfile_[0], &v9); // 开发版本 App 同样放行
}
MEMORY[0x18F5A5484](v6);
return v7;
}
这正是检查是否允许 WebInspector 的关键函数。
使用 frida hook 框架简单验证一下:
➜ passionfruit git:(master) ✗ frida -U webinspectord
____
/ _ | Frida 10.6.61 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
[iPad 4::webinspectord]-> Interceptor.attach(ObjC.classes.RWIRelayDelegateIOS['- _allowApplication:bundleIdentifier:'].implementation, {
onEnter: function(args) {
this.bundleId = new ObjC.Object(args[3]);
},
onLeave: function(retVal) {
const allow = !retVal.equals(NULL)
console.log(this.bundleId + (allow ? ' allows' : ' does not allow') + ' WebInspect')
if (!allow) {
console.log('now patch it');
retVal.replace(ptr(1));
}
}
});
{}
[iPad 4::webinspectord]-> com.tencent.mipadqq does not allow WebInspect
now patch it
com.mx.MxBrowser-iPhone does not allow WebInspect
now patch it
com.apple.WebKit.WebContent allows WebInspect
com.mx.MxBrowser-iPhone does not allow WebInspect
now patch it
com.apple.WebKit.WebContent allows WebInspect
com.mx.MxBrowser-iPhone does not allow WebInspect
now patch it
每次启动新应用的时候都会调用这个函数做一次判断,将其返回值 patch 为 TRUE,第三方浏览器出现在了 Safari 的调试列表中:
另外 macOS 上的 WebInspector 也有类似函数 __int64 __fastcall -[RWIRelayDelegateMac _allowApplication:bundleIdentifier:]
,检查的 entitlement 键名略有不同。
用 THEOS 写成 Tweak,简单粗暴:
{ Filter = { Bundles = ( "com.apple.webinspectord" ); }; }
Tweak.xm
%hook RWIRelayDelegateIOS
- (BOOL)_allowApplication:(void *)ignored bundleIdentifier:(NSString *)bundleId {
%log;
NSLog(@"Force WebInspect enable for %@", bundleId);
return TRUE;
}
%end
本文在 11.1.2 和 10.3.3 上测试通过。
有同学反馈 10.0.2 没有 RWIRelayDelegateIOS 类,我验证了一下 10.0.3 的 IPSW 固件,函数是一样的。只不过直接编译到 webinspectord 而不是链接 WebInspector.framework。拆分链接库应该是 iOS 11 开始的。
在 iOS 9.3.3 上类名不一样,应该对 WebInspectorRelayDelegateIOS
的 - _allowApplication:bundleIdentifier:
进行 hook。其他 iOS 版本的兼容性还有待进一步分析。
不过以上出现的几个方法都需要使用 Code Signing Services 的 api,因此理论上拦截这个更底层的 api 可以做到通用。以下是 frida 的原型:
const SecTaskCopyValueForEntitlement = Module.findExportByName(null, 'SecTaskCopyValueForEntitlement');
const CFRelease = new NativeFunction(Module.findExportByName(null, 'CFRelease'), 'void', ['pointer']);
const CFStringGetCStringPtr = new NativeFunction(Module.findExportByName(null, 'CFStringGetCStringPtr'),
'pointer', ['pointer', 'uint32']);
const kCFStringEncodingUTF8 = 0x08000100;
const expected = [
'com.apple.security.get-task-allow',
'com.apple.private.webinspector.allow-remote-inspection',
'com.apple.private.webinspector.allow-carrier-remote-inspection',
'com.apple.webinspector.allow'
];
Interceptor.attach(SecTaskCopyValueForEntitlement, {
onEnter: function(args) {
const p = CFStringGetCStringPtr(args[1], kCFStringEncodingUTF8);
const ent = Memory.readUtf8String(p);
if (expected.indexOf(ent) > -1)
this.shouldOverride = true
},
onLeave: function(retVal) {
if (!this.shouldOverride)
return
if (!retVal.isNull())
CFRelease(retVal);
retVal.replace(ObjC.classes.NSNumber.numberWithBool_(1));
}
})
整理为 Tweak 插件:GlobalWebInspect ,支持 A12 ARM64e。
本文为原创文章,版权归字节时代所有,欢迎分享本文,转载请保留出处!
好文章,学习了。
我在iPhone 5s ios 10.3.3 越狱设备上安装插件后发现不能正常工作,提示无可检查的应用。几秒钟后显示在设备上启用网页检查器,实际上我是开启了手机Safari的web检查选项的
@张三手上目前没有5s的设备,没法测试。
目前我测试过的设备包括:iPhone 11 Pro 14.2, iPhone 7 13.6,iPhone 6s 13.4,没发现问题。
先尝试在手机终端上执行 killall -9 webinspectord 重启下检查器服务。
或者,在电脑的控制台上看下有没有输出插件的日志,确认下插件是否生效了。
如果还不行,建议使用上面这些设备再试试。