Shared Libraryを用いたSystemCallのHook ******************************************** dlsym/dlopenを用いた動的なSharedLibraryのLoad ==================================================== Contents: .. toctree:: :glob: :maxdepth: 1 注意 ----- :: undefined symbol: dlsym と怒られる場合は、コンパイル時に-ldlを入れてください。 概要 --------- LD_PRELOADを利用すると、shared libの関数をhookできる。 ただし、本来の関数名を他のアドレスに向けてしまうため、処理後に本来の関数をCALLするには、hookした関数内から元の関数を呼び出す必要がある。 この手法を実現するにあたって必要な要素となる、dlsymを使った動的なSharedLibraryのLoad方法について述べる。 基本(objとしてのコンパイル) ------------------------------- コード ^^^^^^^^^ main.c .. code-block:: c :linenos: #include "lib.h" int main() { hoge(); } lib.c .. code-block:: c :linenos: #include int static_value = 100; int hoge(void) { printf("hoge%d\n", static_value); } lib.h .. code-block:: c :linenos: int hoge(void); コンパイル ^^^^^^^^^^^^^ :: 実行例 $gcc -c lib.c $gcc -o a.out main.c lib.o $ldd a.out a.out: libc.so.7 => /lib/libc.so.7 (0x2807d000) これは静的に一つのバイナリにライブラリを組み込んでいるもっとも基本的なパターンです。 基本(共有libとしてのコンパイル) --------------------------------- soとしてのコンパイル :: gcc -o lib.so -shared lib.c archiveのライブラリにしたいのなら、 :: ar r lib.a lib.o gcc main.c lib.a lib.soはshared libとして作成される。lddしようとすると、 :: ldd lib.so lib.so: ldd: lib.so: Shared object "lib.so" not found, required by "ldd" lib.so: exit status 1 ので、 :: LD_LIBRARY_PATH=. ldd lib.so lib.so: libc.so.7 => /lib/libc.so.7 (0x28089000) とするとみえる。(何故、LDのLIBに解析対象のsoのdirが入っていないといけないのかは要調査) 実行 ^^^^^^^^^ :: gcc -o a.out main.c lib.so LD_LIBRARY_PATH=. ./a.out hoge100 ここで、lib.cのprintfの中身をhogehogeに変えると、 :: gcc -o lib.so -shared lib.c LD_LIBRARY_PATH=. ./a.out hogehoge100 となる。ちゃんと、動的にlib.soを呼びに行っている。 プログラム内からのsharedlibの読み込み ---------------------------------------- ここまでは、Linuxにおける、libの説明であった。ここから本題。 main_load.c .. code-block:: c :linenos: #include #include int main() { void *dl_handle; int (*func) (void); dl_handle = dlopen("./lib.so", RTLD_LAZY); func = dlsym(dl_handle, "hoge"); (*func)(); dlclose(dl_handle); } dl_handleは指定されたsoを開く。RTLD_LAZYはman参照。 OR演算でいくつかのオプションを付けることができるのだけど、dlsymは第二引数で指定したシンボルのアドレスを返す。シンボルなので、関数へのポインタでなくてよい。 .. code-block:: c :linenos: #include #include int main() { void *dl_handle; int *value; dl_handle = dlopen("./lib.so", RTLD_LAZY); value = dlsym(dl_handle, "static_value"); printf("%d\n", *value); dlclose(dl_handle); } :実行結果: :: ./a.out 100 と参照できる。これは、nmなどで参照すると、 :: nm lib.so 00001698 D static_value というようにデータセクションのシンボルとして登録されていることがわかる。データセクションなので、書き込み可能で、 .. code-block:: c :linenos: #include #include int main() { void *dl_handle; int (*func) (void); int (*value); dl_handle = dlopen("./lib.so", RTLD_LAZY); func = dlsym(dl_handle, "hoge"); value = dlsym(dl_handle, "static_value"); *value = 200; (*func)(); dlclose(dl_handle); } とすると、 :: hogehoge200 となる。面白い。 余談 ------ :: ar r lib.a lib.o lib.o みたいに、同じobjを2回archiveしたらどうなるんだろうと気になった。 :: nm lib.a lib.o: 00000000 T hoge U puts lib.o: 00000000 T hoge U puts 両方入る。 本来loadされるべきSharedLibraryの関数Hook =========================================== 概要 ----- FreeBSD等のELFを用いているシステムではLD_PRELOADを使用することで、 通常のshared libに先駆けて指定したshared libを読み込むことができます。 これによって、 *既存のELFバイナリの* システムコールなどに任意の処理を割り入れることができます。 今回はlibcのwrite(2)にhookをかけることを例に 見てみましょう。 LD_PRELOADの基本的な使い方 ------------------------------- 詳細はman ld.soなどで見れるRTLD(1)に記載されています。 まずは、システムコールを「上書き」する例を見てみましょう。 コード ^^^^^^ *write_hook_override.c* .. code-block:: c :linenos: #include size_t write(int d, const void *buf, size_t nbytes) { printf("write called.\n"); } *write_hook_test.c* .. code-block:: c :linenos: main() { write(0, "hoge\n", 5); } 実行 ^^^^^^ :: $gcc write_hook_test.c $ gcc -shared -o write_hook.so write_hook_override.c $ ./a.out hoge // ここで、環境変数にLD_PRELOADを設定して再度実行します $ LD_PRELOAD=./write_hook.so ./a.out write was called. a.outの起動時にwrite_hook.soのwriteシンボルが先にloadされ実行されました。 libc.soのwrite(2)がloadされていないことが分かります。 hook後に従来の動作をおこなう -------------------------------------- この例では、通常のwrite(2)の動作では、 write(0, "hoge\n", 5); は"hoge"と出力されることが期待されますが、 write_hook.soのwriteで関数が上書きされてしまい、 本来の動作が行われません。 RTLD_NEXTを用いて元の処理に戻してみましょう。 コード ^^^^^^ *write_hook.c* .. code-block:: c :linenos: #include #include ssize_t write(int d, const void *buf, size_t nbytes) { void *dl_handle; int (*o_write) (int d, const void *buf, size_t nbytes); o_write = dlsym(RTLD_NEXT, "write"); printf("write was called.\n"); return(o_write(d, buf, nbytes)); } 実行 ^^^^ :: $ gcc -shared -o write_hook.so write_hook.c $ LD_PRELOAD=./write_hook.so ./a.out write was called. hoge dlsym(RTLD_NEXT, ); は :: Thus, if the function is called from the main program, all the shared libraries are searched. の通り、の動作を行われるため、結果として、 通常loadされるべきshared libからを検索して、ポインタを返します。 sudo時の注意 ------------------- 最近のsudoは通常env_resetがおこなわれるため、注意が必要です。 :: $ LD_PRELOAD=./write_hook.so sudo ./a.out hoge $ sudo LD_PRELOAD=./write_hook.so ./a.out write was called. hoge のように順番に気をつけてください。 メモ --------- OSXの場合はDYLD_INSERT_LIBRARIESを使う(elf以外の仕様について調査)