1 ポイント 投稿者 GN⁺ 2025-02-19 | 1件のコメント | WhatsAppで共有
  • iOSアプリのリバースエンジニアリングでは、実行中のアプリを観察・操作できる必要があるが、このウィジェットアプリは デバッガ遮断・コード注入遮断・脱獄検知 を併用している
  • 中核の遮断は ptracePT_DENY_ATTACH または同等の効果を持つ直接システムコール(svc #0x80)で実装でき、単純な ptrace ブレークポイントだけでは捕捉できない
  • 直接システムコールは、バイナリ内で mov w16, #26 パターンを探し、svc の位置にブレークポイントを置き、lldb jump でその命令を飛ばす方法で回避する
  • 脱獄検知後に端末を soft-reboot/respring させる動作は、snapshotViewAfterScreenUpdates:無限ループ で呼び出す関数と結び付いており、関数の先頭で thread return して飛ばせる
  • コード注入後のクラッシュは、別個のランタイムフレームワーク検査というより App Group権限の喪失 に近く、containerURLForSecurityApplicationGroupIdentifier: を一時ディレクトリに差し替える swizzle によって通常端末でも実行可能になる

複数の保護機構を重ねたiOSウィジェットアプリ

  • 対象はApp Storeのウィジェットアプリで、一般的なウィジェットアプリより強い 防御動作 を含んでいる
    • デバッガのattachを遮断
    • コード注入時にアプリを終了
    • 脱獄状態で実行すると端末全体が soft-reboot/respring する
  • iOSアプリが脱獄検知やコード難読化のような保護機構を入れること自体は珍しくないが、このアプリは複数の方式を併用している
  • アプリ内部のほかの興味深い動作は後続の記事の題材として残されている

PT_DENY_ATTACH によるデバッガattach遮断

  • 脱獄端末では通常、ssh で接続して debugserver を起動し、別のコンピュータの lldb から接続してアプリをデバッグできる
  • 同じ方法でこのアプリに debugserver を付けると Segmentation fault が発生し、attachに失敗する
  • 原因は ptracePT_DENY_ATTACH リクエストにある
    • ptrace はiOSではprivate APIで、macOSではpublic API
    • PT_DENY_ATTACH は以後、親プロセスからのtraceを拒否するフラグを設定する
    • すでにtrace中なら、アプリは ENOTSUP 状態で終了する
    • このフラグが設定されたプロセスを親がtraceしようとすると、親側で segmentation violation が発生する
  • 単純な実装なら ptrace(PT_DENY_ATTACH, 0, 0, 0) の呼び出しで可能
    • iOSのprivate APIなので、実際の呼び出しでは dlopendlsymlibsystem_kernel.dylibptrace シンボルを探す必要がある
    • この方式は ptrace にブレークポイントを置き、thread return で呼び出しを飛ばせば比較的容易に回避できる

簡単な回避が通用しなかった理由

  • PT_DENY_ATTACH は呼び出された後にのみデバッガを防ぐため、アプリコード実行前にattachすれば回避地点を作れる
  • debugserver をプロセスに直接付けずに起動した後、lldbprocess attach --name TopWidget --waitfor としてアプリ起動を待てば、アプリコード実行前にattachできる
  • しかしこのアプリでは、b ptrace ブレークポイントは最初は解決されず、その後解決されても実際にはhitしないままアプリが終了した
  • アプリが ptrace 関数呼び出しの代わりに、同じ効果を持つ 直接システムコール を使っていたためである

直接システムコール位置の特定

  • ptrace 関数のdisassemblyには、核心として svc #0x80 システムコールが含まれている
    • x0 には PT_DENY_ATTACH の値 31 が入る
    • x1x2x3 には未使用の引数 0 が入る
    • x16 には ptrace システムコール番号 26 が入る
  • アプリは ptrace 関数を呼ばず、inline assembly で同じレジスタ値を設定した後に svc #0x80 を直接実行できる
    • この方式なら dlopendlsym のような疑わしいprivate API lookupを避けられる
    • 共通関数である ptrace にブレークポイントを置く方法では捕まえにくい
  • 回避するには、復号済みアプリバイナリをdisassemblerで開き、ptrace システムコール位置を探す必要がある
  • 検索対象は mov x16, #26、または同じレジスタの32ビットviewである mov w16, #26
    • armconverter.commov x16, #26 のバイト列 50 03 80 D2 を取得し、バイナリ検索に使える
    • mov x16, #26 では結果がなく、mov w16, #26 の検索で4件の結果が見つかった
  • そのうち2件は周辺命令が想定パターンと一致せず、3件目の結果で次の形のコードが確認できた
    • MOV X0, #0x1F
    • MOV X1, #0
    • MOV X2, #0
    • MOV X3, #0
    • MOV W16, #0x1A
    • SVC 0x80
  • 4件目の結果は同じ関数の別branchで、この関数がデバッガattachを防ぐ位置だと確認された

svc 命令を飛ばす

  • disassemblerで確認した svc のアドレスは 0x102A2BB140x102A2BB68
  • lldb では、バイナリ基準アドレスを実際のロードアドレスへ変換するため -s TopWidget を付けてブレークポイントを設定する
    • br s -a 0x102A2BB14 -s TopWidget
    • br s -a 0x102A2BB68 -s TopWidget
  • 実行を続けると svc #0x80 の位置でブレークポイントが止まる
  • 最も簡単な回避は、次の命令アドレスへ jump してシステムコール自体を実行しない方法
    • 例では現在命令の次のアドレス 0x10327bb18 に対して jump *0x10327bb18 を実行している
  • この手順を経ると、デバッガがattachした状態でアプリ内部に入れる

端末をsoft-rebootさせる動作

  • デバッガattachを回避した後も、アプリは端末を soft-reboot/respring させる保護動作を実行する
  • 今回は lldb が引き続きattachしているため、プロセスが SIGKILL を受けた状態とstacktraceを確認できる
  • stacktraceには画面内容をキャプチャする流れが現れる
    • QuartzCoreCARenderServerSnapshot
    • UIKitCore_UISnapshotScreenWindowsRectAfterCommit
    • アプリ内部関数 TopWidget の unnamed symbol
  • lldb image lookup でランタイムアドレスをバイナリ基準アドレス 0x100041898 に変換し、disassemblerで該当関数を確認する
  • decompile結果では、その関数は 無限ループ の中で次の処理だけを繰り返していた
    • +[UIScreen mainScreen] の呼び出し
    • 返された画面オブジェクトに対する snapshotViewAfterScreenUpdates: の呼び出し
    • 結果は使わずに release される
  • snapshotViewAfterScreenUpdates: はview snapshotを作るpublic APIだが、このアプリはメモリ集約的な呼び出しを無限に繰り返していた
  • 関連動画では、この呼び出しの起点が com.apple.tw.twrr notification と関係し、端末に対する risk check を通過できないと respring することが確認されている
  • 回避は、その関数の先頭にブレークポイントを置き、hit時に thread return で関数実行を飛ばす方法で可能

コード注入時に発生したクラッシュ

  • デバッグ中には、画面上のボタンのアクセシビリティ情報をログに出すような複雑なユーティリティを、アプリに注入したframework内で実装し、debuggerから呼び出せる
  • 脱獄端末がない場合でも、FridaFlex を注入すればアプリの初期探索はできる
  • 一般にこの種の注入は、アプリをresignするツールで行われ、例として Sideloadly が使われる
  • このアプリはresign後に実行すると即座にcrashする
  • crash stack の先頭アドレス 0x1002027D4 をdisassemblerで見ると BRK 命令があり、nil の force-unwrap のような意図的クラッシュの状況と一致する
  • decompileされたコードで問題らしい呼び出しは containerURLForSecurityApplicationGroupIdentifier:
    • このメソッドは、同じgroupのアプリと拡張が共にアクセスできるフォルダURLを返す
    • App Group はコード署名の過程で定義される
    • コード注入後にresignすると既存アプリ署名が破棄され、App Group権限も失われる
    • そのためURLではなく nil が返り、アプリがそれを force-unwrap してcrashした可能性が高い
  • ウィジェットアプリはホーム画面ウィジェット表示を担うextensionとApp Groupを共有する必要があるため、この問題は防御目的の意図的動作というより、一般的なコード署名問題に近い

App Group問題の回避

  • 最も簡単な解決はアプリをresignしないこと
    • 脱獄端末では jailbreak tweak でframeworkを注入しつつ、アプリをresignせずに済む
    • 無効な署名のコードを実行したり、任意のアプリを任意のgroupに追加したりすることも可能
  • 脱獄端末がなく、resignが必要な場合でも、小さなframeworkでメソッドをswizzleして回避できる
  • 例のコードは NSFileManagercontainerURLForSecurityApplicationGroupIdentifier: を置き換える
    • 元のメソッドが呼ばれると replacement method が実行される
    • replacement method は shared container ではなく temporaryDirectory を返す
  • この置き換えは、アプリが本来望む動作と同じではない
    • 本来アプリは、アプリとextensionが共にアクセスできる共有フォルダを必要としている
    • 一時ディレクトリはそのような共有フォルダではない
  • ただし、メインアプリの挙動だけを調べる目的なら十分な場合がある
    • このような小さなパッチは app extension を壊す可能性がある
    • メインアプリの基本機能だけ必要なら問題にならないこともある
  • より大きな回避策として、新しいApp Groupを作成し、メインアプリとすべてのextensionをそのgroupでresignしたうえで、関連メソッドが新しいgroup identifier を使うようswizzleする方法もある
    • はるかに大きな作業であり、どうしても必要でなければ jailbroken device を用意した方がよいかもしれない
  • このframeworkを Flex のようなツールと一緒に注入すると、通常端末でもアプリは正常に実行される
  • 脱獄端末では、先のanti-debuggingとrespring保護を再び回避する必要があるが、その後は Flex を注入した状態でアプリ内部に入れる

最終状態

  • 最終的にアプリは、デバッガattach、脱獄検知回避、コード注入がすべて可能な状態になった
  • 実際にアプリ内部で何を見ようとしていたのかは、次の記事の題材として残されている

1件のコメント

 
GN⁺ 2025-02-19
Hacker Newsの意見
  • Bryce Bostwickは、アプリのデバッグとリバースエンジニアリングで本当に素晴らしく刺激的な仕事をしている
    YouTubeで知り、TikTokを猫動画だけが出るように改造する動画(https://youtu.be/YW3jL2gI9IE)を見て、Instagramで自分が使うメッセージ機能だけを残し、ほかを全部取り除く改造を試してみた
    以前からWindhawk(https://windhawk.net/)のような形でWindowsを改造する方法、特に改造とリバースエンジニアリングをもっと掘り下げたいと思っていたが、BryceはiOSでそうした作業をリアルタイムの段階的な動画でうまく紹介してくれている

    • Android側にも似たような人がいれば、もっと学んでみたい
      Revancedで驚くようなことができるのは見たが、そういう作業をどう始めるのかを教えてくれる良いガイドはあまりなさそう
    • 説明されていたことを自分でもやってみようと思う
      もし手順をまとめておくなら興味がある
      写真を投稿したり友人とチャットしたりするだけでもReelsを見せられるのが嫌だ
  • アンチデバッグ、さらにはアンチデバッグを防ぐ手段を再び防ぐテクニックは、DOS/Windows方面では昔からよくあった
    古いクラッキングやアンパックの資料を見ると、こうした内容がさまざまな深さで扱われている
    ユーザーがアプリの動作をどれだけ簡単に制御できるかは、そのプラットフォームがどれだけユーザーに敵対的かと反比例する
    PT_DENY_ATTACHは、まさにそのユーザー敵対性のために作られた機能のように見える
    Windowsにはそういう機能はないと理解していて、代わりにアプリを自分自身にアタッチさせる手法が使われる
    https://www.x86matthew.com/view_post?id=selfdebug
    https://anti-debug.checkpoint.com/techniques/interactive.htm...

    • その通り。PT_DENY_ATTACHは昔、AppleがiTunesのDRM対策の一部として文字どおり自ら作った機能
  • AppleのApp Store審査が、直接システムコールを行うアプリを拒否しないのは少し驚き
    Appleプラットフォームのシステムコールは安定したABIではないので、すべてのシステムコールはlibSystemを経由すべきであり、libSystemを通さず直接システムコールを行うアプリは、本来すべきでないことをしていることになる
    同様に、ここで筆者がsvc 0x80ではなくmov w16, #26をコード内で探した理由も気になる

    • svc 0x80はどんなシステムコールでも実行する命令で、実際にどの呼び出しが実行されるかはx16レジスタによって決まる
      アプリは関係のないシステムコールを大量に行うだろうから、そこにブレークポイントを置いても役に立たないはず
      少なくとも動画ではそう説明していた
    • コンパイラが時々システムコールラッパーをインライン化することがあるので、静的に確認するのはそれほど簡単ではない
      同じ理由でSVC命令を探すと、結果が大量に出てくるはず
      X16に移される正確なシステムコールIDを探せば、すぐ見つけられる
  • 記事の筆者です。質問があれば答えます。共有してくれたxmprtに感謝

    • YouTubeで動画を見たが面白かった
      記事版も用意してくれてよかった
    • とても興味深い記事で、こういう低レベルの携帯電話リバースエンジニアリング記事はいつも読みたいと思っていた
      筆者にいくつか聞きたい。最も有名な商用ツールがGuardsquareで合っているなら、この種の簡単な逆アセンブルを防ぐ新しいものを提供していると見ているのか気になる
      TopWidgetsがそれに近い保護を使っていたのか、それとも自前で作った程度のものだったのかも気になる
    • 動画はとても興味深いのに、もっと多くの人が見たり記事を読んだりしていないのが驚き
      個人的にはAndroidを使っているので技術的にすぐ適用できるわけではないが、iOSの低レベルデバッグがどう動くのかを学ぶ点で、なお大きな価値がある
    • iOSにもPTRACE_SYSCALLのようにシステムコールの入口で捕まえて戻り値を変えたり、SVCがどこで発生するかを検出したりできる機能があるのか気になる
  • 一番上の動画は、これまで見たプログラミング動画の中でも最高レベル
    展開が速く、ちょうどよい前提知識を想定していて、動画の流れを妨げない優れたデモがある

  • 素晴らしい記事
    これが過度に偏執的な普通のアプリだったのか、それともそもそもマルウェアとして疑われてデバッグされていたアプリだったのか、本当に気になる
    そうでないなら、かけた労力はかなり過剰に見える

    • 私の判断では、単に過度に偏執的なアプリに近い
      ウィジェットでかなり面白いことをしていたので、それを守ろうとしたのだと思う。ただ、そうした戦略も結局は少しずつ漏れ始めていた
      バイナリの中には興味深いものもあった
      ある時、Windowsの.isoをダウンロードしているように見えるコードをなぜ見ているのか突き止めようとしていたが、実際にその通りで、ネットワーク速度テストウィジェットに使われていた
    • あるいは、アプリ自体が別のロゴなどで再コンパイルされた場合に、著作権侵害を証明しようとしていたのかもしれない
  • 「PT_DENY_ATTACHを回避する(ハードモード)」より難しいモードもある
    以前macOSでカーネルをパッチして、PT_DENY_ATTACHが何もしないようにしたことがある
    Macはパッチ済みカーネルを実行するのが実際にはかなり簡単なほうだが、iOSではKTRRのようなものがあるため、はるかに面倒そう
    XNUは技術的にはオープンソースだが、再コンパイルするよりもヘックスエディタでパッチを当てるほうが簡単だった

    • kernel task portを使ってproc構造体のビットを反転させることもできる
      署名されていないコードページとRWXを許可して、JITも可能にできる
  • 「脱獄を有効にして実行すると、端末全体がクラッシュする」なら、ストアにマルウェアとして報告すればいいのでは?
    端末をクラッシュさせるのは明らかにマルウェアの挙動であり、隠そうとしている別の悪意ある動作がほかに何かあるのではないかとも疑うべき
    こういうゴミを防ぐのがAppleのクローズドなエコシステムの名目ではなかったのか?

    • 「脱獄を有効にして」というのは、クローズドなエコシステムの外という意味
      Appleは脱獄済み端末でアプリがクラッシュすることにはあまり気にしなさそう
  • com.apple.tw.twrrという通知が本当に気になる
    なぜcom.appleで始まるのか?
    ここで問題になっているアプリはAppleのアプリではなく、Top Widgetsというアプリに見える

    • 通知名は任意の文字列
      衝突を避けるために、そういう「完全な名前」を使うのが慣例
      この場合は、開発者がたまたまその接頭辞を選んだのだと思う
  • ツールが違うだけで、これは1980年代にブートトレースでApple IIのコピー防止を破っていたのとまったく同じ感覚
    変わらないものもある