「 Mach-Oフォーマット 」一覧

Mach-Oのオブジェクトファイルの関数を呼び出す


いわゆるBinary Hacksにあった、「オブジェクトファイルを自力でロードする」のMach-O版をやってみました。MacではオブジェクトファイルもまたMach-O形式で出力されています。

今までの記事の内容で多くのことがわかってきました。特に関数フックが実現出来るようになった今、形式が同じであるオブジェクトファイルのロード&実行はそんなに難易度の高いものではなさそうです。

Mach-Oオブジェクトのロード

まずは簡単にやってみようとおもいます。ここでは以下のデータが必要になってきます。

・symtab_command
・シンボルテーブル
・ストリングテーブル

これらの情報から、すでに関数名から関数実体を検索することが(実は)可能です。知っての通り nlist_64のメンバ n_strx がストリングテーブルのオフセットを示しているので、シンボルテーブル全てのnlistを辿って、名前一致で検索すれば関数が存在していれば発見することが出来ます。
このときの n_value が、そのシンボルが存在するセクションの先頭からのオフセットとなっています。実際には、ロードしたオブジェクトファイル先頭+該当セクションのoffsetメンバ+n_value で関数の所在が計算できます。

求まった場所でそのまま実行できるか、というとメモリ保護の問題があるので mprotectで実行属性を付与します。実行属性さえつけてしまったら、多くの場合はこれで実行が可能です。

ここまでの内容を擬似コードで示すと以下のようになるかなと思います。

リロケーションについて

多くの場合は上記の方法でも問題なく動くことだと思います。ただたまにリロケーションが必要だったりするのでこの場合はちょっと厄介です。一般的なケースではグローバル変数にアクセスしている場合が該当してしまうようです。

リロケーションの情報は __textセクションの reloff メンバで示される位置に、nreloc個の情報が格納されています。これが struct relocation_info の配列となっています。この配列メンバを地道に解釈して、必要な位置の値を書き換えていくことでリロケーションが完成するようです。

多くの場合PC相対アドレスをジャンプ先として埋め込んでいくとか変数参照先としているような感じです。そのため、はめ込み元とアクセス先のアドレスを計算して差分を埋め込む、という形になります。


Mach-Oでの関数フック


ようやくここまでたどり着きました。以前 elfバイナリでやったことのMach-O版です。
実行体は外部.soの何かの関数を呼び出しているとして、その関数への参照をフックしたいという要求に応えてみたいと思います。
フックした後の関数は実行体内部に存在するとします。

これらの関係を図示すると以下のようになります(図は以前の使い回しです…)。

elf-hook1

今までに調査してわかっている内容を利用するとこれが実現出来ます。

前提条件にあるように外部の共有ライブラリを利用する場合、インポート関数テーブルが生成されます。このテーブルが参照するデータの中に関数の実体が格納される部分がありました。このあたりの調査過程はこちらの記事を参考にしてみてください。
この la_symbol_ptrセクションの該当する部分を求めて、置き換えたい関数のアドレスに更新するだけで処理は完了です。
実は、jump_tableだとか __IMPORTというセクションも存在するらしいのですが、手元ではこれらの実行体を作ることが出来ず未確認です。

準備

ロードコマンド LC_SYMTAB からシンボルテーブルの位置を求める
ロードコマンド LC_STRTAB からストリングテーブルの位置を求める
ロードコマンド LC_DYSYMTAB から動的シンボルテーブルの情報を取得する
セクションの情報を処理して、la_symbol_ptrのセクション情報を取得する

フック仕込み

関数名から関数のアドレスが書かれている場所を求めるまでは以下のようになります。

シンボルテーブルの中身(nlist構造体の配列)の中から関数名情報を求める。
関数名がフックを仕込みたい関数名かどうかをチェック。
次にその関数が Indirectテーブルの中でどの位置にあるかを検索
上記で求まった序数で、インポートテーブルの場所を特定し、その中に新しい関数のアドレスを書き込んでおく。
ここでのポイントは序数はあくまでインポートテーブルでの序数とするため、reserved1で示される開始インデックス値を引いて求める必要があります。

インダイレクトテーブルに関しては以前の記事にて走査の方法が使えるのでここでは割愛します。


Mach-O編 Import関数の列挙


