BLOGTIMES
2020/08/14

LD_PRELOAD で標準ライブラリの関数の挙動を変更する

  c  linux  softwareengineering 
このエントリーをはてなブックマークに追加

Linux で LD_PRELOAD 環境変数と共有ライブラリを使うと、標準ライブラリの関数の前後に処理を挟んだり、処理を上書きしたりする簡易アスペクト指向のようなことができます。

簡単なターゲットプログラム

今回ターゲットにする関数はみんな大好き printf()
今日はこんな感じのプログラムを用意しました。

helloworld.c

#include <stdio.h> int main(){ printf("Hello World!\n"); printf("%d\n", 2020); return 0; }

これをコンパイルして実行すると、こんな感じの実行結果になります。

$ gcc helloworld.c -o helloworld $ ./helloworld Hello World! 2020

今回の目的はこの helloworld をリコンパイルせずに printf() の挙動を変更することにあります。

printf() は何者なのか?

今回はプログラムの挙動を変更するために、同じ名前で同じ引数(つまり同じシグネチャ)を持った関数を作って、プログラムの関数呼び出しを横取り(いわゆる、フック)するという方法を取ります。このため、あらかじめ対象となる関数のシグネチャを取得しておく必要があります。

今回の対象は printf() ですから、Linux であれば、printf() の定義は /usr/include/stdio.h に入っているはずです。
ファイルの中から printf() を探すと以下の行が見つかります。

・・・・ /* Write formatted output to stdout. This function is a possible cancellation point and therefore not marked with __THROW. */ extern int printf (const char *__restrict __format, ...); ・・・・・

関数呼び出しをフックするとは

前述のとおり、今回は関数呼び出しをフックすることで関数の動作を変更します。
このため、ある程度の C 言語の知識と Linux の動的リンカー/ローダーである ld.so*1 の仕組みについての知識が必要になります。おまじないのように言われることが多い LD_PRELOAD 環境変数は ld.so の動作を変更するものです。

動的リンカー/ローダーがどのライブラリをロードするのかについては ldd というコマンドで確かめることができます。
例えば、先ほどの helloworld を ldd してみると以下のような出力が得られます。

$ ldd ./helloworld linux-vdso.so.1 => (0x00007ffe5d1f6000) libc.so.6 => /lib64/libc.so.6 (0x00007f9fe9b16000) /lib64/ld-linux-x86-64.so.2 (0x00007f9fe9ee4000)

以下のように nm を使ってシンボルを取り出してみると printf() は libc.so.6 の中に入っていることが確認できます。

$ nm -gD /lib64/libc.so.6 | grep -E " printf$" 0000000000053410 T printf

また、ldd で出力されるライブラリの並びには意味があり、上に表示されているものの方が優先順位が高くなっています。つまり、複数のライブラリに同じ名前のシンボルがあった場合、上のライブラリで見つかったものが優先されることになります。

したがって、 libc.so.6 よりも上の行に表示されるライブラリに printf() を仕込むことができれば、自分のプログラムから呼出す printf() の挙動が変更できるということになります。さらに、libc.so.6 の printf は上書きされて無くなってしまっているわけではなく、シンボルの検索順序の関係で見えなくなっているだけに過ぎないので、呼び出し方を工夫すれば libc.so.6 の printf() を呼び出すこともできるということになります。

逆に、今回のやり方でできるのは対象となる関数が so に含まれている場合だけで、プログラムを静的リンクされている場合には適用できません。

関数を上書きするためのライブラリを作る

今回は printf()(と puts()*2 )を上書きするための以下のプログラムを作成しました。

今回は単純に関数が呼出される前に「BEFORE 関数名」、関数が呼出された後に「AFTER 関数名」を出力するという単純な処理を追加するプログラムになっています。今回の printf は可変長引数 (...) を持っているので、オリジナルの printf() ではなく va_list に対応した vprintf() を必要があり、ちょっとハマってしまいました。

hook.c

#include <dlfcn.h> #include <stdio.h> #include <stdarg.h> typedef int (*ORIGINAL_PUTS)(const char *__s); typedef int (*ORIGINAL_PRINTF)(const char *__restrict __format, ...); int puts(const char *__s){ ORIGINAL_PUTS original_puts = (ORIGINAL_PUTS)dlsym(RTLD_NEXT, "puts"); int ret; original_puts("BEFORE puts()"); ret = original_puts(__s); original_puts("AFTER puts()"); return ret; } int printf(const char *__restrict __format, ...){ ORIGINAL_PRINTF original_printf = (ORIGINAL_PRINTF)dlsym(RTLD_NEXT, "printf"); int ret; va_list args; va_start(args, __format); original_printf("BEFORE printf()\n"); ret = vprintf(__format, args); /* printfを呼ぶと動かないので注意 */ original_printf("AFTER printf()\n"); va_end(args); return ret; }

プログラムのポイントは見慣れない dlsym() という関数。

Man page of DLOPEN

関数 dlsym() は、 dlopen() が返した動的ライブラリの「ハンドル」と、 NULL 終端されたシンボル名の文字列を引き数に取り、 そのシンボルがロードされたメモリーのアドレスを返す。

シンボルがロードされたメモリーのアドレスという記述だとちょっとピンと来づらいですが、要は ld.so にロードされた関数への関数ポインタが取れるということです。これを使ってオリジナルの libc.so 側の printf() にアクセスすることになります。

hook.c は以下のコマンドでコンパイルすることができ、成功すると hook.so というファイルが得られます。

gcc -g -Wall -D_GNU_SOURCE -fPIC -shared -o hook.so hook.c -ldl

生成された hook.so を nm でダンプすると、自分で定義した printf や puts というシンボルが含まれていることが分かります。

$ nm -gD ./hook.so w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses 0000000000201038 B __bss_start w __cxa_finalize w __gmon_start__ 0000000000201038 D _edata 0000000000201040 B _end 0000000000000878 T _fini 00000000000005d8 T _init U dlsym 0000000000000787 T printf 0000000000000735 T puts U vprintf

実際に動作させてみる

作成した hook.so と helloworld は以下のように起動させて、動作確認することができます。
printf() と puts() の前後に処理が追加されているのが分かります。

$ LD_PRELOAD=./hook.so ./helloworld BEFORE puts() Hello World! AFTER puts() BEFORE printf() 2020 AFTER printf()

この状態の ldd を確認すると、以下のようにlibc.so.6 よりも上に ./hook.so が来るのが確認できます。
つまり LD_PRELOAD 環境変数によってライブラリの優先順位が変わり、こちらに入っている printf() が優先的に呼び出されることになるわけです。

$ LD_PRELOAD=./hook.so ldd ./helloworld linux-vdso.so.1 => (0x00007ffc2bbe0000) ./hook.so (0x00007fed7ab10000) libc.so.6 => /lib64/libc.so.6 (0x00007fed7a742000) libdl.so.2 => /lib64/libdl.so.2 (0x00007fed7a53e000) /lib64/ld-linux-x86-64.so.2 (0x00007fed7ad12000)

ちょっと調べてみるだけという興味本位で始めたことでしたが、予想以上に勉強になりました。

参考


トラックバックについて
Trackback URL:
お気軽にどうぞ。トラックバック前にポリシーをお読みください。[policy]
このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/11910
Trackbacks
このエントリにトラックバックはありません
Comments
愛のあるツッコミをお気軽にどうぞ。[policy]
古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
コメントはありません
Comments Form

OpenID を使ってログインすることができます。

Identity URL: Yahoo! JAPAN IDでログイン