- iOSアプリのリバースエンジニアリングでは、実行中のアプリを観察・操作できる必要があるが、このウィジェットアプリは デバッガ遮断・コード注入遮断・脱獄検知 を併用している
- 中核の遮断は
ptrace の PT_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に失敗する
- 原因は
ptrace の PT_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なので、実際の呼び出しでは
dlopen、dlsym で libsystem_kernel.dylib の ptrace シンボルを探す必要がある
- この方式は
ptrace にブレークポイントを置き、thread return で呼び出しを飛ばせば比較的容易に回避できる
簡単な回避が通用しなかった理由
PT_DENY_ATTACH は呼び出された後にのみデバッガを防ぐため、アプリコード実行前にattachすれば回避地点を作れる
debugserver をプロセスに直接付けずに起動した後、lldb で process attach --name TopWidget --waitfor としてアプリ起動を待てば、アプリコード実行前にattachできる
- しかしこのアプリでは、
b ptrace ブレークポイントは最初は解決されず、その後解決されても実際にはhitしないままアプリが終了した
- アプリが
ptrace 関数呼び出しの代わりに、同じ効果を持つ 直接システムコール を使っていたためである
直接システムコール位置の特定
ptrace 関数のdisassemblyには、核心として svc #0x80 システムコールが含まれている
x0 には PT_DENY_ATTACH の値 31 が入る
x1、x2、x3 には未使用の引数 0 が入る
x16 には ptrace システムコール番号 26 が入る
- アプリは
ptrace 関数を呼ばず、inline assembly で同じレジスタ値を設定した後に svc #0x80 を直接実行できる
- この方式なら
dlopen、dlsym のような疑わしいprivate API lookupを避けられる
- 共通関数である
ptrace にブレークポイントを置く方法では捕まえにくい
- 回避するには、復号済みアプリバイナリをdisassemblerで開き、
ptrace システムコール位置を探す必要がある
- 検索対象は
mov x16, #26、または同じレジスタの32ビットviewである mov w16, #26
- armconverter.com で
mov 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 のアドレスは 0x102A2BB14 と 0x102A2BB68
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には画面内容をキャプチャする流れが現れる
QuartzCore の CARenderServerSnapshot
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から呼び出せる
- 脱獄端末がない場合でも、Frida や Flex を注入すればアプリの初期探索はできる
- 一般にこの種の注入は、アプリを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して回避できる
- 例のコードは
NSFileManager の containerURLForSecurityApplicationGroupIdentifier: を置き換える
- 元のメソッドが呼ばれると 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件のコメント
Hacker Newsの意見
Bryce Bostwickは、アプリのデバッグとリバースエンジニアリングで本当に素晴らしく刺激的な仕事をしている
YouTubeで知り、TikTokを猫動画だけが出るように改造する動画(https://youtu.be/YW3jL2gI9IE)を見て、Instagramで自分が使うメッセージ機能だけを残し、ほかを全部取り除く改造を試してみた
以前からWindhawk(https://windhawk.net/)のような形でWindowsを改造する方法、特に改造とリバースエンジニアリングをもっと掘り下げたいと思っていたが、BryceはiOSでそうした作業をリアルタイムの段階的な動画でうまく紹介してくれている
Revancedで驚くようなことができるのは見たが、そういう作業をどう始めるのかを教えてくれる良いガイドはあまりなさそう
もし手順をまとめておくなら興味がある
写真を投稿したり友人とチャットしたりするだけでもReelsを見せられるのが嫌だ
アンチデバッグ、さらにはアンチデバッグを防ぐ手段を再び防ぐテクニックは、DOS/Windows方面では昔からよくあった
古いクラッキングやアンパックの資料を見ると、こうした内容がさまざまな深さで扱われている
ユーザーがアプリの動作をどれだけ簡単に制御できるかは、そのプラットフォームがどれだけユーザーに敵対的かと反比例する
PT_DENY_ATTACHは、まさにそのユーザー敵対性のために作られた機能のように見える
Windowsにはそういう機能はないと理解していて、代わりにアプリを自分自身にアタッチさせる手法が使われる
https://www.x86matthew.com/view_post?id=selfdebug
https://anti-debug.checkpoint.com/techniques/interactive.htm...
AppleのApp Store審査が、直接システムコールを行うアプリを拒否しないのは少し驚き
Appleプラットフォームのシステムコールは安定したABIではないので、すべてのシステムコールはlibSystemを経由すべきであり、libSystemを通さず直接システムコールを行うアプリは、本来すべきでないことをしていることになる
同様に、ここで筆者が
svc 0x80ではなくmov w16, #26をコード内で探した理由も気になるsvc 0x80はどんなシステムコールでも実行する命令で、実際にどの呼び出しが実行されるかはx16レジスタによって決まるアプリは関係のないシステムコールを大量に行うだろうから、そこにブレークポイントを置いても役に立たないはず
少なくとも動画ではそう説明していた
同じ理由でSVC命令を探すと、結果が大量に出てくるはず
X16に移される正確なシステムコールIDを探せば、すぐ見つけられる
記事の筆者です。質問があれば答えます。共有してくれたxmprtに感謝
記事版も用意してくれてよかった
筆者にいくつか聞きたい。最も有名な商用ツールがGuardsquareで合っているなら、この種の簡単な逆アセンブルを防ぐ新しいものを提供していると見ているのか気になる
TopWidgetsがそれに近い保護を使っていたのか、それとも自前で作った程度のものだったのかも気になる
個人的にはAndroidを使っているので技術的にすぐ適用できるわけではないが、iOSの低レベルデバッグがどう動くのかを学ぶ点で、なお大きな価値がある
一番上の動画は、これまで見たプログラミング動画の中でも最高レベル
展開が速く、ちょうどよい前提知識を想定していて、動画の流れを妨げない優れたデモがある
素晴らしい記事
これが過度に偏執的な普通のアプリだったのか、それともそもそもマルウェアとして疑われてデバッグされていたアプリだったのか、本当に気になる
そうでないなら、かけた労力はかなり過剰に見える
ウィジェットでかなり面白いことをしていたので、それを守ろうとしたのだと思う。ただ、そうした戦略も結局は少しずつ漏れ始めていた
バイナリの中には興味深いものもあった
ある時、Windowsの
.isoをダウンロードしているように見えるコードをなぜ見ているのか突き止めようとしていたが、実際にその通りで、ネットワーク速度テストウィジェットに使われていた「PT_DENY_ATTACHを回避する(ハードモード)」より難しいモードもある
以前macOSでカーネルをパッチして、PT_DENY_ATTACHが何もしないようにしたことがある
Macはパッチ済みカーネルを実行するのが実際にはかなり簡単なほうだが、iOSではKTRRのようなものがあるため、はるかに面倒そう
XNUは技術的にはオープンソースだが、再コンパイルするよりもヘックスエディタでパッチを当てるほうが簡単だった
署名されていないコードページとRWXを許可して、JITも可能にできる
「脱獄を有効にして実行すると、端末全体がクラッシュする」なら、ストアにマルウェアとして報告すればいいのでは?
端末をクラッシュさせるのは明らかにマルウェアの挙動であり、隠そうとしている別の悪意ある動作がほかに何かあるのではないかとも疑うべき
こういうゴミを防ぐのがAppleのクローズドなエコシステムの名目ではなかったのか?
Appleは脱獄済み端末でアプリがクラッシュすることにはあまり気にしなさそう
com.apple.tw.twrrという通知が本当に気になる
なぜ
com.appleで始まるのか?ここで問題になっているアプリはAppleのアプリではなく、Top Widgetsというアプリに見える
衝突を避けるために、そういう「完全な名前」を使うのが慣例
この場合は、開発者がたまたまその接頭辞を選んだのだと思う
ツールが違うだけで、これは1980年代にブートトレースでApple IIのコピー防止を破っていたのとまったく同じ感覚
変わらないものもある