前回の最後で dysymの undef extsymbolのほうがImport関数の情報として正しそうと感じていましたが、どうやらそれは間違っていたようです。正しくはIndirectテーブルから求めていくのが正解のようです。

このIndirectテーブルは struct dysymtab_command の indirectsymoff で示される場所に配置されています。
そしてこのテーブルのエントリ数は nindirectsyms となっています。

ではこのエントリ数分のnindirectsymsがインポート関数かと言われるとそうでもないようで、この中の一部分となっているようです。
セクション __la_symbol_ptr で示される reserved1メンバがこのIndirectテーブルでの開始点を示すことになっているようです。
そこで、エントリのreserved1から nindirectsymsまでの中身を確認してみます。

このエントリは uint32_t の配列となっているので取り出すと単なる整数です。
これはシンボルテーブルのエントリのインデックスとなっています。情報表示をするにはシンボルテーブルの情報にアクセスする必要があります。

これらの処理を以下のようにコードにしてみました。割と自明なところのコードは省きます。

この実行結果の一部を公開するとこんな感じになります。

  256 : __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev
  257 : __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEi
  258 : __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEm
  261 : __ZNSt3__16localeD1Ev
  262 : __ZNSt3__18ios_base33__set_badbit_and_consider_rethrowEv
  263 : __ZNSt3__18ios_base5clearEj
  264 : __ZSt9terminatev
  265 : __ZdlPv
  266 : __Znwm
  268 : ___cxa_begin_catch
  269 : ___cxa_end_catch
  245 : __Unwind_Resume
  267 : ___cxa_atexit
  271 : ___stack_chk_fail
  273 : __dyld_get_image_header
  274 : __dyld_get_image_name
  275 : __dyld_get_image_vmaddr_slide
  276 : __dyld_image_count
  277 : _getpid
  278 : _mach_port_deallocate
  280 : _mach_vm_region
  281 : _printf
  282 : _strcmp
  283 : _strlen
  284 : _sysctl

それっぽいものが出力されるようになりました!


セグメント&セクションの補足と外部関数への依存について


mach-o編もずいぶんと進んできたので、PEの時にやったように依存するライブラリ情報だけでなく、関数名を表示したいを思って色々を調べてみました。その結果、条件限定かもしれませんがうまく表示できるようになりました。今回はその内容をメモとして公開しようと思います。正しい方法とか指摘してくれると幸いです。

セクションについて

今までのセクション情報で表示できていない部分(わかっていなかった部分)がありました。それは __stubs, __nl_symbol_ptr, __la_symbol_ptr といった一部のセクションでは reserve1, reserve2のメンバに有効な値が入っているということでした。たとえば、 __stubs セクションでは reserve1に インダイレクトシンボルのインデックス開始点が格納され、reserve2にスタブ1要素のバイト数が格納されていました。他の場合でも reserve1にインダイレクトシンボルの開始点が格納されていたりします。

主に動的ライブラリ用の情報を主に調べていくと、情報として必要になるのは symtab, dysymtabのロードコマンドの情報と、__stubs, __nl_symbol_ptr, __la_symbol_ptr のセクション、そして各シンボルテーブルといったところです。含まれているシンボルを一覧する際には symtabの中身を表示すればよい程度でしたが、動的ライブラリの中に含まれる関数(&シンボル)を使っている場合、ここまで簡単なものではなかったです。

__stubsについて

ここの中身は名前の通り外への関数のスタブです。elfバイナリでいうところの PLTっぽい印象を受けました。とりあえずここに含まれる関数の情報を出力するとそのバイナリが使用している外部関数の情報が一覧として出力できそうです。
 この関数情報を辿る場合には、まず reserve1で開始インデックス点を取得し、サイズと1要素の関係から含まれる個数を求めます。その後インダイレクトテーブルからインデックスを取得します。このインデックス値がシンボルテーブルのインデックス値となるようなので、シンボルの文字列テーブルからシンボル名を取得することができるようになります。

__nl_symbol_ptrセクション

このセクションのreserve1にはインダイレクトテーブル内での開始インデックス値が格納されています。そこから参照するするインデックス値がテーブル内から求まるので、シンボルテーブルにアクセスします。注意点としては、インダイレクトテーブルからテーブル引きした値が特定の値かどうかチェックする必要があるということです。ある値ではシンボルテーブルへアクセスすると範囲外アクセスとなってしまいます(INDIRECT_SYMBOL_LOCAL, INDIRECT_SYMBOL_ABS)。

