本当はこわいLLDB

 本当はこわいLLDB

iOSDC2020 Day2 16:20〜
https://fortee.jp/iosdc-japan-2020/proposal/62a95cf9-1b1b-401a-8a1c-11c0490aad00

【Special Thanks】
発表資料のレビューとリハに付き合ってくれた
ddd503(@dk31486981 )

E88ad298e614c1a0929219fae46c0f01?s=128

MiyasakaKazutoshi

September 21, 2020
Tweet

Transcript

  1. 本当はこわいLLDB みやし

  2. ⾃⼰紹介 Name: みやし Twitter: @po_miyasaka Work:minne @ GMOペパボ OSSの宣伝: 


    @yhkaplan⽒とVisionAPIを使ったクレカリーダーのOSSを作成し ました。
 iOS13以降必須ですが、card.ioよりも読み取れるカードも多く、 UIもカスタマイズしやすいので、ぜひ活⽤してみてください!
 https://github.com/yhkaplan/credit-card-scanner 2
  3. 発表内容 Objective-Cでコンパイルされたアプリであれば、 LLDBで実⾏中に⾊々なハックができてしまいます。 今回の発表では、実際にアプリにアタッチしてハックしてみよ うと思います。 (ちなみに、SwiftでかかれたコードではLLDBでハックできませ んでした。) 
 3

  4. ⽬次 • 下準備 • スライドについての備考 • 拡張コマンドのインストール • SIPを解除する •

    アプリにアタッチする • アプリ全体を眺める • 依存しているダイナミックライブラリの⼀覧化 • アセンブリを書き出す • アプリ内でハードコードされた⽂字列を⼀覧化 • ローカルデータベースの中身を⾒る • アプリ内のDocumentsやCachesの中身を⾒る • 表示中のView階層を⾒る 4 • 気になる箇所にブレークポイントを挟む • 正規表現でマッチする関数でブレーク • 関数のアドレスを指定してブレーク • UI操作契機でブレーク • ブレークした関数の引数の中身を調べる • 引数とレジスタの関係 • 引数が⽂字列だった場合 • 引数がクラスインスタンスだった場合 • 引数が関数だった場合 • ブレークした関数の返り値を調べる • ブレークした関数の引数や返り値の書き換え • リバースエンジニアリングの防⽌法 • 結論
  5. スライドについての備考 • ターミナルコマンドには「$」LLDBコマンドには「(lldb)」のプ レフィックスをつけます。 • ⽂字数削減のため「メソッド」のことでも「関数」と表記して ます。 • このスライドは私個⼈の趣味の範囲で作ったものであり、所属 する組織団体とは関係ありません。

    下準備 5
  6. 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
  7. SIPを解除する MACはSIPによって守られているため、デフォルト設定では サードパーティのアプリにアタッチできません。 MACをリカバリーモードにしてターミナルで以下を叩くこ とでSIPをDisableにできます。 ⚠ SIPの解除はMACを脆弱にするのでしない⽅がいいです。
 試す場合は⾃⼰責任でお願いします!  $ csrutil

    disable && reboot 
 下準備 7
  8. 任意のMACアプリにアタッチする Xcodeで以下のようなMy Macをターゲットに持つProjectを 開きます。
 
 
 Debug > Attach to

    Process PID or Name から
 任意のアプリ名を⼊⼒してアタッチします。 下準備 8
  9. 任意のMACアプリにアタッチする② ターミナルじゃなくてXcodeでアタッチするのは、Xcodeが便利だからです。 レジスタのフォーマットは、右クリックの「Edit Summary Format」で以下のように⼊⼒す ればOKです。以下のNSStringの部分をcharに変えたりすると⽂字列が表示されるようにな ります。 下準備 9

  10. 依存しているダイナミックライブラリを⼀覧化 
 
 絶対パスで出⼒されるので、ライブラリにアクセスしやすくて便利です。 以下のコマンドを使うとライブラリがどのようにメモリに展開されているか も表示してくれます。 Static ライブラリは表示されません。  (lldb) image

    list  $ vmmap <pid> アプリ全体を眺める 10
  11. アセンブリを書き出す パスは image list で出⼒されたパスをそのまま指定できま す。 アセンブリを眺めると、関数名とか、⽂字列が表示されてお り、処理を把握するヒントになる。
 (Swiftでビルドされている場合はそのようなヒントは残らな い)

    $ otool -tvV /Application/Hoge/Contents/MacOS/Hoge アプリ全体を眺める 11
  12. アプリ内でハードコードされた⽂字列を⼀覧化 ソースコードにハードコードされた⽂字列(ascii)が表示され ます。IBに紐付いた変数名やプロトコル名なども表示される ようです。 $ strings -t x /Application/Hoge/Contents/MacOS/Hoge アプリ全体を眺める

    12
  13. ローカルデータベースの中身を⾒る アプリから参照している外部ファイルのパスが⼀覧化されま す。その中に.sqliteや.realmがある場合は、ターミナルの sqliteコマンドやRealm Browserなどを使⽤することでデー タベースの中身を確認することができます。 (lldb) lsof アプリ全体を眺める 13

  14. アプリ内のDocumentsやCachesの中身を⾒る 気になったファイルは以下のコマンドによりFinderに表示するこ とができます。 (lldb) ls ./Library/Caches (lldb) yoink ./Library/Caches/hoge.csv アプリ全体を眺める

    14
  15. 表示中のView階層を⾒る Catalyst等でUIKitを使っていればVC階層も表示できる
 出⼒されたアドレスを使ってViewのisHiddenを切り替えることができます。 viewの特定に役⽴ちます。 
 (lldb) pviews 
 (lldb) pvc

    
 (lldb) tv 0x00006000020d8510 アプリ全体を眺める 15
  16. 正規表現にマッチする関数でブレーク -s でモジュールを限定できる。 以下コマンドはUIKitに含まれるすべての関数でブレークする。 ※ シンボルがない場合は全てのシンボルが___lldb_unnamed_symbol5013$$Hogeのように 命名されるので⽂字列指定はできない。Objective-Cであれば、後述のレジスタ内の情報か ら関数を判定できるが、Swiftでコンパイルされている場合はメソッド名を判別することは 不可能 (lldb)

    rbreak '\-\[UIViewController\ ' -s UIKit (lldb) rbreak .* -s UIKit 気になる箇所にブレークポイントを挟む 16
  17. 関数のアドレスを指定してブレーク 前述で出⼒したアセンブリの任意の箇所でブレークしたい場合は、
 記載されているオフセットにベースアドレスを⾜したものを指定する必要があります。 ベースアドレスは実⾏中のアプリのアセンブリ(図左)と 出⼒したアセンブリ(図右)を⾒⽐べて、
 同じに⾒える処理のアドレスの差分を計算すれば割り出せます。 上記のような出⼒であれば以下のように差分を計算すればベースアドレスが割り出せます。 
 (lldb) break

    set -a <ベースアドレス + オフセット> (lldb) p/x (0x108a03182 - 0x0000000100008182)   0x00000000089fb000 気になる箇所にブレークポイントを挟む 17
  18. UI操作契機でブレーク① まずはmainターゲットのアプリの全ての関数にブレークポイントを貼る
 プロセスをContinueすると⾊んな箇所でブレークするので、以下のコマ ンドで⽌まった箇所のブレークポイントを⽚っ端から破棄する ⼀通り不要なブレークポイントを破棄したら⽬的のUIコンポーネントを タップする。そのタイミングで⽌まった箇所が⽬的のブレークポイント (lldb) rbreak .* -s

    Hoge (lldb) cbd 気になる箇所にブレークポイントを挟む 18
  19. 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 true p/a $arg3 (unsigned long) $180 = 0x0000000102ff5ed0 "backAction" po $arg4 <HOGEViewController: 0x6000008e6490> po $arg5 <ToolbarItem: 0x60000312cbb0> identifier = "back" 
 (lldb) break set -n ‘HOGEViewController backAction’ 気になる箇所にブレークポイントを挟む 19
  20. 引数とレジスタの関係① Objective-Cからコンパイルされた任意の関数でブレークし た時レジスタには以下のように値がマッピングされます。
 
 $arg1 ( $rdi ) : 関数のレシーバ

    
 $arg2 ( $rsi ) : セレクタ名 
 $arg3 ( $rdx ) : 第⼀引数 
 $arg4 ( $rcx ) : 第⼆引数 
 $arg5 ( $r8 ) : 第三引数 
 $arg6 ( $r9 ) : 第四引数 20 ブレークした関数の引数の中身を確認する
  21. 引数とレジスタの関係② 例えば以下の関数でブレークした場合は
 以下のように引数がレジスタにマッピングされます。 AppKit`-[NSApplication targetForAction:to:from:]: $arg1 ( $rdi ) :

    関数のレシーバ → NSApplicationのインスタンス 
 $arg2 ( $rsi ) : セレクタ名   → 'targetForAction:to:from:’という⽂字列 
 $arg3 ( $rdx ) : 第⼀引数    → targetForAction: に対する値 
 $arg4 ( $rcx ) : 第⼆引数    → to: に対する値 
 $arg5 ( $r8 ) : 第三引数    → from: に対する値 
 $arg6 ( $r9 ) : 第四引数    → nil 21 ブレークした関数の引数の中身を確認する
  22. 引数とレジスタの関係③ 実際レジスタには以下のように謎い64bitの数字が⼊ってます。
 この数字をみても⼈間には何を表しているかわかりません。
 これらの数字がインスタンスか、⽂字列か、関数のアドレスを指すかなどを調べ るために、いろんなコマンドを使⽤して、その実態を調べる必要があります。
 次の章から値の実態の確認をしていきます。 $arg1 ( $rdi )

    : 関数のレシーバ → 0x00007f92d9105e40 
 $arg2 ( $rsi ) : セレクタ名   → 0x00007fff7950ab9b 
 $arg3 ( $rdx ) : 第⼀引数    → 0x00007fff24da1a1e 
 $arg4 ( $rcx ) : 第⼆引数    → 0x00006000020d8510 
 $arg5 ( $r8 ) : 第三引数    → 0x0000600002703d80 
 $arg6 ( $r9 ) : 第四引数    → nil 22 ブレークした関数の引数の中身を確認する
  23. 引数が⽂字列(char) だった場合 レジスタの中身が⽂字列へのポインタだった場合、以下の3つの どの⽅法でもその⽂字列を確認することができます。 ※ SwiftのStringはこのように中身を確認することはできません。 (lldb) p (char *)0x00007fff7950ab9b

    (lldb) p (char *)$rdi (lldb) p (char *)$arg1 ブレークした関数の引数の中身を確認する 23
  24. 引数がクラスインスタンスだった場合① レジスタの中身がクラスインスタンスのポインタだった場合、 以下の3つのどの⽅法でも実態を確認することができます。 ※ Swiftでコンパイルされた場合このように中身を確認することはで きません。 (lldb) po 0x0000600002703d80 (lldb)

    po $rdi (lldb) po $arg1 24 ブレークした関数の引数の中身を確認する
  25. 引数がクラスインスタンスだった場合② プロパティにアクセスするためには、インスタンス変数を作る必要があります。以下の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 ブレークした関数の引数の中身を確認する
  26. 引数がクラスインスタンスだった場合③ インスタンス変数のメンバや関数の⼀覧は以下のように表示することができます。 関数⼀覧 メンバ⼀覧
 うまく表示されないときは、以下のようにモジュールをインポートするとうまく いくようになるかもしれません。 (lldb) lookup NSTextField -m

    AppKit -l (lldb) type lookup NSTextField (lldb) po @import AppKit 26 ブレークした関数の引数の中身を確認する
  27. 引数がクラスインスタンスだった場合④ 「UI操作に対するアクションでブレーク②」でシンボルがない場合でも、レシーバとなるインスタンスのポインタ とセレクタ名があれば関数を実⾏することができます。 
 
 以下のように、対象のモジュールの全ての関数にブレークポイントを貼った後に 対象のメソッドを実⾏します。(-g オプションを使うことでLLDBコマンド契機で実⾏された関数に対してもブレー クすることができます。) -

    
 以下のようなシンボルのないメソッドでブレークしたら、$arg1と$arg2を確認してください。
 ⽬的の関数でブレークしていることがわかります。 (lldb) rbreak .* -s HogeModule (lldb) exp -g —- [$1 hogeMethod: nil]  HogeModule`___lldb_unnamed_symbol470$$HogeModule: 27 ブレークした関数の引数の中身を確認する
  28. 引数がクラスインスタンスだった場合⑤ レジスタがBlockクラスのインスタンスを指していた場合、以下のよう にinvoke:に割り当てられているアドレスにブレークポイントを設置す ることで該当のBlockが実⾏されたときにブレークできます。 (lldb) po $arg1 <__NSStackBlock__: 0x70000ab7c018> signature: hogehogehumohumo

    invoke : 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 0x7fff43be780e 28 ブレークした関数の引数の中身を確認する
  29. 引数が関数だった場合 レジスタの中身が関数のアドレスだった場合、
 以下の⽅法で関数かどうかがわかります。 以下のようにSummaryに関数名が表示されます。 (lldb) image lookup -a 0x70000ab7c018 (lldb)

    image lookup -a 0x7fff27d93211 Address: CoreFoundation[0x00007fff26c7d211] Summary: CoreFoundation`__27-[__NSSet:]_block_invoke 29 ブレークした関数の引数の中身を確認する
  30. 返り値を確認する 以下のコマンドでフレームを抜けたあと、$raxレジスタを確 認すると関数の返り値が⼊っています。 
 ※ Swiftでコンパイルされた場合このように中身を確認することは基 本できません。 (lldb) finish 30

    ブレークした関数の返り値の中身を確認する
  31. 引数や返り値を書き換える $arg1や$raxなどのレジスタを書き換えることで引数や返り 値を書き換えることができます。 (lldb) po $rax = 1 (lldb) po

    $rdi = 0x00006000014dee20 処理を制御する 31
  32. アタッチされてる場合の分岐を実装 以下のコードでアタッチを判別できるようです。
 ローカルビルドでiPhone実機とMACで動作確認しました。
 シュミレータは常にアタッチしてるものとみなされているようです。
 リリースビルドではためしてません。
 (アセンブリレベルで操作されたら意味ない気もする) 
 リバースエンジニアリングの防⽌策 let mib

    = UnsafeMutablePointer<Int32>.allocate(capacity: 4) mib[0] = CTL_KERN mib[1] = KERN_PROC mib[2] = KERN_PROC_PID mib[3] = getpid() var size: Int = MemoryLayout<kinfo_proc>.size var info: kinfo_proc? = nil sysctl(mib, 4, &info, &size, nil, 0) if (info.unsafelyUnwrapped.kp_proc.p_flag & P_TRACED) > 0 { print(“アタッチされているのでリターンします”) return  } 32
  33. • 普通にSwiftで書いたコードは、型、シンボル等の情報はほ ぼ残らないので実⾏中に制御できないが、Objective-Cで書 かれている場合はできてしまう。 • 依存ライブラリがObjective-Cで書かれている場合も注意 • Swiftで書かれている場合もローカルDBや外部ファイルは 閲覧ができるのでアプリにとっての機密情報は⼊れない か、最低でも暗号化するのがよさそう。

    結論 33