iOSDC2020 Day2 16:20〜 https://fortee.jp/iosdc-japan-2020/proposal/62a95cf9-1b1b-401a-8a1c-11c0490aad00
【Special Thanks】 発表資料のレビューとリハに付き合ってくれた ddd503(@dk31486981 )
本当はこわいLLDBみやし
View Slide
⾃⼰紹介Name: みやしTwitter: @po_miyasakaWork:minne @ GMOペパボ OSSの宣伝: @yhkaplan⽒とVisionAPIを使ったクレカリーダーのOSSを作成しました。 iOS13以降必須ですが、card.ioよりも読み取れるカードも多く、UIもカスタマイズしやすいので、ぜひ活⽤してみてください! https://github.com/yhkaplan/credit-card-scanner2
発表内容Objective-Cでコンパイルされたアプリであれば、LLDBで実⾏中に⾊々なハックができてしまいます。今回の発表では、実際にアプリにアタッチしてハックしてみようと思います。(ちなみに、SwiftでかかれたコードではLLDBでハックできませんでした。) 3
⽬次• 下準備• スライドについての備考• 拡張コマンドのインストール• SIPを解除する• アプリにアタッチする• アプリ全体を眺める• 依存しているダイナミックライブラリの⼀覧化• アセンブリを書き出す• アプリ内でハードコードされた⽂字列を⼀覧化• ローカルデータベースの中身を⾒る• アプリ内のDocumentsやCachesの中身を⾒る• 表示中のView階層を⾒る4• 気になる箇所にブレークポイントを挟む• 正規表現でマッチする関数でブレーク• 関数のアドレスを指定してブレーク• UI操作契機でブレーク• ブレークした関数の引数の中身を調べる• 引数とレジスタの関係• 引数が⽂字列だった場合• 引数がクラスインスタンスだった場合• 引数が関数だった場合• ブレークした関数の返り値を調べる• ブレークした関数の引数や返り値の書き換え• リバースエンジニアリングの防⽌法• 結論
スライドについての備考• ターミナルコマンドには「$」LLDBコマンドには「(lldb)」のプレフィックスをつけます。• ⽂字数削減のため「メソッド」のことでも「関数」と表記してます。• このスライドは私個⼈の趣味の範囲で作ったものであり、所属する組織団体とは関係ありません。下準備5
LLDBの便利な拡張コマンドをインストール• https://github.com/facebook/chisel 使⽤するコマンド: pvc, pviews• https://github.com/po-miyasaka/LLDB使⽤するコマンド: vinfo, cbd• https://github.com/DerekSelander/LLDB使⽤するコマンド: lsof, ls, lookup, tv, yoink ※ Derek⽒の「Advanced Apple Debugging & Reverse Engineering」はおすすめです。下準備6
SIPを解除するMACはSIPによって守られているため、デフォルト設定ではサードパーティのアプリにアタッチできません。MACをリカバリーモードにしてターミナルで以下を叩くことでSIPをDisableにできます。⚠ SIPの解除はMACを脆弱にするのでしない⽅がいいです。 試す場合は⾃⼰責任でお願いします! $ csrutil disable && reboot 下準備7
任意のMACアプリにアタッチするXcodeで以下のようなMy Macをターゲットに持つProjectを開きます。 Debug > Attach to Process PID or Name から 任意のアプリ名を⼊⼒してアタッチします。下準備8
任意のMACアプリにアタッチする②ターミナルじゃなくてXcodeでアタッチするのは、Xcodeが便利だからです。レジスタのフォーマットは、右クリックの「Edit Summary Format」で以下のように⼊⼒すればOKです。以下のNSStringの部分をcharに変えたりすると⽂字列が表示されるようになります。下準備9
依存しているダイナミックライブラリを⼀覧化 絶対パスで出⼒されるので、ライブラリにアクセスしやすくて便利です。以下のコマンドを使うとライブラリがどのようにメモリに展開されているかも表示してくれます。Static ライブラリは表示されません。 (lldb) image list $ vmmap アプリ全体を眺める10
アセンブリを書き出すパスは image list で出⼒されたパスをそのまま指定できます。アセンブリを眺めると、関数名とか、⽂字列が表示されており、処理を把握するヒントになる。 (Swiftでビルドされている場合はそのようなヒントは残らない)$ otool -tvV /Application/Hoge/Contents/MacOS/Hogeアプリ全体を眺める11
アプリ内でハードコードされた⽂字列を⼀覧化ソースコードにハードコードされた⽂字列(ascii)が表示されます。IBに紐付いた変数名やプロトコル名なども表示されるようです。$ strings -t x /Application/Hoge/Contents/MacOS/Hogeアプリ全体を眺める12
ローカルデータベースの中身を⾒るアプリから参照している外部ファイルのパスが⼀覧化されます。その中に.sqliteや.realmがある場合は、ターミナルのsqliteコマンドやRealm Browserなどを使⽤することでデータベースの中身を確認することができます。(lldb) lsofアプリ全体を眺める13
アプリ内のDocumentsやCachesの中身を⾒る気になったファイルは以下のコマンドによりFinderに表示することができます。(lldb) ls ./Library/Caches(lldb) yoink ./Library/Caches/hoge.csvアプリ全体を眺める14
表示中のView階層を⾒るCatalyst等でUIKitを使っていればVC階層も表示できる 出⼒されたアドレスを使ってViewのisHiddenを切り替えることができます。viewの特定に役⽴ちます。 (lldb) pviews (lldb) pvc (lldb) tv 0x00006000020d8510アプリ全体を眺める15
正規表現にマッチする関数でブレーク-s でモジュールを限定できる。以下コマンドはUIKitに含まれるすべての関数でブレークする。※ シンボルがない場合は全てのシンボルが___lldb_unnamed_symbol5013$$Hogeのように命名されるので⽂字列指定はできない。Objective-Cであれば、後述のレジスタ内の情報から関数を判定できるが、Swiftでコンパイルされている場合はメソッド名を判別することは不可能(lldb) rbreak '\-\[UIViewController\ ' -s UIKit(lldb) rbreak .* -s UIKit気になる箇所にブレークポイントを挟む16
関数のアドレスを指定してブレーク前述で出⼒したアセンブリの任意の箇所でブレークしたい場合は、 記載されているオフセットにベースアドレスを⾜したものを指定する必要があります。ベースアドレスは実⾏中のアプリのアセンブリ(図左)と 出⼒したアセンブリ(図右)を⾒⽐べて、 同じに⾒える処理のアドレスの差分を計算すれば割り出せます。上記のような出⼒であれば以下のように差分を計算すればベースアドレスが割り出せます。 (lldb) break set -a <ベースアドレス + オフセット>(lldb) p/x (0x108a03182 - 0x0000000100008182) 0x00000000089fb000気になる箇所にブレークポイントを挟む17
UI操作契機でブレーク①まずはmainターゲットのアプリの全ての関数にブレークポイントを貼る プロセスをContinueすると⾊んな箇所でブレークするので、以下のコマンドで⽌まった箇所のブレークポイントを⽚っ端から破棄する⼀通り不要なブレークポイントを破棄したら⽬的のUIコンポーネントをタップする。そのタイミングで⽌まった箇所が⽬的のブレークポイント(lldb) rbreak .* -s Hoge(lldb) cbd気になる箇所にブレークポイントを挟む18
UI操作契機でブレーク②まずは以下のブレークポイントを貼る。 -Cオプションに指定したコマンドはブレークしたとき実⾏される -G trueは⾃動的にContinueするためのオプションContinueして操作したときに、以下のようなログが取れる。上記を解釈すると、「ToolbarItemのアクションが発⽕により- [HOGEViewController backAction]が実⾏される」 ということなので、以下のブレークポイントを貼ることにより、ハンドリングできるようになる、 (ブレークポイントが作成できない場合、シンボルがない可能性があります。その場合の救済処置を後のページに記載します)(lldb) rbreak targetForAction:to:from: -C 'p/a $arg3' -C 'po $arg4' -C 'po $arg5' -G truep/a $arg3(unsigned long) $180 = 0x0000000102ff5ed0 "backAction"po $arg4po $arg5 identifier = "back" (lldb) break set -n ‘HOGEViewController backAction’気になる箇所にブレークポイントを挟む19
引数とレジスタの関係①Objective-Cからコンパイルされた任意の関数でブレークした時レジスタには以下のように値がマッピングされます。 $arg1 ( $rdi ) : 関数のレシーバ $arg2 ( $rsi ) : セレクタ名 $arg3 ( $rdx ) : 第⼀引数 $arg4 ( $rcx ) : 第⼆引数 $arg5 ( $r8 ) : 第三引数 $arg6 ( $r9 ) : 第四引数20ブレークした関数の引数の中身を確認する
引数とレジスタの関係②例えば以下の関数でブレークした場合は 以下のように引数がレジスタにマッピングされます。AppKit`-[NSApplication targetForAction:to:from:]:$arg1 ( $rdi ) : 関数のレシーバ → NSApplicationのインスタンス $arg2 ( $rsi ) : セレクタ名 → 'targetForAction:to:from:’という⽂字列 $arg3 ( $rdx ) : 第⼀引数 → targetForAction: に対する値 $arg4 ( $rcx ) : 第⼆引数 → to: に対する値 $arg5 ( $r8 ) : 第三引数 → from: に対する値 $arg6 ( $r9 ) : 第四引数 → nil21ブレークした関数の引数の中身を確認する
引数とレジスタの関係③実際レジスタには以下のように謎い64bitの数字が⼊ってます。 この数字をみても⼈間には何を表しているかわかりません。 これらの数字がインスタンスか、⽂字列か、関数のアドレスを指すかなどを調べるために、いろんなコマンドを使⽤して、その実態を調べる必要があります。 次の章から値の実態の確認をしていきます。$arg1 ( $rdi ) : 関数のレシーバ → 0x00007f92d9105e40 $arg2 ( $rsi ) : セレクタ名 → 0x00007fff7950ab9b $arg3 ( $rdx ) : 第⼀引数 → 0x00007fff24da1a1e $arg4 ( $rcx ) : 第⼆引数 → 0x00006000020d8510 $arg5 ( $r8 ) : 第三引数 → 0x0000600002703d80 $arg6 ( $r9 ) : 第四引数 → nil22ブレークした関数の引数の中身を確認する
引数が⽂字列(char) だった場合レジスタの中身が⽂字列へのポインタだった場合、以下の3つのどの⽅法でもその⽂字列を確認することができます。※ SwiftのStringはこのように中身を確認することはできません。(lldb) p (char *)0x00007fff7950ab9b(lldb) p (char *)$rdi(lldb) p (char *)$arg1ブレークした関数の引数の中身を確認する23
引数がクラスインスタンスだった場合①レジスタの中身がクラスインスタンスのポインタだった場合、以下の3つのどの⽅法でも実態を確認することができます。※ Swiftでコンパイルされた場合このように中身を確認することはできません。(lldb) po 0x0000600002703d80(lldb) po $rdi(lldb) po $arg124ブレークした関数の引数の中身を確認する
引数がクラスインスタンスだった場合②プロパティにアクセスするためには、インスタンス変数を作る必要があります。以下の2つの⽅法のどちらでもポインターからインスタンス変数を作成することができます。変数名は $2 のように⾃動的につけられて出⼒されます。出⼒された変数を使えばメンバにアクセスできます。 しかし $2.viewのようにアクセスできない場合は以下のようにメソッド形式にするとアクセスできるときがあります。(lldb) p (UIViewController *)$arg1(lldb) vinfo $arg1(lldb) po $3.view(lldb) po [$4 setHidden: 1](lldb) po [$1 view]25ブレークした関数の引数の中身を確認する
引数がクラスインスタンスだった場合③インスタンス変数のメンバや関数の⼀覧は以下のように表示することができます。関数⼀覧メンバ⼀覧 うまく表示されないときは、以下のようにモジュールをインポートするとうまくいくようになるかもしれません。(lldb) lookup NSTextField -m AppKit -l(lldb) type lookup NSTextField(lldb) po @import AppKit26ブレークした関数の引数の中身を確認する
引数がクラスインスタンスだった場合④「UI操作に対するアクションでブレーク②」でシンボルがない場合でも、レシーバとなるインスタンスのポインタとセレクタ名があれば関数を実⾏することができます。 以下のように、対象のモジュールの全ての関数にブレークポイントを貼った後に対象のメソッドを実⾏します。(-g オプションを使うことでLLDBコマンド契機で実⾏された関数に対してもブレークすることができます。)- 以下のようなシンボルのないメソッドでブレークしたら、$arg1と$arg2を確認してください。 ⽬的の関数でブレークしていることがわかります。(lldb) rbreak .* -s HogeModule(lldb) exp -g —- [$1 hogeMethod: nil] HogeModule`___lldb_unnamed_symbol470$$HogeModule:27ブレークした関数の引数の中身を確認する
引数がクラスインスタンスだった場合⑤レジスタがBlockクラスのインスタンスを指していた場合、以下のようにinvoke:に割り当てられているアドレスにブレークポイントを設置することで該当のBlockが実⾏されたときにブレークできます。(lldb) po $arg1<__NSStackBlock__: 0x70000ab7c018>signature: hogehogehumohumoinvoke : 0x7fff43be780e (DFRFoundation`___DFRHandleHandshake_block_invoke)copy : 0x7fff43be9c2c (DFRFoundation`__copy_helper_block_e8_32r40r48r)dispose : 0x7fff43be9c76 (DFRFoundation`__destroy_helper_block_e8_32r40r48r)(lldb) break set -a 0x7fff43be780e28ブレークした関数の引数の中身を確認する
引数が関数だった場合レジスタの中身が関数のアドレスだった場合、 以下の⽅法で関数かどうかがわかります。以下のようにSummaryに関数名が表示されます。(lldb) image lookup -a 0x70000ab7c018(lldb) image lookup -a 0x7fff27d93211Address: CoreFoundation[0x00007fff26c7d211]Summary: CoreFoundation`__27-[__NSSet:]_block_invoke29ブレークした関数の引数の中身を確認する
返り値を確認する以下のコマンドでフレームを抜けたあと、$raxレジスタを確認すると関数の返り値が⼊っています。 ※ Swiftでコンパイルされた場合このように中身を確認することは基本できません。(lldb) finish30ブレークした関数の返り値の中身を確認する
引数や返り値を書き換える$arg1や$raxなどのレジスタを書き換えることで引数や返り値を書き換えることができます。(lldb) po $rax = 1(lldb) po $rdi = 0x00006000014dee20処理を制御する31
アタッチされてる場合の分岐を実装以下のコードでアタッチを判別できるようです。 ローカルビルドでiPhone実機とMACで動作確認しました。 シュミレータは常にアタッチしてるものとみなされているようです。 リリースビルドではためしてません。 (アセンブリレベルで操作されたら意味ない気もする) リバースエンジニアリングの防⽌策let mib = UnsafeMutablePointer.allocate(capacity: 4)mib[0] = CTL_KERNmib[1] = KERN_PROCmib[2] = KERN_PROC_PIDmib[3] = getpid()var size: Int = MemoryLayout.sizevar info: kinfo_proc? = nilsysctl(mib, 4, &info, &size, nil, 0)if (info.unsafelyUnwrapped.kp_proc.p_flag & P_TRACED) > 0 {print(“アタッチされているのでリターンします”)return }32
• 普通にSwiftで書いたコードは、型、シンボル等の情報はほぼ残らないので実⾏中に制御できないが、Objective-Cで書かれている場合はできてしまう。• 依存ライブラリがObjective-Cで書かれている場合も注意• Swiftで書かれている場合もローカルDBや外部ファイルは閲覧ができるのでアプリにとっての機密情報は⼊れないか、最低でも暗号化するのがよさそう。結論33