__la_symbol_ptrセクション

このセクションもまた __nl_symbol_ptrセクションと同じように処理をします。

アクセスのコードとか

これらの情報にアクセスしている部分のコード抜粋(というか擬似コード?)を掲載します。

このコードにより今まで使用しているサンプルプログラム(C)の実行体を表示させてみると以下のようになります。オフセットや中身のデータは正しそうですし、一応関数名(シンボル)もまた表示されているのでうまくいっていそうです。

nl_symbol_ptr
  offset:00001000  data: 0000000000000000  (dyld_stub_binder)
  offset:00001008  data: 0000000000000000  (ABSOLUTE)
la_symbol_ptr
  offset:00001010  data: 0000000100000F54  (_printf)
*** import func(?) ***
0 : _printf (100000f3e)

もうすこし複雑なプログラムにしてみようと思います。以下に示すようにCの標準関数とC++の関数を使用してみました。これならば依存ライブラリは2つになりますので、さっきよりは多くの情報が出力されるのが期待できます。

これに対する実行結果は以下のようになりました。

nl_symbol_ptr
  offset:00001000  data: 0000000000000000  (dyld_stub_binder)
  offset:00001008  data: 0000000000000000  (ABSOLUTE)
la_symbol_ptr
  offset:00001028  data: 0000000100000ECC  (__ZNSolsEPFRSoS_E)
  offset:00001030  data: 0000000100000ED6  (__ZNSt8ios_base4InitC1Ev)
  offset:00001038  data: 0000000100000EE0  (__ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc)
  offset:00001040  data: 0000000100000EEA  (___cxa_atexit)
  offset:00001048  data: 0000000100000EF4  (_printf)

*** import func(?) ***
0 : __ZNSolsEPFRSoS_E (100000e9c)
1 : __ZNSt8ios_base4InitC1Ev (100000ea2)
2 : __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc (100000ea8)
3 : ___cxa_atexit (100000eae)
4 : _printf (100000eb4)

C++のマングルが入っています。明らかにC++のランタイムへの参照が見て取れます。また先ほどと同様のC関数もまた含まれています。importの項目は依存している関数や変数の情報だけを表示しているようにも見え、__stubsの情報が今求めているImport関数一覧と近いものという考えが合っているように思います。

ただ実際のところ、dysymの情報にある undef extsymbol の情報を表示させてみると以下のようになり、__stubsによる一覧よりも多いことがわかります。より正確にはこちらの情報の方がImportの一覧として正しい気もしてきます。

*** undef extsymbol ***
0 : __ZNSolsEPFRSoS_E
1 : __ZNSt8ios_base4InitC1Ev
2 : __ZNSt8ios_base4InitD1Ev
3 : __ZSt4cout
4 : __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
5 : __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
6 : ___cxa_atexit
7 : _printf
8 : dyld_stub_binder

mach-oの外部関数解決の仕組みを追う(__stubs, __nl_symbol_ptr, __la_symbol_ptrセクション)


C/C++ミックスの実行体を使うようにしたら、実は __la_symbol_ptr セクションは存在しつつ、 __got セクションが出現しました。__la_symbol_ptrセクションが示す先が PLT.GOT相当の領域っぽいなぁと思っていただけに、そのものズバリなセクションが出現して驚きでした。この__gotセクションも他の同種セクションのように扱うだけで参照先を特定することができます。

今回は、こういった外部関数の呼び出しにおいてどのように解決処理がなされるのかを追いかけてみたいと思います。

用意するもの

上記のような簡単な Cプログラムを用意して確認してみます。
このプログラムで、1回目の printf で関数初回呼び出し、2回目で解決済み呼び出しとなるのか、を調査しようという計画です。

調査

まずは実行体の調査をしておきます。このプログラムでは __stubs の内容が以下のようになっていました(以前のプログラムを実行させて必要そうなデータだけ抜粋します)。

section[1] : __TEXT [__stubs]
           : addr=0x100000f3e, offset=3902, size=6
           : align=2, reloff=0, nreloc=0
           : resv0=0, resv1=6, resv2=0
           : flags=80000408 > S_SYMBOL_STUBS S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
           : Indirect Sym Index = 0, Size of Stubs = 6 (stub count=1)
