作者: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


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/555/