前の記事では、共有
ELFライブラリのコールピックアップ方法について説明しました。 次に、
Mach-O-形式のライブラリで同じことを行う方法を見ていきます。
状況を簡単に思い出します。 Mac OS X用のプログラムがあります。これは、多くのサードパーティのダイナミックリンクライブラリを使用し、そのライブラリは互いの機能も使用します。
タスクは次のとおりです。あるライブラリから別のライブラリへの関数の呼び出しをインターセプトし、ハンドラーで元の関数を呼び出します。
いつものように、せっかちな人は今すぐすべてを
ダウンロードして試すことができ
ます 。
わかりやすくするために、架空の例を示します。C言語の「test」と呼ばれるプログラム(ファイルtest.c)と、事前にコンパイルされた内容が変更されていない共有ライブラリ(ファイルlibtest.c)があります。 このライブラリは、1つの関数libtest()を提供します。 実装では、それぞれが標準のC言語ライブラリ(libSystem.B.dylibに含まれるMac OSに付属)のputs()関数を使用します。 説明した状況の概略図を見てみましょう。

タスクは次のとおりです。
- libtest.dylibライブラリのputs()関数呼び出しを、メインプログラム(test.cファイル)に実装されたhooked_puts()関数呼び出しに置き換える必要があります。この関数呼び出しは、元のputs()を使用できます。

- 行われた変更を破棄します。つまり、libtest()の繰り返し呼び出しが元のput()の呼び出しにつながることを確認します。

同時に、コードの変更またはライブラリ自体の再コンパイルは許可されず、メインプログラムのみが許可されます。 呼び出しのリダイレクト自体は、プログラムを再起動せずに、特定のライブラリに対してのみ、オンザフライで実行する必要があります。
Mach-Oについて簡単に
Mach-Oを理解するための最良の方法は、以下の画像を見ることです。

人類はまだその構造をより明確に描写できていないようです。 最初の近似では、すべてが次のようになります。
- タイトル-ターゲットアーキテクチャに関する情報と、ファイルの内容をさらに解釈するためのさまざまなオプションがここに保存されます。
- ダウンロードコマンド-最初にロードするために、Mach-Oパーツをダウンロードする方法と場所、セグメント(以下を参照)、シンボルテーブル、およびこのファイルが依存するライブラリを示します。
- セグメント-セクションにコードまたはデータをロードするメモリ領域を記述します。
パーサーユーティリティ
2番目の近似については、いくつかのユーティリティに精通する必要があります。
- otool-システムに付属しているコンソールプログラムです。 ファイルのさまざまな部分(ヘッダー、ダウンロードコマンド、セグメント、セクションなど)の内容を表示できます。 起動時に-v(冗長)スイッチを追加すると特に便利です。
- MachOView -GPLで配布され、GUIがあり、Mac OS 10.6以降でのみ動作します。 Mach-Oの全内容を表示できます。他の部分のデータに基づいて、いくつかのセクションの情報を補足します。これは非常に便利です。

概して、一般ユーザーにとってMach-Oを理解するには、さまざまな例でMachOViewを試してください。 しかし、ヘッダー、ロードコマンド、セグメント、セクション、シンボルテーブル、およびそれらのフィールドの正確な説明の正確な構造は不明であるため、これはMach-Oプログラミングには十分ではありません。 しかし、これは仕様に関する大きな問題ではありません。 そして、公式のApple Webサイトでいつでも
利用できます 。 また、開発ツールをインストールしている場合は、/ usr / include / mach-o(特にloader.h)からヘッダーファイルを確認できます。
さらに、ファイルの内容はディスク上とまったく同じ順序でメモリ内にありますが、リンカはシンボルテーブルの一部、行のテーブル全体を削除し、ブート時にメモリ内の実際のオフセットの値を置くことができます。必要に応じて、ファイル内でこれらの値を通常ゼロにするか、ディスク上のオフセットに対応させることができます。
ヘッダー構造は単純です(32ビットアーキテクチャの場合は引用しますが、64ビットはそれほど変わりません)。
struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; };
すべてはマジック値で始まります(機械語の
バイト順序に関する合意に応じて、0xFEEDFACEまたはその逆)。 次に、プロセッサアーキテクチャのタイプ、ブートコマンドの数とサイズ、および他の機能を説明するフラグが表示されます。
例:

