- blogs:
- cles::blog

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



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()
が優先的に呼び出されることになるわけです。
ちょっと調べてみるだけという興味本位で始めたことでしたが、予想以上に勉強になりました。
† 参考
- A Simple LD_PRELOAD Tutorial, Part Two
- Dynamic linker tricks: Using LD_PRELOAD to cheat, inject features and investigate programs | Rafał Cieślak's blog
- *1: Man page of LD.SO
- *2: 引数のない
printf()
がputs()
に置き換えられてしまうため。参考:LD_PRELOADでprintfを後から差し替える - Qiita
このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/11910
古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
コメントは承認後の表示となります。
OpenIDでログインすると、即時に公開されます。
OpenID を使ってログインすることができます。
- 居酒屋 八(ひらく) (3)
- 退去通知もネットで可能な時代に (1)
- ケーブルカーのケーブルが切... (1)
- Visual Studio 2017/2019 で ... (1)
- 国土交通省オフィシャルの「... (1)
2 . Windows 10 で勝手にログアウトされないようにする(26778)
3 . Word で数式がグレーアウトされていて挿入できないときは(24670)
4 . リモートデスクトップで Alt + PrtSc と同じことをするには(20951)
5 . Visual Studio 2017/2019 で scanf() がエラー(C4996)になるときは(20399)