section[2] : __TEXT [__stub_helper]
           : addr=0x100000f44, offset=3908, size=26
           : align=4, reloff=0, nreloc=0
           : resv0=0, resv1=0, resv2=0
           : flags=80000400 > S_REGULAR S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS

section[0] : __DATA [__nl_symbol_ptr]
           : addr=0x100001000, offset=4096, size=16
           : align=8, reloff=0, nreloc=0
           : resv0=1, resv1=0, resv2=0
           : flags=00000006 > S_NON_LAZY_SYMBOL_POINTERS
           : Indirect Sym Index = 1
section[1] : __DATA [__la_symbol_ptr]
           : addr=0x100001010, offset=4112, size=8
           : align=8, reloff=0, nreloc=0
           : resv0=3, resv1=0, resv2=0
           : flags=00000007 > S_LAZY_SYMBOL_POINTERS
           : Indirect Sym Index = 3

nl_symbol_ptr
  offset:00001000  data: 0000000000000000  (dyld_stub_binder)
  offset:00001008  data: 0000000000000000  (ABSOLUTE)
la_symbol_ptr
  offset:00001010  data: 0000000100000F54  (_printf)
*** import ***
0 : _printf (100000f3e)

main関数直後でbreakさせて、コードを確認してみます。

(lldb) disassemble
a.outmain:
-> 0x100000f00:  pushq  %rbp
   0x100000f01:  movq   %rsp, %rbp
   0x100000f04:  subq   $0x10, %rsp
   0x100000f08:  leaq   0x4f(%rip), %rdi          ; "Hello,"
   0x100000f0f:  movl   $0x0, -0x4(%rbp)
   0x100000f16:  movb   $0x0, %al
   0x100000f18:  callq  0x100000f3e               ; symbol stub for: printf
   0x100000f1d:  leaq   0x41(%rip), %rdi          ; "world.\n"
   0x100000f24:  movl   %eax, -0x8(%rbp)
   0x100000f27:  movb   $0x0, %al
   0x100000f29:  callq  0x100000f3e               ; symbol stub for: printf
   0x100000f2e:  movl   $0x0, %ecx
   0x100000f33:  movl   %eax, -0xc(%rbp)
   0x100000f36:  movl   %ecx, %eax
   0x100000f38:  addq   $0x10, %rsp
   0x100000f3c:  popq   %rbp
   0x100000f3d:  ret    

printfの関数として 0x100000f3e アドレスを呼び出しています。この部分は事前情報によれば、__stubsの内容の先頭アドレスと一致しています。まさにprintfのスタブですね。
この 0x100000f3e アドレスでは以下のようになっていました。この周辺をちょっと調べてみます。

(lldb) disassemble 
a.outsymbol stub for: printf:
-> 0x100000f3e:  jmpq   *0xcc(%rip)               ; (void *)0x0000000100000f54

(lldb) memory read 0x100000f44+0xcc   ; 上記のrip+相対0xccで求まる番地
0x100001010: 54 0f 00 00 01 00 00 00 00 00 00 00 00 00 00 00  T...............
0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

(lldb) si
* thread #1: tid = 0x25c01, 0x0000000100000f54 a.out, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x0000000100000f54 a.out
-> 0x100000f54:  pushq  $0x0
   0x100000f59:  jmpq   0x100000f44

(lldb) b 0x100000f29
(lldb) c

Process 1698 stopped
* thread #1: tid = 0x25c01, 0x0000000100000f29 a.outmain + 41, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000000100000f29 a.outmain + 41
a.outmain + 41:
-> 0x100000f29:  callq  0x100000f3e               ; symbol stub for: printf
   0x100000f2e:  movl   $0x0, %ecx

(lldb) si
Process 1698 stopped
* thread #1: tid = 0x25c01, 0x0000000100000f3e a.outprintf, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x0000000100000f3e a.outprintf
a.outsymbol stub for: printf:
-> 0x100000f3e:  jmpq   *0xcc(%rip)               ; (void *)0x00007fff966e3784: printf
   0x100000f44:  leaq   0xbd(%rip), %r11          ; (void *)0x00007fff5fc3b7b8

(lldb) memory read 0x100001010
0x100001010: 84 37 6e 96 ff 7f 00 00 00 00 00 00 00 00 00 00  .7n.?...........
0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

