作者:CodeColorist
作者博客:https://blog.chichou.me/
iOS 7 之后的 Safari 提供了远程调试设备上网页的功能。在设备和 mac 端的 Safari 上均开启开发者功能之后,可以用 USB 连接手机,然后在 Develop 菜单中选择对应的页面打开 WebInspector:
但是这个调试功能只对 Xcode 真机调试的 App 和 MobileSafari 开启了。
App 是否支持 WebInspector 是通过 entitlement 控制的。已知将 com.apple.security.get-task-allow
设置为 true
之后会允许调试 WebView。Xcode 编译出来的调试版本 App 都会带上这个 entitlement,这也是 lldb 真机调试必须的配置。
MobileSafari 肯定不允许 lldb 调试,不过可以看到(iOS 11.1.2)它注册了一个这样 entitlement:
如果想检视其他 App 的内容,有没有什么好办法?
重打包不仅可以对付 apk,还可以帮助逆向 iOS 应用。
要对一个应用进行重新打包,首先需要拿到未加密的安装包。通过 App Store 下载的安装包都经过加密处理,需要对其进行“砸壳”(解密)。熟悉逆向的同学很快就掏出 dumpdecrypted 等工具解决了,不过这需要有已越狱的设备;偷懒的也可以使用别人砸好的应用,比如从某些助手之流下载。
既然 WebInspector 需要这个 entitlement,那么直接修改掉可执行文件的代码签名,加上这个特权即可。ldid 和 jtools 等工具可以搞定,因为重签名不是本文的重点,因此就简单带过。最后推荐一个懒人工具 MonkeyDev,可以在 Xcode 中自动化完成添加调试签名的流程,只要一个砸壳好的 ipa。
重打包也不是万能的。由于签名不匹配会导致之前应用的数据丢失,另外一些 App 可能对自身做额外的完整性检查,以对抗重打包行为。
有了越狱环境能做很多事情。
在 Android 上,WebView 提供了一个 setWebContentsDebuggingEnabled
方法,可以启用 devtools 调试网页。写一个 Xposed 插件就可以全局开启。在 iOS 上有没有类似的 kill switch?
在 iOS 设备上启用了 WebInspector 之后会出现一个 webinspectord
的服务进程。在 iOS 11.1.2 上,这个进程的代码只有一点点:
其实是放在链接库里了。
(注:不同 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
前面提到的两条线索已经很明显了:
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; }
这正是检查是否允许调试的关键函数。其调用了 Code Signing Service 函数SecTaskCopyValueForEntitlement 检查 XPC 调用者是否具有指定的 entitlement 权限。
使用 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:]
行为存在少许差异,检查的 Key 名是不一样的。不过相比手机设备上丢失符号的版本,这个显然可读性要强得多了。
THEOS 是 iOS 安全研究不可或缺的开发工具。把这个 hook 做成越狱插件自然用起来更方便。THEOS 创建工程时设置注入进程目标为 webinspectord:
{ Filter = { Bundles = ( "com.apple.webinspectord" ); }; }
Tweak.xm 的 hook 逻辑简单粗暴:
%hook RWIRelayDelegateIOS // for 11.1.2 - (BOOL)_allowApplication:(void *)ignored bundleIdentifier:(NSString *)bundleId { %log; NSLog(@"Force WebInspect enable for %@", bundleId); return TRUE; } %end
配置好 SSH 环境变量后 make package install
部署到设备,搞定。在 11.1.2 和 10.3.3 上测试通过。
有同学反馈 10.0.2 的 WebInspector.framework 没有 RWIRelayDelegateIOS
类。我验证了一下 10.0.3 的 IPSW 固件,函数是一样的,只不过直接编译到 webinspectord 而不是放进动态链接库。拆分链接库应该是 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 插件:
https://github.com/ChiChou/GlobalWebInspect