必須のブートコマンドは次のとおりです。
- LC_SEGMENT-特定のセグメントに関するさまざまな情報を含みます:サイズ、セクション数、ファイル内のオフセット、ロード後のメモリ内
- LC_SYMTAB-文字と文字列のテーブルをロードします
- LC_DYSYMTAB-インポートテーブルを作成します。文字データは文字テーブルから取得されます
- LC_LOAD_DYLIB-サードパーティライブラリへの依存関係を示します
例(それぞれ32ビットバージョンと64ビットバージョン):
最も重要なセグメントは次のとおりです。
- __TEXT-実行可能コードおよびその他の読み取り専用データ
- __DATA-書き込み可能なデータ。 インポートテーブルを含む
- __OBJC-Objective-Cランタイム標準ライブラリのさまざまな情報
- __IMPORT-32ビットアーキテクチャ専用のテーブルをインポートします(Mac OS 10.5でのみ生成しました)
- __LINKEDIT-ここで、ダイナミックローダーは、既にロードされているモジュール(文字、文字列などのテーブル)のデータを見つけます。
ダウンロードコマンドは、次のフィールドで始まります。
struct load_command { uint32_t cmd;
その後、コマンドのタイプに応じて、さらに多くの異なるフィールドがあります。
例:

これらのセグメントで最も興味深いセクションは次のとおりです。
- __TEXT、__ text-実際のコード
- __TEXT、__ cstring-定数文字列(二重引用符で囲みます)
- __TEXT、__ const-さまざまな定数
- __DATA、__ data-初期化された変数(文字列と配列)
- __DATA、__ la_symbol_ptr-インポートされた関数へのポインターのテーブル
- __DATA、__ bss-初期化されていない静的変数
- __IMPORT、__ jump_table-インポートされた関数の呼び出しのスタブ
将来的には、1つのMach-Oでは、インポートテーブルは__IMPORT、__ jump_table(32ビット、Mac OS 10.5)、または__DATA、__ la_symbol_ptr(64ビット、またはMac OS 10.6以前)のいずれかになります。
セグメント内のセクションの構造は次のとおりです。
struct section { char sectname[16]; char segname[16]; uint32_t addr; uint32_t size; uint32_t offset; uint32_t align; uint32_t reloff; uint32_t nreloc; uint32_t flags; uint32_t reserved1; uint32_t reserved2; };
セグメントの名前とセクション自体、サイズ、ファイル内のオフセット、ダイナミックローダーが配置したメモリ内のアドレスがあります。 さらに、特定のセクションに固有の他の情報があります。
例:

ファットバイナリ
もちろん、Appleがターゲットアーキテクチャ(Motorola-> IBM-> Intel)のスムーズな移行を繰り返した結果、実行可能ファイルとライブラリは、複数のバージョンの実行可能コードを一度に格納するために「学習」したことに言及する価値があります。 一般に、これらのファイルは
fat binaryと呼ばれます。 実際、これらは1つのファイルに集められたいくつかのMach-Oですが、そのヘッダーは特別です。 サポートされているアーキテクチャの数と種類、および各アーキテクチャへのオフセットに関する情報が含まれています。 このオフセットには、上記の構造を持つ通常のMach-Oがあります。
Cでは次のようになります。
struct fat_header { uint32_t magic; uint32_t nfat_arch; };
0xCAFEBABEが魔法の下に隠されている場合(またはその逆-異なるプロセッサ上のマシン語の異なるバイト順について覚えておいてください)。 そしてその後、その型のちょうどnfat_arch構造がすぐに続きます:
struct fat_arch { cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t offset; uint32_t size; uint32_t align; };
実際には、フィールド名は、プロセッサの種類、特定のMach-Oのファイル内のオフセット、サイズ、および配置を表しています。
実験プログラム
インポートされた関数を呼び出す操作を調べるために、Cで次のファイルを取得します。
ファイルtest.c
void libtest(); //from libtest.dylib int main() { libtest(); //calls puts() from libSystem.B.dylib return 0; }
libtest.cファイル
#include <stdio.h> void libtest()
動的レイアウトを探索する
Intelプロセッサに限定されています。 Mac OS 10.5を入手しましょう。 これらのファイルを新しいXcodeプロジェクトに追加し、コンパイル(32ビットバージョン)してデバッグモードで実行し、libtest.dylibライブラリのlibtest()関数でputs()関数が呼び出される行で停止します。 libtest()のアセンブラリストを次に示します。

もう1つの命令を実行しましょう。

そして彼女の記憶を見てください:

これは、インポートテーブルのセル(この場合、セル__IMPORT、__ jump_table)であり、レイトバインディング(遅延バインディング)が使用されている場合、またはターゲット関数にすぐにジャンプする場合、ダイナミックローダー(関数__dyld_stub_binding_helper_interface)を呼び出すためのスプリングボードとして機能します。 これは、その後のputs()の呼び出しによって確認されます。

そしてメモリ内:

そのため、ダイナミックローダーがCALL間接呼び出し命令(0xE8)をJMP間接ジャンプ命令(0xE9)に置き換えたことがわかります。 したがって、__ jump_table要素をリダイレクトするには、初期コンテンツの代わりに、置換関数の先頭への間接遷移の命令を規定するだけで十分です。
もう一つの興味深い点。 JMPがダイナミックローダー(別名リンカー)への切り替えに使用されないのはなぜですか? はい。スタックに戻りアドレスを格納するCALLは、インポートテーブルのどの要素が呼び出し元を呼び出したかをリンカが判断するのに役立つためです。 したがって、必要な機能の間接JMPを使用してCALLをそれ自体に変更することにより、どのようなシンボルであるかを計算し、有効にします。
次に、プロジェクトをMac OS 10.6に転送し、32ビットおよび64ビットアーキテクチャ用のファットバイナリをコンパイルします。 念のため、Xcodeでは次のようにします。

コンパイルし、64ビットバージョンを実行して(たとえば、Snow Leopardのインポートテーブルは32ビットと同じになります)、puts()呼び出しで再び停止します。

繰り返しますが、単純なCALLです。 さらに調査します。

ここで、通常の__IMPORT、__ jump_tableとの違いはすでに顕著です。
__TEXT、__ symbol_stub1へようこそ。 この表は、インポートされた各関数のJMP命令のセットです。 私たちの場合、上記のような指示は1つしかありません。 このような各命令は、__ DATA、__ la_symbol_ptrテーブルの対応するセルで指定されたアドレスに移行します。 後者は、このMach-Oのインポートテーブルです。
しかし、研究を続けましょう。 移行が行われるアドレスを見ると:

次に、以下が表示されます。

__TEXT、__ stub_helperセクションに入ります。 これは基本的に、Mach-OのPLT(プロシージャリンケージテーブル)です。 最初の命令(この場合、R11と組み合わせたLEA、または単純なPUSHがあった可能性があります)、動的リンカーは再割り当てが必要なシンボルを記憶し、2番目の命令は常に同じアドレスにつながります-バインディングを処理する__dyld_stub_binding_helper関数の開始:

動的リンカーがputs()の再配置を実行すると、__ DATA、__ la_symbol_ptrの対応するセルは次のようになります。

そして、これはlibSystem.B.dylibモジュールのputs()関数のアドレスです。 つまり、アドレスに置き換えると、コールをリダイレクトするという望ましい効果が得られます。
だから。 この段階で、動的リンクの発生方法、Mach-Oのインポートテーブルの種類、およびそれらがどの要素で構成されているかを特定の例を使用して確認しました。 それでは、Mach-Oの解析に取りかかりましょう!
インポートテーブルでアイテムを検索する
インポートテーブルで、シンボルの名前で対応するセルを見つける必要があります。 このアクションのアルゴリズムはやや重要です。
まず、キャラクターテーブルでキャラクター自体を見つける必要があります。 後者は、次の構造の配列です。
struct nlist { union { int32_t n_strx; } n_un; uint8_t n_type; uint8_t n_sect; int16_t n_desc; uint32_t n_value; };
n_un.n_strxは、この文字の名前の文字列テーブルの先頭からのバイト単位のオフセットです。 残りは、シンボルのタイプ、シンボルが配置されているセクションなどに関係します。 つまり、実験ライブラリlibtest.dylib(32ビットバージョン)の最後の要素の一部を次に示します。

行テーブルは、それぞれがゼロで終わる名前のチェーンです。 ただし、コンパイラは各名前の先頭にアンダースコア「_」を追加するため、たとえば、文字列テーブルでは「puts」という名前は「_puts」のようになります。
以下に例を示します。

対応するロードコマンド(LC_SYMTAB)から文字と文字列のテーブルの場所を見つけることができます。

ただし、シンボルテーブルは均一ではありません。 いくつかのセクションがあります。 そのうちの1つは特に興味深いものです。これらは未定義(未定義)の文字、つまり動的にリンクされている文字です。 ところで、MachOViewは背景が青みがかったものを強調表示します。 シンボルテーブルのどの部分が未定義のシンボルのサブセットを反映しているかを判断するには、動的シンボルロードコマンド(LC_DYSYMTAB)を調べる必要があります。

Cでの彼女のプレゼンテーションは次のとおりです。
struct dysymtab_command { uint32_t cmd; uint32_t cmdsize; uint32_t ilocalsym; uint32_t nlocalsym; uint32_t iextdefsym; uint32_t nextdefsym; uint32_t iundefsym; uint32_t nundefsym; uint32_t tocoff; uint32_t ntoc; uint32_t modtaboff; uint32_t nmodtab; uint32_t extrefsymoff; uint32_t nextrefsyms; uint32_t indirectsymoff; uint32_t nindirectsyms; uint32_t extreloff; uint32_t nextrel; uint32_t locreloff; uint32_t nlocrel; };
ここで、dysymtab_command.iundefsymは、未定義の文字のサブセットで始まる文字テーブルのインデックスです。 dysymtab_command.nundefsym-未定義の文字の数。 探しているのは意図的に不定のシンボルなので、このサブセットのシンボルテーブルでのみ検索する必要があります。
そして今、非常に重要な点:名前でシンボルを見つけること、私たちにとって最も重要なことは、シンボルテーブルのインデックスを最初から覚えることです。 これらのインデックスの数値は別の重要なテーブルであるため、間接シンボルのテーブルです。 dysymtab_command.indirectsymoffの値で見つけることができ、インデックスの数によってdysymtab_command.nindirectsymsが決まります。
些細なケースでは、このテーブルは1つの要素のみで構成されています(実際にはさらに多くの要素があります)。

最後に、最後に見つける必要がある要素の__IMPORT、__ jump_tableセクションを見てみましょう。 次のようになります。

このセクションのsection.reserved1フィールドは非常に重要です(MachOViewはIndirect Sym Indexと呼ばれます)。 これは、__ jump_table要素で1対1の対応が始まる間接シンボルテーブルのインデックスを意味します。 また、間接シンボルテーブルの要素は、シンボルテーブルのインデックスであることを覚えています。 私が得ているものをキャッチしますか?
しかし、最終的にすべての知識を収集する前に、画像を完成させるために、__ DATA、__ la_symbol_ptrがインポートテーブルの役割を果たすSnow Leopardの状況を簡単に見てみましょう。 実際、違いはあまり目立ちません。
以下は、キャラクターの読み込みコマンドです。

そして、ここに彼女の最後の要素があります:

ダイナミックシンボルロードコマンド(LC_DYSYMTAB)からのデータに対応する2つのあいまいな文字が青みがかった背景に表示されます。

また、間接シンボルの表には、すでに1つの要素ではなく、4つの要素があります。

ただし、秘蔵セクション__la_symbol_ptrのreserved1フィールドを見ると、間接シンボルのテーブルでの要素の1対1の反映は、最後の要素の先頭からではなく、4番目の要素(インデックスは3)から始まることがわかります。

__la_symbol_ptrセクションで説明されているインポートテーブルの内容は次のとおりです。

これらすべてのMach-Oの微妙さを知ったので、インポートテーブルで目的のアイテムを見つけるためのアルゴリズムを定式化できます。
リダイレクトアルゴリズム
すべてのアクションを言葉で説明します。コメントが豊富にあるにもかかわらず、コードはそれほど明確ではない可能性があるためです。
- LC_SYMTAB loadコマンドからのデータに従って、文字と行のテーブルを見つけます。
- LC_DYSYMTABブートコマンドから、未定義のシンボルのサブセットが開始するシンボルテーブルの要素(iundefsymフィールド)から学習します。
- 文字テーブル内の未定義文字のサブセットの中から、名前でターゲット文字を探しています。
- シンボルテーブルの先頭からターゲットシンボルのインデックスを覚えています。
- LC_DYSYMTABロードコマンド(indirectsymoffフィールド)からのデータを使用して、間接シンボルのテーブルを見つけます。
- 間接シンボルテーブル(reserved1フィールド)で、インポートテーブルの表示を開始するインデックス(__DATA、__ la_symbol_ptrセクションの内容(または__IMPORT、__ jump_table-1つあります))を見つけます。
- このインデックスから始めて、間接シンボルテーブルを見て、シンボルテーブル内のターゲットシンボルのインデックスに対応する値を探します。
- インポートテーブルが間接シンボルテーブルに表示されたときに、最初からターゲットシンボルがキャッチされた方法を覚えています。 格納されている値は、インポートテーブル内の目的のアイテムのインデックスです。
- __la_symbol_ptr(または__jump_table)セクションのデータによると、インポートテーブル(オフセットフィールド)が見つかります。
- ターゲット要素のインデックスが含まれているので、アドレス(__la_symbol_ptrの場合)を必要な値に書き換えます(またはオペランドを使用してCALL / JMP命令をJMPに変更します-必要な関数のアドレス(__jump_tableの場合))。
ファイルから読み込むことによってのみ、文字、文字列、および間接文字のテーブルを操作する必要があることに注意してください。 そして、インポートテーブルを説明するセクションの内容を読み、そしてもちろん、既にメモリにある自身をリダイレクトします。 これは、文字テーブルと文字列テーブルがターゲットMach-Oの実際の状態を表示する場合と表示しない場合があるためです。 結局、ダイナミックローダーは私たちの前でそこで働き、テーブル自体を配置することなく、シンボルに関する必要なすべてのデータを安全に保存しました。
リダイレクトの実装
あなたの考えをコードに変える時です。 リダイレクトごとに必要なMach-O要素の検索を最適化するために、操作全体を3つの段階に分割します。
void *mach_hook_init(char const *library_filename, void const *library_address);
Mach-Oファイル自体とメモリ内の表示に基づいて、この関数は、インポートテーブルへのオフセット、文字、行のテーブル、および動的文字のテーブルからの間接文字の表示である不透明な記述子を返します。このモジュールの。 この記述子は次のとおりです。
struct mach_hook_handle { void const *library_address; //base address of a library in memory char const *string_table; //buffer to read string_table table from file struct nlist const *symbol_table; //buffer to read symbol table from file uint32_t const *indirect_table; //buffer to read the indirect symbol table in dynamic symbol table from file uint32_t undefined_symbols_count; //number of undefined symbols in the symbol table uint32_t undefined_symbols_index; //position of undefined symbols in the symbol table uint32_t indirect_symbols_count; //number of indirect symbols in the indirect symbol table of DYSYMTAB uint32_t indirect_symbols_index; //index of the first imported symbol in the indirect symbol table of DYSYMTAB uint32_t import_table_offset; //the offset of (__DATA, __la_symbol_ptr) or (__IMPORT, __jump_table) uint32_t jump_table_present; //special flag to show if we work with (__IMPORT, __jump_table) };
mach_substitution mach_hook(void const *handle, char const *function_name, mach_substitution substitution);
この関数は、使用可能なライブラリ記述子、ターゲットシンボルの名前、インターセプターのアドレスに従って、上記のアルゴリズムに従ってリダイレクト自体を実行します。
void mach_hook_free(void *handle);
これは、mach_hook_init()を返したハンドルをクリーンアップします。
これらのプロトタイプを考えると、テストプログラムを書き直す必要があります。
#include <stdio.h> #include <dlfcn.h> #include "mach_hook.h" #define LIBTEST_PATH "libtest.dylib" void libtest(); //from libtest.dylib int hooked_puts(char const *s) { puts(s); //calls the original puts() from libSystem.B.dylib, because our main executable module called "test" remains intact return puts("HOOKED!"); } int main() { void *handle = 0; //handle to store hook-related info mach_substitution original; //original data for restoration Dl_info info; if (!dladdr((void const *)libtest, &info)) //gets an address of a library which contains libtest() function { fprintf(stderr, "Failed to get the base address of a library!\n", LIBTEST_PATH); goto end; } handle = mach_hook_init(LIBTEST_PATH, info.dli_fbase); if (!handle) { fprintf(stderr, "Redirection init failed!\n"); goto end; } libtest(); //calls puts() from libSystem.B.dylib puts("-----------------------------"); original = mach_hook(handle, "puts", (mach_substitution)hooked_puts); if (!original) { fprintf(stderr, "Redirection failed!\n"); goto end; } libtest(); //calls hooked_puts() puts("-----------------------------"); original = mach_hook(handle, "puts", original); //restores the original relocation if (!original) { fprintf(stderr, "Restoration failed!\n"); goto end; } libtest(); //again calls puts() from libSystem.B.dylib end: mach_hook_free(handle); handle = 0; //no effect here, but just a good advice to prevent double freeing return 0; }
テストケースの完全な実装は、リダイレクトアルゴリズムとプロジェクトファイルと共に
ダウンロードできます 。
試運転
次のようなものを試してください:
user@mac$ arch -i386 ./test libtest: calls the original puts()
user@mac$ arch -x86_64 ./test libtest: calls the original puts()
プログラムの結論は、最初に設定されたタスクが完全に実行されたことを示しています。
便利なリンク
頑張って