unoh.github.com

LD_PRELOADを使って任意の関数呼び出しにフックしてみる

2008-04-06 15:12:58 +0000

尾藤正人(a.k.a BTO)です

先日の社内勉強会のLTでLD_PRELOADについて簡単にやってみました。

LD_PRELOADって?



環境変数$LD_PRELOADを使うと他のライブラリの読み込みの前に任意のライブラリを先に読み込ませることができます。
実行プログラムの形式にELF形式を採用しているOSで使うことができます。
Linuxであれば問題なく使用できるはずです。

何ができるのか



プログラムを変更することなく、任意の関数を上書きしたり、任意の関数にフックすることができます。

libhookwriteを作ってみた



簡単なサンプルプログラムとしてlibhookwriteというのを作ってみました。
libhookwriteはその名の通りwrite(2)にフックをかけることができます。
といってもできることは限られていてファイルのタイムスタンプの更新か、任意のプログラムをsh経由で実行することしかできません。

ダウンロード



libhookwrite-0.0.1.tar.gz

インストール



お約束通り

tar zxvf libhookwrite-0.0.1.tar.gz
cd libhookwrite-0.0.1
./configure
make
sudo make install


rpmでもいけます。

rpmbuild -ta libhookwrite-0.0.1.tar.gz
sudo rpm -ivh libhookwrite-0.0.1-1.i386.rpm


使い方



環境変数$LD_PRELOADにlibhookwrite.soを指定します。
さらに環境変数$HOOKWRITE_UPDATE_FILEか$HOOKWRITE_COMMANDを指定します。

HOOKWRITE_UPDATE_FILEにファイル名を指定すると、write(2)が呼ばれたときに指定されたファイルのタイムスタンプを現在時刻に更新します。

HOOKWRITE_COMMANDに任意のコマンドを指定すると、write(2)が呼ばれたときに指定されたコマンドを/bin/sh経由で実行します。

例えば、次のようなシェルスクリプトcat.shを実行すると、write(2)が呼ばれた時点で$HOME/tmp/catというファイルのタイムスタンプを現在の時刻に更新して、"hoge"と出力します。

#!/bin/sh
HOOKWRITE_UPDATE_FILE=$HOME/tmp/cat ¥
  HOOKWRITE_COMMAND='echo hoge' ¥
  LD_PRELOAD=/usr/lib/libhookwrite.so ¥
  /bin/cat $@


使い道



例えばエディタがファイルを保存したときに、何らかのアクションを起こしたい場合に使えるかもしれません。
プログラムに依存しないので、vimだろうがemacsだろうが何でもいけるはず。
ただ、スワップファイルの書き出しにも反応してしまいますが。(^^;

vimを使ってsymfonyプロジェクトの開発をやってて、ファイルを保存したい瞬間にsymfony ccを走らせてキャッシュクリアしたい場合はこんな感じのaliasを作ればいいでしょうか。

alias vim="LD_PRELOAD=/usr/lib/libhookwrite.so HOOKWRITE_COMMAND='/symfony/project/symfony cc' /usr/bin/vim"


プログラム



プログラム自体は非常に簡単です。

static ssize_t (*write_org) (int fd, const void *buf, size_t count) = NULL;


write(2)と同じプロトタイプ宣言を持つ関数ポインタwrite_orgを定義しています。
オリジナルのwriteを保存するのに使います。

__attribute__((constructor))
void
_save_original_functions()
{
    write_org = (ssize_t(*)(int, const void *, size_t)) dlsym(RTLD_NEXT, "write");
}


dlsym(3)を使ってwriteのアドレスを取り出して、write_orgに保存しています。
__attribute__((constructor))というのはgccの拡張になってて、これを指定しておくとmain関数が呼ばれる前に実行してくれます。

後は単純にwrite関数を改めて定義して、その中に任意の処理を書いておきます。
最後に return write_org(fd, buf, count);でオリジナルの処理を呼び出して終わりです。

参考



man ld.soでマニュアルが読めます。
LD_PRELOADの他にもいろいろな機能がありますので、一度読んでおくといいかもしれません。

fakechroot



LD_PRELOADを使った面白いソフトウェアでfakechrootというのがあります。
chroot(2)の実行にはroot権限が必要ですが、fakechrootでは擬似的に一般ユーザ権限でchrootを実現しています。
LD_PRELOADを使って、ファイル操作に関わる部分にすべてフックをかけて擬似的にchroot環境を提供しているんでしょうね。

Mac OS Xだと



実行プログラムの形式がELFではなくMach-Oが採用されています。
なので、LD_PRELOADは使えないはず。
代わりにDYLD_INSERT_LIBRARIESというのが使えます。

man dyld


詳しく検証してないので、詳細はよくわかってません。

まとめ



LD_PRELOADを使うとプログラムに直接手を加えることなく、外から挙動をコントロールすることができます。
応用範囲は無限大です。