(lldb) disassemble --start-address 0x7fff93021878
libdyld.dylib`dyld_stub_binder:
   0x7fff93021878:  pushq  %rbp
   0x7fff93021879:  movq   %rsp, %rbp
   0x7fff9302187c:  subq   $0xc0, %rsp
   0x7fff93021883:  movq   %rdi, (%rsp)
   0x7fff93021887:  movq   %rsi, 0x8(%rsp)

わかる人には不要な話ですが、とりあえず解説をしてみます。
jmpq 命令では指定されたアドレスの内容へ飛んでいます。またこのアセンブラを見るとRIPとオフセット値(0xCC)でアドレスを求めています。そのアドレスの中身を見てみると、元々のコメントで指摘あるように 0x100000f54 が書かれていました。
このアドレスが 0x100001010 となります。このアドレスというのが la_symbol_ptrセクションの内容(先頭)となっています。
0x100000f54番地付近では 0x10000f44へジャンプするコードが見えます。この 0x10000f44 というアドレスは、__stub_helper の内容です。ここで __stub_helperの実行が行われることがわかります。
__stub_helperのやっていることはちょっと難しいので飛ばして、2回目のprintfの実行まで飛ばします。同じように 0x100000f3eのコードを見てみるとコメント部分が変わっていることに気付きます。先ほどと同じように 0x100001010 のアドレスの中身を見てみます。前回見たときの値と違う値が格納されていることがわかります。動作が同じなので今回はこの値を使って関数を呼び出していることになります。このアドレスは何かというとこれが printf関数実体です。

__stub_helperの実行後は何らかの関数アドレスが更新され、アドレス解決されることがわかります。この元々何かのアドレス格納されているが実行後更新されて格納される場所がまさにPLT.GOTっぽいなーと思います。


Mach-OのFatバイナリ(Universal Binary) について


Universal Binaryというものがあります。これは1つのバイナリの中に2つ以上の実行体が格納されたものです。32bit版と64bit版のバイナリを1つのファイルで配布することも出来るようになるので便利なシロモノです。Windowsもこういったものを採用してほしかったと思います。

この形式の場合次のようなヘッダがファイル先頭に付きます。

構造としては fat_headerが出現し、nfat_archの個数分だけfat_arch構造体が後続します。fat_arch構造体にあるoffsetの位置からmach_headerやmach_header_64が出現します。

手元で試してみたところ、ファイルマジックは FAT_CIGAM のほうで出現し、この構造体の他のメンバについてはエンディアン反転する必要がありました。後続のmachヘッダ部分でのMH_MAGIC/MH_MAGIC_64についてはそのままだったのでひっくり返す必要はありませんでした。
この差がちょっと不思議な感じです。

今までのコードはマジック値をそのままチェックしていましたが、今回の件でわかるようにエンディアンが逆の場合でも読めるように直さないといけないなと感じました。


Mach-O セグメントとセクション補足


dysymtab_commandの中身を調べていたのですが、よくわからない感じだったので後回しにすることにしました。今回はセグメントやセクションの中身の方を調べていきたいとおもいます。

現在すでにわかっているセグメント&セクションの情報はこんな感じでした。

  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
      section[1] : __TEXT [__stubs]
      section[2] : __TEXT [__stub_helper]
      section[3] : __TEXT [__cstring]
      section[4] : __TEXT [__unwind_info]
      section[5] : __TEXT [__eh_frame]
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
      section[1] : __DATA [__la_symbol_ptr]
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0

セグメントについて

今回の例ではセグメントとして以下のものが含まれていました。

  • __PAGEZERO
  • __TEXT
  • __DATA
  • __LINKEDIT

__TEXTセグメントは実行コードと読み込み専用のデータが配置される領域です。__DATAセグメントは読み書き可能なデータが配置される領域です。
一方初めて見るような他のセグメントを調べてみると次のようになっていました。__PAGEZEROセグメントはメモリ1ページを使用し、仮想メモリのゼロ番地となるように配置されるセグメントです。__LINKEDITセグメントはシンボル、文字列、リロケーションテーブルなどがダイナミックリンカが使用する情報が配置されるセグメントです。

セクションについて

今回の例では以下のセクションが含まれていました。

  • __text
  • __stubs
  • __stub_helper
  • __cstring
  • __unwind_info
  • __eh_frame
  • __nl_symbol_ptr
  • __la_symbol_ptr

__text は実行可能なプログラムコードが格納されているセクションです。
__cstringは文字定数が格納されているセクションです。

__nl_symbol_ptrはNon-lazy symbol pointersの意らしく、__la_symbol_ptrはLazy symbol pointers の意らしいです。従来は __IMPORTセグメント__jump_tableというものがあったようですが、最近は__la_symbol_ptrに変化しているとかいう話です。

今回のサンプルのバイナリデータでは含まれておらず表示されませんでしたが、初期化済みデータが格納される__dataセクションや、未初期化のスタティック変数領域としての__bssセクションといったものもMach-Oバイナリの中に存在します。

セクションの情報の中にはそのセクションの種別&属性を設定しているフラグが存在します。これを表示するようにしてみたものが以下となります。

  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
                 : addr=0x100000f10, offset=3856, size=45
                 : align=16, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=80000400 > S_REGULAR S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
      section[1] : __TEXT [__stubs]
                 : addr=0x100000f3e, offset=3902, size=6
                 : align=2, reloff=0, nreloc=0
                 : resv0=0, resv1=6, resv2=0
                 : flags=80000408 > S_SYMBOL_STUBS S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
      section[2] : __TEXT [__stub_helper]
                 : addr=0x100000f44, offset=3908, size=26
                 : align=4, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=80000400 > S_REGULAR S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
      section[3] : __TEXT [__cstring]
                 : addr=0x100000f5e, offset=3934, size=13
                 : align=1, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=00000002 > S_CSTRING_LITERALS
      section[4] : __TEXT [__unwind_info]
                 : addr=0x100000f6b, offset=3947, size=72
                 : align=1, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=00000000 > S_REGULAR
      section[5] : __TEXT [__eh_frame]
                 : addr=0x100000fb8, offset=4024, size=64
                 : align=8, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=00000000 > S_REGULAR
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
                 : addr=0x100001000, offset=4096, size=16
                 : align=8, reloff=0, nreloc=0
                 : resv0=1, resv1=0, resv2=0
                 : flags=00000006 > S_NON_LAZY_SYMBOL_POINTERS
      section[1] : __DATA [__la_symbol_ptr]
                 : addr=0x100001010, offset=4112, size=8
                 : align=8, reloff=0, nreloc=0
                 : resv0=3, resv1=0, resv2=0
                 : flags=00000007 > S_LAZY_SYMBOL_POINTERS

Mach-O 実行体のロードコマンドの情報を表示してみる


前回は概要とセクション情報表示程度だったので、今回はもうすこしロードコマンドの表示をすすめて見ようと思います。

ロードコマンドの種別によって、そのコマンドのサイズと型が決まります。
あとはこれに応じてメンバの表示を行ってみたものが下記の結果です。
ロードコマンドの型の準備と表示が面倒なだけなので、プログラムコードは割愛します。

Mach-O 64bit
  CPU TYPE      : x86_x64
  CPU SUB TYPE  : i386 ALL(Lib64)
  FileType      : MH_EXECUTE
  Cmd(s)        : 16
  CmdSize       : 1296
  Flag(s)       : MH_NOUNDEFS MH_TWOLEVEL MH_PIE (200085)
Load Command(s)
  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
      section[1] : __TEXT [__stubs]
      section[2] : __TEXT [__stub_helper]
      section[3] : __TEXT [__cstring]
      section[4] : __TEXT [__unwind_info]
      section[5] : __TEXT [__eh_frame]
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
      section[1] : __DATA [__la_symbol_ptr]
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0
  [ 4] : LC_DYLD_INFO_ONLY
    rebaseOff=8192
    rebaseSize=8
    bindOff=8200
    bindSize=24
    weakBindOff=0
    weakBindSize=0
    lazyBindOff=8224
    lazyBindSize=16
    exportOff=8240
    exportSize=48
  [ 5] : LC_SYMTAB
    symOff=8360
    nSyms=4
    strOff=8440
    strSize=56
  [ 6] : LC_DYSYMTAB
    ilocalSym=0
    nLocalSym=0
    iExtdefSym=0
    nExtdefSym=2
    iUndefSym=2
    nUndefSym=2
    tocOff=0
    nToc=0
    modtabOff=0
    nModtab=0
    extrefsymOff=0
    nExtrefSyms=0
    indirectSymOff=8424
    nIndirectSyms=4
    extrelOff=0
    nExtrel=0
    locrelOff=0
    nlocrel=0
  [ 7] : LC_LOAD_DYLINKER
    name: /usr/lib/dyld
  [ 8] : LC_UUID
    uuid: 69016DA8-F21836CE-98122CF8-DDD270E2
  [ 9] : LC_VERSION_MIN_MACOSX
    version = 10.8.0
  [10] : LC_SOURCE_VERSION
    version=0.0.0.0
  [11] : LC_MAIN
    entryOff=3856
    stackSize=0
  [12] : LC_LOAD_DYLIB
    /usr/lib/libSystem.B.dylib
  [13] : LC_FUNCTION_STARTS
    dataOff=8288
    dataSize=8
  [14] : LC_DATA_IN_CODE
    dataOff=8296
    dataSize=0
  [15] : LC_DYLIB_CODE_SIGN_DRS
    dataOff=8296
    dataSize=64

セグメントやセクション以外にも色々と興味深いものがあります。
”otool -L”コマンドで表示される依存関係は LC_LOAD_DYLIB の情報を表示していることがわかります(予想できます)。上記の物は若干otoolの結果と違うので見直してみます。

LC_LOAD_DYLIBのロードコマンド構造体の中身は以下のようになっています。上記では手抜き?してnameだけ表示していました。
これをcurrent_versionやcompatibility_versionについても表示してみます。

これによりバージョンの情報が出るようになって、より結果が期待するものに近づきました。

  [12] : LC_LOAD_DYLIB
    /usr/lib/libSystem.B.dylib (compatibility ver 1.0.0, current ver 169.3.0)

シンボルテーブルの中身表示

まずはLC_SYMTABの中身を表示してみます。このロードコマンドの構造体は以下のようになっています。

symOffやstrOffは先頭からのオフセットが入っています。そして nSyms がシンボル数を示しています。
symOffの位置からシンボルの情報が配列として格納されています。この配列要素の構造が nlist.h, stab.h にあるらしいです。Windowsなのでこの型を自前で定義すると以下のようになりました。

そしてファイルの先頭からのオフセットを足してシンボルテーブルやストリングテーブルの位置を確定させ、シンボル名を表示する部分は以下のようになってます。

これによりサンプルのプログラムでは以下のように表示されました。

---- Symbol table(s) ----
    __mh_execute_header
    _main
    _printf
    dyld_stub_binder

ここまでの関係を図示するとこんな感じになります。

macho-format-symtab

シンボル(関数とか)の位置を表示してみる

ここまででシンボルの名前情報は取得できるようになったので、次にそのシンボルがどこにあるのかを調べてみます。

シンボルに関しては nlist構造体で情報が定義されています。

nSectメンバはそのシンボルがあるセクションのインデックスです。nDescについてはシンボルの参照に関する追加情報といったところです。
大事なのは nTypeメンバで、この値によって、nValueの値が意味する内容が変化します。そしてこのnTypeが取り得る値は以下の項目のOR結合となっているようです。

これらの情報を解釈して、もうすこしシンボルに関して情報を出すようにしてみました。シンボルが示す関数が自身のものの場合にはその仮想アドレスを表示するようにしています。

64bit版も同様に解釈部分を実装して表示させると以下のようにシンボルの情報が表示されます。

Symbol table(s)
        __mh_execute_header vaddr=0x100000000   (defined at sect1)  [Public]
        _main vaddr=0x100000f10   (defined at sect1)  [Public]
        _printf  (undefined.)
        dyld_stub_binder  (undefined.)

まとめ

ロードコマンドの中身を表示することができるようになりました。そしてその際にシンボルテーブルが格納されていることがわかったので、シンボルテーブルの中身を解釈して、シンボル名やアドレス情報を確認することができるようになりました。


Mach-O形式の調査~導入編~


PEフォーマットは以前調査したので、今度はApple製品で採用されているMach-O(マークオー)形式の調査をしてみたいと思います。まずは最小サンプルの実行体を用意して既存ツールでちょっと確認してみます。

このようなサンプルを準備して、依存する他のライブラリの情報がどうなっているかを確認します。

$ otool -L sample01.out
sample01.out:
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)

$ otool -L sample02.out 
sample02.out:
  /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 56.0.0)
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)

Mach-O形式は Appleのサイトに仕様がきちんと公開されているのでこれを見ればよいです(Mac OS X ABI Mach-O File Format Reference)。
この情報を元に自分のプログラムコードで各情報を取得&表示ということをしばらくはやってみたいと思います。

Mach-O概要

Mach-O(マーク・オー, Mach object)はMacで採用されているアプリバイナリインターフェース(ABI)です。ファイルの構造としては先頭にヘッダ構造を持ち、ロードコマンド、データ(各セグメントやセクション情報)、と記録されています。

ヘッダ部分では、マジックナンバーと対象とするアーキテクチャ情報、後続のデータに関する個数などの情報を記録しています。
ロードコマンド部分では、後続するデータ部分の位置・構造についての情報を保持しています。
データ部分には、セグメントが記録され、この中にセクション情報が複数個入っています。セグメントはアプリケーションをロードする際に仮想メモリにどのように領域をマッピングするかといった情報を定義しています。ページ属性もここに含まれます。

先頭のヘッダ部分についての構造は以下となります。32bit,64bitで最初から構造体が違うので注意が必要です。

これらの構造体は loader.h にて定義されています。
この cputypeメンバが CPU_TYPE_I386 や CPU_TYPE_x86_64 は Intel Macを示すとのことです。そもそも 32bit/64bitでは magicメンバがすでに違うようで、MH_MAGIC(0xfeedface) と MH_MAGIC_64(0xfeedfacf) とマジックが異なっていました。

この後続くロードコマンド部分は load_command構造体を先頭とするデータブロックの配列として記録されています。

このcmdメンバの値に応じて後続するデータの意味が変わり、ブロックのサイズとしては cmdSize が示す物となっています。cmdの指す内容は、 LC_SEGMENT, LC_SEGMENT_64, LC_SYMTAB,… と多岐にわたります。これについては公式リファレンスの Table 4 を参照して下さい。

mach_headerの部分を読み込んで表示してみるプログラムを作りました。
実行するとこのようになります。

Mach-O 64bit
  CPU TYPE      : x86_x64
  CPU SUB TYPE  : i386 ALL(Lib64)
  FileType      : MH_EXECUTE
  Cmd(s)        : 16
  CmdSize       : 1296
  Flag(s)       : MH_NOUNDEFS MH_TWOLEVEL MH_PIE (200085)

このプログラムはWindows(VisualStudio2012)で作成したので、定義を自前で行っているためコードが長めです。Mac環境ならば、loader.hやmach/machine.h のヘッダインクルードですむところが多いです。

このヘッダの後にはロードコマンドが続きます。とりあえずロードコマンドの中身のうち、後続のデータであるセクションに関する物(LC_SEGMENT,LC_SEGMENT_64)だけ表示して構造を追いかけてみます。

プログラムコードは以下のようになります。

実行結果

Load Command(s)
  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0
  [ 4] : 0x80000022, 48
  [ 5] : 0x2, 24
  [ 6] : 0xb, 80
  [ 7] : 0xe, 32
  [ 8] : 0x1b, 24
  [ 9] : 0x24, 16
  [10] : 0x2a, 16
  [11] : 0x80000028, 24
  [12] : 0xc, 56
  [13] : 0x26, 16
  [14] : 0x29, 16
  [15] : 0x2b, 16

表示させてみてわかったことは LoadCommandのセグメント種が出現した後には、さらにセクション用のデータが続く場合があるということです。これも解釈して表示するようにしてみたところ以下の結果となりました。

Load Command(s)
  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
      section[1] : __TEXT [__stubs]
      section[2] : __TEXT [__stub_helper]
      section[3] : __TEXT [__cstring]
      section[4] : __TEXT [__unwind_info]
      section[5] : __TEXT [__eh_frame]
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
      section[1] : __DATA [__la_symbol_ptr]
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0

このセクション用のデータとは以下の構造体です.
2014/05現在、Appleのリファレンスに記載してある section_64構造体が間違っています。下記のものが正しいです(ヘッダでもこうなっているので、ドキュメントのミスでしょう)

まとめ

PEフォーマットと比べるとシンプルな構造のように思えます。ここまでの関連性について図示してみるとこうなりました。ロードコマンドの部分もいわゆるチャンク構造のようになっています。このMach-o形式は実行体だけでなく中間形式でも使われているようです。

macho-format