どうも、でぃーぴーぶの魚脳です。今回はiOSアプリの解析時に使用するツールclass-dumpを利用した時遭遇したエラー、その原因及び解決策をソースコードを合わせて解説できたらと思います。
- class-dumpの現状と問題点
- chained fixupsを導入するまで
- class-dumpのソースコード分析(本家)
- chained fixups導入後
- chained fixups 101
- classdumpcのソースコード分析(lechium氏バージョンclass-dump)
- 終わり
class-dumpの現状と問題点
class-dumpとは
Steve Nygard氏によるMach-O ファイルに格納された Objective-C ランタイム情報を調べるためのコマンドラインユーティリティです。クラス、カテゴリ、プロトコルの宣言を生成します。
「owasp-mastg-ja」の説明にあるようにclass-dumpはMach-Oファイルを解析する静的ツールとなります。診断中は「必須」とは言えませんが、使ってなにかヒントを得られるかもしれないといった「使い得」な立ち位置となります。
クラスやカテゴリなどのプログラムのメタデータを出す機能自体はotool -ov
とさほど変わりませんが、しっかりヘッダーファイル形式で表示してくれるため、かなり見やすいです。
しかしながら、class-dumpが新し目のアプリ(Mach-O実行ファイル)に対応できなくなっています。筆者が実行時に主に以下の種類のエラーを観測しました。
Cannot find offset for address xxxx in xxxx
Unknown load command: 0x00000032
Unknown load command: 0x80000033
Unknown load command: 0x80000034
Unknown load command: 0x00000032
エラー文をググってみたら0x32はどうやらロードコマンドLC_BUILD_VERSIONを指すようです
#ifndef LC_BUILD_VERSION #define LC_BUILD_VERSION 0x32 /* build for platform min OS version */ /* * The build_version_command contains the min OS version on which this * binary was built to run for its platform. The list of known platforms and * tool values following it. */
LC_BUILD_VERSIONに関する処理は現在class-dumpのmainブランチに存在しますが、配布中の最新バージョンVer.3.5には存在しないせいで、エラーを起こしたと思います。対策としてはソースコードから自分でコンパイルすれば大丈夫です。
case LC_SOURCE_VERSION: targetClass = [CDLCSourceVersion class]; break; case LC_DYLIB_CODE_SIGN_DRS: targetClass = [CDLCLinkeditData class]; break; // Designated Requirements case LC_BUILD_VERSION: targetClass = [CDLCBuildVersion class]; break;
ちなみに少なくともmacOS SDK 11.0以降からLC_BUILD_VERSION
が追加された模様です。*1
Unknown load command: 0x80000033
/Unknown load command: 0x80000034
こっちもググってみたら、0x80000033
と0x80000034
はそれぞれロードコマンドLC_DYLD_EXPORTS_TRIE
とLC_DYLD_CHAINED_FIXUPS
を指すことが判明しました。
chained fixupsとは、プログラムが実行される際に動的リンクされた実行可能ファイルや共有ライブラリ(.dylib,Framework)内に存在する、アドレスの修正を行うための新しい仕組です。アップルのカンファレンスWWDC21で登場したXcode13のリリースノートでこの新機能について触れられたようです。
All programs and
dylibs
built with a deployment target of macOS 12 or iOS 15 or later now use the chained fixups format. This uses different load commands and LINKEDIT data, and won’t run or load on older OS versions. (49851380)*2
あまりに大々的に出されてないせいなのか、未だにchained fixupsに関する情報は少なく、既存ツールの改修が遅れたと推測します。
本家のclass-dumpのmainブランチもまさにそのうちの一つで、未だにこちらのロードコマンドに対応していないせいで、エラーを起こしたと思います。この2つのエラーの対策に関してはこのあとchained fixupsなどの紹介をあわせて説明します。
Cannot find offset for address xxxx in dataOffsetForAddress
こちらのエラーも結局chained fixupsに関する変更点が起因したものとなります。簡潔に説明するとMach-Oファイル内のクラスや関数などに関する情報の位置を指すアドレスの変換方法が変更されたため、新しめのMach-Oファイルの時はこのようなエラーが発生します。
これらのエラーの大半はchained fixupsに起因したものであるため、先にchained fixupsについて知る必要があると思いました。
chained fixupsを導入するまで
さて、chained fixupsを紹介する前に従来の仕組みを軽く紹介します。
Mach-Oでは内部向けのポインターを実行時メモリ上の仮想メモリアドレスに変換することをrebaseと言います。逆に動的ライブラリなどの外部に指すポインターを仮想メモリアドレスに変換することをbindと言います、厳密な意味では異なりますが、ELFと似た仕組みだと思います。
両方どっちもMach-Oファイルにdyld用のオペコードを仕込んで、あとdyldの手を借りて処理を行います。rebaseに関してはオペコードから*3
- セグメントの番号
- セグメント内のオフセット
- 操作タイプ
の3つの情報を得ることで変更すべき箇所を特定してから、(ASLRが生み出したスライド分のアドレスをたした)仮想メモリアドレスに書き換えます。
しかし、従来の仕組みを適用したMach-Oファイルの変更すべき箇所に格納されるのは全部デフォルトの仮想メモリアドレス(オフセット0x100000000固定)であり、メタデータの抽出ぐらいなら十分な情報です。これがあったからなのか本家のclass-dumpではrebaseに関する処理が実装されたものの、実行する箇所が最終的にコメントアウトされています。
一方bindですが、Mach-Oの動的リンク方式はlazy binding、non-lazy binding、weak bindingの3種類存在しますが、ここではlazy bindとnon-lazy bindだけ軽く説明します
lazy binding
関数類を呼ぶ機会が多いため最初から一気にbindするには時間かかります、それを解消するためlazy binding はシンボル名の検索とアドレスの解決を初回呼び出し時まで遅延させることでプログラムの起動を早めます。*4 lazy binding手順は↓の画像が示すようになります
- 1回目の呼び出し時のアドレスは実は
__stubs
の項目を指します(赤線1) __stubs
の項目内に保存されたオペコードで__la_symbol_ptr
まで飛ぶ、ここはかなりELFのPLTと似ています(赤線2)__la_symbol_ptr
に保存されたアドレスは__stub_helper
を指します(赤線3)__stub_helper
内の処理を経て最終的に__DATA,_CONST,__got
のdyld_stub_binder
まで飛ぶ(赤線4,5)dyld_stub_binder
は動的ライブラリ内のシンボルを見つけ、それを__la_symbol_ptr
に書き戻し、実際のメソッドにジャンプします- 2回目以降の呼び出しはそのまま
__la_symbol_ptr
から実際のメソッドにジャンプします
non-lazy binding
主に文字列などのデータ類に適用します、起動時dyld
がアドレスを探してくれて、最終的に__DATA_CONST,__got
/__DATA, __nl_symbol_ptr
に本当のアドレスを書き換える(緑線1)
class-dumpのソースコード分析(本家)
本家のclass-dumpではプログラムの最初からMach-Oファイルを読み込みロードコマンドdyld_info
/dyld_info_only
に含まれたBinding Info Offset
が指す箇所に格納されるオペコードをパースします。
// - (void)machOFileDidReadLoadCommands:(CDMachOFile *)machOFile; { //[self logRebaseInfo]; [self parseBindInfo]; [self parseWeakBindInfo]; //[self logLazyBindInfo]; //[self logExportedSymbols]; //NSLog(@"symbolNamesByAddress: %@", symbolNamesByAddress); } - (void)parseBindInfo; { if (debugBindOps) { NSLog(@"----------------------------------------------------------------------"); NSLog(@"bind_off: %u, bind_size: %u", _dyldInfoCommand.bind_off, _dyldInfoCommand.bind_size); } const uint8_t *start = (uint8_t *)[self.machOFile.data bytes] + _dyldInfoCommand.bind_off; const uint8_t *end = start + _dyldInfoCommand.bind_size; [self logBindOps:start end:end isLazy:NO]; }
実際関数logBindOps
が処理を担当します、switch…case構造でオペコードごとに対応しています、ここでは2つだけ抜粋して解説します。
- (void)logBindOps:(const uint8_t *)start end:(const uint8_t *)end isLazy:(BOOL)isLazy; { //... switch (opcode) { //... case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM://0x40 symbolName = (const char *)ptr; symbolFlags = immediate; if (debugBindOps) NSLog(@"BIND_OPCODE: SET_SYMBOL_TRAILING_FLAGS_IMM, flags: %02x, str = %s", symbolFlags, symbolName); while (*ptr != 0) ptr++; ptr++; // skip the trailing zero break; //... case BIND_OPCODE_DO_BIND://0x90 if (debugBindOps) NSLog(@"BIND_OPCODE: DO_BIND"); [self bindAddress:address type:type symbolName:symbolName flags:symbolFlags addend:addend libraryOrdinal:libraryOrdinal]; address += _ptrSize; bindCount++; break;
BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM
はシンボル名を付与するオペコードです、このオペコード(\x40
)のあとにシンボル名の文字列がついて来ます。文字列の最後に\x00
入っており、そこまでループで1バイトずつ読み込む。
BIND_OPCODE_DO_BIND
はBIND処理終了時に用いられるオペコードであり、シンボルの文字列の取得やアドレスにオフセットを加えるなどの操作が終わったあとに、アドレスを__la_symbol_ptr
に書き込むはずが、class-dumpはそこまでする必要がないので、単純に記録して使われるときに備えれば十分です。ここら辺の具体的な処理はbindAddress
関数に入っています。
//... _symbolNamesByAddress = [[NSMutableDictionary alloc] init]; //... - (void)bindAddress:(uint64_t)address type:(uint8_t)type symbolName:(const char *)symbolName flags:(uint8_t)flags addend:(int64_t)addend libraryOrdinal:(int64_t)libraryOrdinal; { #if 0 NSLog(@" Bind address: %016lx, type: 0x%02x, flags: %02x, addend: %016lx, libraryOrdinal: %ld, symbolName: %s", address, type, flags, addend, libraryOrdinal, symbolName); #endif NSNumber *key = [NSNumber numberWithUnsignedInteger:address]; // I don't think 32-bit will dump 64-bit stuff. NSString *str = [[NSString alloc] initWithUTF8String:symbolName]; _symbolNamesByAddress[key] = str; }
bindAddress
関数は引数であるシンボル名の文字列とアドレスを辞書_symbolNamesByAddress
に入れます
- (NSString *)symbolNameForAddress:(NSUInteger)address; { return [_symbolNamesByAddress objectForKey:[NSNumber numberWithUnsignedInteger:address]]; }
取り出す際にはアドレスを関数symbolNameForAddress
に渡せば辞書から引いてくれて、シンボル名の文字列を返してくれる。実際クラスのメタデータを取得する関数loadClasses
とカテゴリ(実質クラスみたいなもの)のメタデータを取得する関数loadCategories
はこの関数を使ってシンボル名の文字列を取得して最終の出力内容を構築していきます。
chained fixups導入後
従来の動的リンク方式には2つデメリットが存在します。
- 前述の通り、従来の動的リンク方式はどうしても特殊なオペコードに依存します、これらのオペコードは少なくない空間を占めた上で、動的リンク後無用なバイト列に化して、かなりもったいないです
- rebase、bindそれぞれの段階で情報を読み込むために実行ファイルの同じページを繰り返してアクセスする可能性が大いにあり、効率的にはよくありません。
chained fixupsはこの2つのデメリットを解消するような仕組みになります。まずオペコードを撤廃して動的リンク関連の情報をすべて(近似的な)連結リストに移します。これにより、冗長なオペコードなしで次々と情報を読み込むことができ、さらに実行ファイルのサイズが多少抑えられますし、アプリ起動時間もそれに応じて短縮されるとのことだそうです。
おまけにchained fixupsの導入でlazy binding形式が廃止となりすべてがnon-lazy bindingとなったため、従来のclass-dumpがエラーを吐くようになりました。
chained fixupsについて少し知ったうえで、もう一回本家のclass-dumpを振り返って見ると、そもそも本家githubの更新が5年前から止まっていて、配布中の最新バージョンVer3.5も10年前のものでしたので、3年前に登場した仕組みに対応できないのは仕方ありません。
また、実はclass-dumpから派生したツールが数少ないものの存在します、その一部を試した結果と所感は以下のようになります。(基本本家のclass-dumpをベースにしたツールを対象にしています)
- MonkeyDev/class-dump
- 本家Ver3.5をベース
- swiftとObj-Cの難読化にもある程度対応したらしい
- chained fixupsに対応してない
- HeiTanBc/class-dump
- 本家Ver3.5をベース
- ロードコマンドの解析に対応してないものの、chained fixups機能に対応済み
- lechium氏版と比べると少しい精度?が足りない→実装が簡易的なため?
- lechium/classdumpios
- ロードコマンド対応
- iOS13-16 SDKに対応と明言→chained fixupsに対応済み
- etc…
派生ツールが存在するものの、今のMach-Oファイルに対してエラーを吐かずに実行できるバージョンは一握りという現状に陥っています。
chained fixups 101
改良版ツールがどういった対応をしたのかを確認する前にchained fixupsが具体的にどういう感じの機構なのか、この小節で例を交えて紹介します。
まずchained fixups対応後のMach-Oファイルは以下のような構造になっています。
主な変更点としては
- ロードコマンドから
LC_DYLD_INFO
が消えLC_DYLD_CHAINED_FIXUPS
が追加されます。これがエラーを引き起こす一つの理由 __DATA(_CONST)
セグメントにrebaseとbindに関連する情報が含まれる連結リスト風な構造体がズラリ。画像の中ではrebaseとbaseが分かれて存在するような形になっていますが、実際は両種ごちゃまぜでセグメントに配置される形になります。__LINKEDIT
セグメントに従来のオペコードの代わりに動的ライブラリに関する情報が含まれるchained fixups関連の構造体が多数存在します。(後ほどいくつをピックアップして紹介します)
新しく追加されたデータ間の関係を示す画像が以下のようになります。
重要な構造体は↓の3つ:
- dyld_chained_import
- CHAIEND_POINTER_64_REBASE
- CHAIEND_POINTER_64_BIND
まずはdyld_chained_import
、この構造体は__LINKEDIT
のchained_starts_in_segment
の後ろに格納され、dyld_chained_fixups_header
のimports_offset
から位置を特定できます。
// DYLD_CHAINED_IMPORT struct dyld_chained_import { uint32_t lib_ordinal : 8, weak_import : 1, name_offset : 23; };
構造体の中にlib_ordinal
はインデックスとなり、Mach-O中「〇〇個目」LC_LOAD_DYLIB
と関連することを示します(なぜか1から始まるが、dyldが処理中毎回-1処理を挟んでいます)。name_offset
は文字通りシンボル名を探す時のオフセットになります、dyld_chained_import
のあとに「Symbol Name Pool」といったシンボル名が固まって格納される箇所があり、そこの「✕✕番目」バイトから\x00
まではシンボル名の文字列になります。
次はCHAIEND_POINTER_64_REBASE
とCHAIEND_POINTER_64_BIND
なんですが、8バイトの構造体で内外部向けポインターのところに配置されます、いわば「仮」のアドレスといった役割になっていて、後々変換後のアドレスに書き換えられます。
そして肝心な構造体ですがそれぞれこんな感じになります
// DYLD_CHAINED_PTR_64/DYLD_CHAINED_PTR_64_OFFSET struct dyld_chained_ptr_64_rebase { uint64_t target : 36, // 64GB max image size (DYLD_CHAINED_PTR_64 => vmAddr, DYLD_CHAINED_PTR_64_OFFSET => runtimeOffset) high8 : 8, // top 8 bits set to this (DYLD_CHAINED_PTR_64 => after slide added, DYLD_CHAINED_PTR_64_OFFSET => before slide added) reserved : 7, // all zeros next : 12, // 4-byte stride bind : 1; // == 0 }; // DYLD_CHAINED_PTR_64 struct dyld_chained_ptr_64_bind { uint64_t ordinal : 24, addend : 8, // 0 thru 255 reserved : 19, // all zeros next : 12, // 4-byte stride bind : 1; // == 1 };
rebase構造体中のtargetはMach-Oファイルにおけるアドレスのオフセットを示します、実行時にあらかじめメモリ中に展開したプログラムのアドレスに加算してrebase構造体所在のアドレスへ書き換えます。
bind構造体中のordinal
はdyld_chained_import
の配列のインデックスと一致する、dyldがそれを利用して動的ライブラリのアドレスを探してくれて、その結果をbind構造体所在のアドレスへ書き換えます。
両方の構造体に共通して存在するnextは次のrebase/bind構造体までの距離(next*4byte)になります、これで次々と先にある構造体まで飛べて、rebase/bindの作業を行うことができます、これが連結リスト的な構造と言われている故と思われます。
ここからではテストアプリ交えてchained fixups変更点の確認をします
仮に↓のような関数を含むプログラムがあるとします
@implementation NSString (Addition) - (void)addMethod; { NSLog(@"addMethod fired"); } @end
そしてその実行時のアセンブリ↓のようなります
0x10e311df0 <+16>: leaq 0x22c9(%rip), %rdi ; @"addMethod fired" 0x10e311df7 <+23>: movb $0x0, %al 0x10e311df9 <+25>: callq 0x10e31237e ; symbol stub for: NSLog 0x10e311dfe <+30>: addq $0x10, %rsp 0x10e311e02 <+34>: popq %rbp 0x10e311e03 <+35>: retq
Mach-Oファイルのベースアドレスを引くことで文字列addMethod fired
のファイル内オフセットが0x40c0(0x22c9+0x10e311df7-0x10e310000)
であることがわかります。
NSString型の文字列は__DATA_CONST __cfstring
セクションの32バイト分の容量を占めます、文字列自体のポインターは0x40d0
からの8バイトに格納されるため、この8バイトはrebase構造体とみなされます。
0x40d0
に格納された8バイトはrebasing時にrebase構造体として扱われます
オフセットが指す先には文字列addMethod fired
が確認できます
そしてrebasingを終えて、メモリ上のアドレスが本来の位置に書き換えられたことが確認できます。
(lldb) x/4xg $rdi 0x10e3140c0: 0x00007ff863b7aba8 0x00000000000007c8 0x10e3140d0: 0x000000010e3123e5 0x000000000000000f (lldb) x/2xg 0x000000010e3123e5 0x10e3123e5: 0x6f6874654d646461 0x0064657269662064 → addMethod fired
次に関数NLogを見てみます、アドレスはアセンブラ上0x10e31237e
になり、Mach-Oファイルのベースアドレスを引くことでオフセットは0x237e(0x10e31237e - 0x10e310000)
であることがわかります。
$ otool -v objective-test -s __TEXT __stubs objective-test: Contents of (__TEXT,__stubs) section 000000010000237e jmpq *0x1c7c(%rip) 0000000100002384 jmpq *0x1c7e(%rip)
0x237e
にはアセンブリがありそのまま0x4000(0x237e+0x6+0x1c7c)
まで飛びます、そこは__DATA __got
セクションになるため、bind対象となります。(前述したようにchained fixupsが導入した以降全部non-lazy bindになるため)
bind構造体に当てはまると、ordinal
が0になることがわかります
実際otoolを使って、0番目のdyld_chained_import
が関連するのはFoundation
の_NSLog
であることがわかります、dyldがこの情報を使ってそのうち該当する動的ライブラリ関数のアドレスを返してくれます。
$ otool -chained_fixups objective-test dyld chained import[0] = 0x00000201 lib_ordinal = 1 (Foundation) weak_import = 0 name_offset = 1 (_NSLog)
最終的に0x4000
からのbind構造体は動的ライブラリ関数のアドレスに書き換えられます
(lldb) x/4xg 0x000000010e314000 0x10e314000: 0x00007ff800c2d1f9 0x00007ff800c16b92 0x10e314010: 0x00000001159c4974 0x00007ff80004cd9c (lldb) disassemble -s 0x00007ff800c2d1f9 Foundation`NSLog: 0x7ff800c2d1f9 <+0>: pushq %rbp 0x7ff800c2d1fa <+1>: movq %rsp, %rbp 0x7ff800c2d1fd <+4>: subq $0xd0, %rsp 0x7ff800c2d204 <+11>: leaq -0xd0(%rbp), %r10 0x7ff800c2d20b <+18>: movq %rsi, 0x8(%r10) 0x7ff800c2d20f <+22>: movq %rdx, 0x10(%r10) 0x7ff800c2d213 <+26>: movq %rcx, 0x18(%r10)
classdumpcのソースコード分析(lechium氏バージョンclass-dump)
本家バージョンでは結局シンボル名の文字列とアドレスを辞書_symbolNamesByAddress
に入れて、のちのちclasslistなどのところでアドレスが出現する度辞書から引けばメタデータが手に入れられます。lechium氏バージョンも、辞書_symbolNamesByAddress
から必要に応じてシンボル名の文字列を取り出すことが本家から変わらず、変わったのは「アドレスとシンボル名の文字列のペア」を探す手順だけです。
上の関係図からCHAINED POINTER 64 BIND
/REBASE
までたどり着けるようにするにはロードコマンドからdyld_chianed_fixups_header
→dyld_chained_starts_in_image
→dyld_chained_starts_in_segment
の順番でジャンプする必要があります、lechium氏バージョンのclassdumpcはまさにこの順番でした(ソースコードの抜粋となります)、最終的にCHAINED POINTER 64 BIND
/REBASE
を解析する関数はprocessFixupsInPage
になるのがわかります。
// lechiumバージョンclassdumpcから抜粋 - (void)machOFileDidReadLoadCommands:(CDMachOFile *)machOFile; { // fixup-chainの部分は_linkeditセグメントの最初から始まる uint8_t *fixup_base = (uint8_t *)[[self linkeditData] bytes]; //dyld_chianed_fixups_header struct dyld_chained_fixups_header *header = (struct dyld_chained_fixups_header *)fixup_base; printChainedFixupsHeader(header); [self printImports:header]; //dyld_chained_starts_in_image struct dyld_chained_starts_in_image *starts_in_image = (struct dyld_chained_starts_in_image *)(fixup_base + header->starts_offset); uint32_t *offsets = starts_in_image->seg_info_offset; for (int i = 0; i < starts_in_image->seg_count; ++i) { //... //dyld_chained_starts_in_segment struct dyld_chained_starts_in_segment* startsInSegment = (struct dyld_chained_starts_in_segment*)(fixup_base + header->starts_offset + offsets[i]); //... int pageCount = 0; for (int j = 0; j < MIN(startsInSegment->page_count, maxPageNum); ++j) { if ([CDClassDump printFixupData]){ fprintf(stderr," PAGE %d (offset: %d)\n", j, page_starts[j]); } if (page_starts[j] == DYLD_CHAINED_PTR_START_NONE) { continue; } // fixup chainの連結リスト部分を走査する [self processFixupsInPage:(uint8_t *)[self.machOFile bytes] fixupBase:fixup_base header:header startsIn:startsInSegment page:j]; pageCount++; if ([CDClassDump printFixupData]){ fprintf(stderr,"\n"); } }
前述したようにchained fixupsの導入でrebase/bindされるべき箇所には仮のアドレスいわば構造体が配置されるため、従来のようにbindだけ処理するのでは事足りません、よって関数processFixupsInPage
内ではbindとrebase両方分けて対処しています
- (void)processFixupsInPage:(uint8_t *)base fixupBase:(uint8_t*)fixupBase header:(struct dyld_chained_fixups_header *)header startsIn:(struct dyld_chained_starts_in_segment *)segment page:(int)pageIndex { uint32_t chain = (uint32_t)segment->segment_offset + segment->page_size * pageIndex + segment->page_start[pageIndex]; bool done = false; int count = 0; while (!done) { if (segment->pointer_format == DYLD_CHAINED_PTR_64 || segment->pointer_format == DYLD_CHAINED_PTR_64_OFFSET) { struct dyld_chained_ptr_64_bind bind = *(struct dyld_chained_ptr_64_bind *)(base + chain); if (bind.bind) { //we are binding a symbol struct dyld_chained_import import = ((struct dyld_chained_import *)(fixupBase + header->imports_offset))[bind.ordinal]; char *symbol = (char *)(fixupBase + header->symbols_offset + import.name_offset); uint64_t peeked = [self.machOFile peekPtrAtOffset:chain ptrSize:_ptrSize]; uint64_t raw = _OSSwapInt64(peeked); //honestly not sure why byte swapping is 'necessary' here, but it works. if ([CDClassDump printFixupData]){ NSString *lib = _imports[[NSString stringWithUTF8String:symbol]]; fprintf(stderr," 0x%08x RAW: %#010llx BIND ordinal: %d addend: %d dylib: %s (%s)\n", chain, raw, bind.ordinal, bind.addend, [lib UTF8String], symbol); } [self bindAddress:raw symbolName:symbol]; } //... - (void)bindAddress:(uint64_t)address symbolName:(const char *)symbolName { // put in dictionary address:symbolname NSNumber *key = [NSNumber numberWithUnsignedInteger:address]; // I don't think 32-bit will dump 64-bit stuff. NSString *str = [[NSString alloc] initWithUTF8String:symbolName]; _symbolNamesByAddress[key] = str; }
案の定bind構造体のordinal
利用して該当のdyld_chained_import
を探して、そのname_offset
からシンボル名の文字列を入手しています。最終的にbind構造体とシンボル名の文字列を辞書_symbolNamesByAddress
に入れてclasslistなどのメタデータ解析に備えます。
//... else { // rebase 0x%08lx struct dyld_chained_ptr_64_rebase rebase = *(struct dyld_chained_ptr_64_rebase *)&bind; uint64_t raw = [self.machOFile peekPtrAtOffset:chain ptrSize:_ptrSize]; uint64_t unpackedTarget = (((uint64_t)rebase.high8) << 56) | (uint64_t)(rebase.target); // The DYLD_CHAINED_PTR_64 target is vmaddr, but // DYLD_CHAINED_PTR_64_OFFSET target is vmoffset. Need to add preferredLoadAddress to find it! -- major missing piece to getting this working. if (segment->pointer_format == DYLD_CHAINED_PTR_64_OFFSET) { unpackedTarget += self.machOFile.preferredLoadAddress; //ODLog(@"unpackedTarget adjusted", unpackedTarget); } if ([CDClassDump printFixupData]){ fprintf(stderr," %#010x RAW: 0llx REBASE target: %#010llx high8: %#010x\n", chain, raw, unpackedTarget, rebase.high8); } [self rebaseAddress:raw target:unpackedTarget]; } - (void)rebaseAddress:(uint64_t)address target:(uint64_t)target { NSLog(@"rebasing"); NSNumber *key = [NSNumber numberWithUnsignedInteger:address]; // I don't think 32-bit will dump 64-bit stuff. // numberWithUnsignedInteger just change unsigned integer to NSNumber NSNumber *val = [NSNumber numberWithUnsignedInteger:target]; // _based is a dictionary for based address _base[address] = target _based[key] = val; }
rebaseに関してはオフセットがそもそも構造体に含まれてるため、それを仮想メモリアドレスに変換して、キーは構造体、値は仮想メモリアドレスの辞書_based
に格納してclasslistなどのメタデータ解析に備えます。
classdumpc(lechiumバージョン)fixup chainへの対策追加後の流れをまとめると:
- ロードコマンド
LC_DYLD_CHAINED_FIXUPS
の解析 dyld_chianed_fixups_header
→dyld_chained_starts_in_image
→dyld_chained_starts_in_segment
の順番でchained fixupsが存在するセグメントまでたどる- 「仮のアドレス」→rebase/bindの構造体を解析
- rebase構造体から仮のアドレスと仮想メモリアドレスの辞書を生成する
- bind構造体から仮のアドレスとシンボル名文字列の辞書を生成する
- Classlist、CategoryListを走査して、上記の辞書をあわせてメタデータを生成
さて、エラーの理由が判明した今、結局class-dumpのエラーはどう対処すればいい?
結論から言うと、本家ツールの更新がされる未来が見えないため、正直派生のlechium氏製classdumpcの方を使うの最善策と思います
終わり
今回はiOSアプリの解析ツールclass-dump現状使用中で遭遇するエラーの一つーchained fixupsについて少し深堀りしてみました、今後はobjective-cのruntimeやswiftとの間の差について引き続き勉強して、機会があったら共有したいと思います。
*1:LC_VERSION_MIN_MACOSXは、macOS SDK 11.0では存在しない。LC_BUILD_VERSION に含まれる minos が同様の情報を扱っている
*5:https://raw.githubusercontent.com/qyang-nj/llios/main/articles/images/dynamic_linking_binding.png →Mach-O動的リンクの解説原文リンク
*6:https://mmbiz.qpic.cn/mmbiz_png/cZycwcLIiajpliaaAxVX1Bvdo0Ud4YDI7847vYrGLgOyHsBaCjHqAzZBGnYMS7ibGF3yzicCaoOXSFQyHOvluRmYkQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1 →解説原文リンク(中国語)
*7:https://mmbiz.qpic.cn/mmbiz_png/cZycwcLIiajpliaaAxVX1Bvdo0Ud4YDI78LdqibOVZ8kicE19vxyiatFvWZAzrqEyc5UqWaQy4Jt936gib2tEFT4mBUg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1 原文は*6と同様