いわゆるBinary Hacksにあった、「オブジェクトファイルを自力でロードする」のMach-O版をやってみました。MacではオブジェクトファイルもまたMach-O形式で出力されています。
今までの記事の内容で多くのことがわかってきました。特に関数フックが実現出来るようになった今、形式が同じであるオブジェクトファイルのロード&実行はそんなに難易度の高いものではなさそうです。
Mach-Oオブジェクトのロード
まずは簡単にやってみようとおもいます。ここでは以下のデータが必要になってきます。
・symtab_command
・シンボルテーブル
・ストリングテーブル
これらの情報から、すでに関数名から関数実体を検索することが(実は)可能です。知っての通り nlist_64のメンバ n_strx がストリングテーブルのオフセットを示しているので、シンボルテーブル全てのnlistを辿って、名前一致で検索すれば関数が存在していれば発見することが出来ます。
このときの n_value が、そのシンボルが存在するセクションの先頭からのオフセットとなっています。実際には、ロードしたオブジェクトファイル先頭+該当セクションのoffsetメンバ+n_value で関数の所在が計算できます。
求まった場所でそのまま実行できるか、というとメモリ保護の問題があるので mprotectで実行属性を付与します。実行属性さえつけてしまったら、多くの場合はこれで実行が可能です。
ここまでの内容を擬似コードで示すと以下のようになるかなと思います。
void* findObjFunction( const char* name ) { mach_header_64* mh; // object mach-o 先頭. symtab_command* symtab = (symtab_command*)getLoadCommand(mh, LC_SYMTAB ); nlist_64* nlist = (nlist_64*)( (uint8_t*)obj + symtab->symoff ); strtab = obj + symtab->stroff; section_64* sections = NULL; load_command* lc = (load_command*)p; for( int i = 0; i < mh->ncmds; ++i ) { if( lc->cmd == LC_SEGMENT_64 ) { sections = (section_64*)((uint8_t*)lc+sizeof(segment_command_64)); } lc = (load_command*)( (uint8_t*)lc + lc->cmdsize ); } void* target = NULL; for( int i = 0; i < symtab->nsyms; ++i ) { const char* sym_name = strtab+nlist[i].n_un.n_strx; if( strcmp( sym_name, name ) == 0 ) { section_64* sect = sections + nlist[i].n_sect-1; target = (uint8_t*)obj + sect->offset + nlist[i].n_value; break; } } if( target ) { uintptr_t addr = (uintptr_t)target; addr &= ~(4096-1); mprotect( (void*)addr, 4096, PROT_READ|PROT_WRITE|PROT_EXEC); } return target; } void test() { type int (*FuncType)( int , int ); FuncType pfunc = (FuncType)findObjFunction( "_add" ); int r = pfunc( 10, 20 ); }
リロケーションについて
多くの場合は上記の方法でも問題なく動くことだと思います。ただたまにリロケーションが必要だったりするのでこの場合はちょっと厄介です。一般的なケースではグローバル変数にアクセスしている場合が該当してしまうようです。
リロケーションの情報は __textセクションの reloff メンバで示される位置に、nreloc個の情報が格納されています。これが struct relocation_info の配列となっています。この配列メンバを地道に解釈して、必要な位置の値を書き換えていくことでリロケーションが完成するようです。
多くの場合PC相対アドレスをジャンプ先として埋め込んでいくとか変数参照先としているような感じです。そのため、はめ込み元とアクセス先のアドレスを計算して差分を埋め込む、という形になります。