ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

【お知らせ】2021年5月10日~リニューアルオープン!今後はこちらで新しい記事を公開します。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:ペネトレーションテスト

class-dumpの実行エラーから始まるchained fixups入門

どうも、でぃーぴーぶの魚脳です。今回はiOSアプリの解析時に使用するツール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

こっちもググってみたら、0x800000330x80000034 はそれぞれロードコマンドLC_DYLD_EXPORTS_TRIELC_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に関する処理が実装されたものの、実行する箇所が最終的にコメントアウトされています。

オフセット全部0x100000000固定であることがMachOViewから確認できる

一方bindですが、Mach-Oの動的リンク方式はlazy binding、non-lazy binding、weak bindingの3種類存在しますが、ここではlazy bindとnon-lazy bindだけ軽く説明します

lazy binding

関数類を呼ぶ機会が多いため最初から一気にbindするには時間かかります、それを解消するためlazy binding はシンボル名の検索とアドレスの解決を初回呼び出し時まで遅延させることでプログラムの起動を早めます。*4 lazy binding手順は↓の画像が示すようになります

*5

  1. 1回目の呼び出し時のアドレスは実は__stubsの項目を指します(赤線1)
  2. __stubsの項目内に保存されたオペコードで__la_symbol_ptrまで飛ぶ、ここはかなりELFのPLTと似ています(赤線2)
  3. __la_symbol_ptrに保存されたアドレスは__stub_helperを指します(赤線3)
  4. __stub_helper内の処理を経て最終的に__DATA,_CONST,__gotdyld_stub_binder まで飛ぶ(赤線4,5)
  5. dyld_stub_binder は動的ライブラリ内のシンボルを見つけ、それを__la_symbol_ptr に書き戻し、実際のメソッドにジャンプします
  6. 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つデメリットが存在します。

  1. 前述の通り、従来の動的リンク方式はどうしても特殊なオペコードに依存します、これらのオペコードは少なくない空間を占めた上で、動的リンク後無用なバイト列に化して、かなりもったいないです
  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ファイルは以下のような構造になっています。

chained fixup適用前(左)chained fixup適用後(右)

*6

主な変更点としては

  • ロードコマンドからLC_DYLD_INFOが消えLC_DYLD_CHAINED_FIXUPSが追加されます。これがエラーを引き起こす一つの理由
  • __DATA(_CONST)セグメントにrebaseとbindに関連する情報が含まれる連結リスト風な構造体がズラリ。画像の中ではrebaseとbaseが分かれて存在するような形になっていますが、実際は両種ごちゃまぜでセグメントに配置される形になります。
  • __LINKEDITセグメントに従来のオペコードの代わりに動的ライブラリに関する情報が含まれるchained fixups関連の構造体が多数存在します。(後ほどいくつをピックアップして紹介します)

新しく追加されたデータ間の関係を示す画像が以下のようになります。

chained fixupsに関連する構造体間の関係性

*7

重要な構造体は↓の3つ:

  • dyld_chained_import
  • CHAIEND_POINTER_64_REBASE
  • CHAIEND_POINTER_64_BIND

import_offsetから特定

まずはdyld_chained_import、この構造体は__LINKEDITchained_starts_in_segmentの後ろに格納され、dyld_chained_fixups_headerimports_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_REBASECHAIEND_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構造体中のordinaldyld_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型構造体

NSString型の文字列は__DATA_CONST __cfstringセクションの32バイト分の容量を占めます、文字列自体のポインター0x40d0からの8バイトに格納されるため、この8バイトはrebase構造体とみなされます。

*8

0x40d0に格納された8バイトはrebasing時にrebase構造体として扱われます

rebase構造体

オフセットが指す先には文字列addMethod fired が確認できます

0x23e5から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になるため)

DATA_CONST,gotが0x4000から始まる

bind構造体に当てはまると、ordinalが0になることがわかります

bind構造体

実際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_headerdyld_chained_starts_in_imagedyld_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_headerdyld_chained_starts_in_imagedyld_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との間の差について引き続き勉強して、機会があったら共有したいと思います。