Linux でLD_PRELOAD
環境変数と共有ライブラリを使うと、標準ライブラリの関数の前後に処理を挟んだり、処理を上書きしたりする簡易アスペクト指向のようなことができます。
† 簡単なターゲットプログラム
今回ターゲットにする関数はみんな大好きprintf()
。
今日はこんな感じのプログラムを用意しました。
helloworld.c
これをコンパイルして実行すると、こんな感じの実行結果になります。
今回の目的はこの helloworld をリコンパイルせずにprintf()
の挙動を変更することにあります。
† printf() は何者なのか?
今回はプログラムの挙動を変更するために、同じ名前で同じ引数(つまり同じシグネチャ)を持った関数を作って、プログラムの関数呼び出しを横取り(いわゆる、フック)するという方法を取ります。このため、あらかじめ対象となる関数のシグネチャを取得しておく必要があります。
今回の対象はprintf()
ですから、Linux であれば、printf()
の定義は/usr/include/stdio.h に入っているはずです。
ファイルの中からprintf()
を探すと以下の行が見つかります。
† 関数呼び出しをフックするとは
前述のとおり、今回は関数呼び出しをフックすることで関数の動作を変更します。
このため、ある程度の C 言語の知識と Linux の動的リンカー/ローダーであるld.so
*1 の仕組みについての知識が必要になります。おまじないのように言われることが多いLD_PRELOAD
環境変数はld.so
の動作を変更するものです。
動的リンカー/ローダーがどのライブラリをロードするのかについてはldd
というコマンドで確かめることができます。
例えば、先ほどのhelloworld
をldd
してみると以下のような出力が得られます。
以下のようにnm
を使ってシンボルを取り出してみるとprintf()
はlibc.so.6
の中に入っていることが確認できます。
また、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
プログラムのポイントは見慣れないdlsym()
という関数。
Man page of DLOPEN
関数 dlsym() は、 dlopen() が返した動的ライブラリの「ハンドル」と、 NULL 終端されたシンボル名の文字列を引き数に取り、 そのシンボルがロードされたメモリーのアドレスを返す。
シンボルがロードされたメモリーのアドレスという記述だとちょっとピンと来づらいですが、要は ld.so にロードされた関数への関数ポインタが取れるということです。これを使ってオリジナルのlibc.so
側のprintf()
にアクセスすることになります。hook.c
は以下のコマンドでコンパイルすることができ、成功するとhook.so
というファイルが得られます。
生成されたhook.so
をnm
でダンプすると、自分で定義したprintf
やputs
というシンボルが含まれていることが分かります。
† 実際に動作させてみる
作成したhook.so
と helloworld は以下のように起動させて、動作確認することができます。printf()
とputs()
の前後に処理が追加されているのが分かります。
この状態のldd
を確認すると、以下のようにlibc.so.6
よりも上に./hook.so
が来るのが確認できます。
つまりLD_PRELOAD
環境変数によってライブラリの優先順位が変わり、こちらに入っているprintf()
が優先的に呼び出されることになるわけです。
ちょっと調べてみるだけという興味本位で始めたことでしたが、予想以上に勉強になりました。
† 参考
printf()
がputs()
に置き換えられてしまうため。参考:LD_PRELOADでprintfを後から差し替える - QiitaこのエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/11910
コメントは承認後の表示となります。
OpenIDでログインすると、即時に公開されます。
OpenID を使ってログインすることができます。