概要
やむを得ない理由や興味本位でLinuxのシステムコール(writeなど)に割り込みをかけたいことがあります。この記事では、SharedLibについて簡単に解説した後、dlsymを使った動的なSharedLibraryのLoad方法について述べます。
また、LD_PRELOADを利用すると、shared libの関数を上書きすることができるものの、本来の関数を他の処理に向けてしまうため、本来の関数も読み出すには、hookした関数内から元の関数を呼び出す必要があります。 このテクニックについても述べます。
sharedLibについて(SKIP可)
C言語では単なる式や演算を行うほかに様々な関数を使うことができます。例えば、write(2)やprintf(3)です。これらの関数はman上で(2)で示されるOSのシステムコールか、glibcなどのC言語標準のコードです。もちろん、自分で書いたコード、あるいは、他のライブラリを使用することもできます。
C言語のソースコードはコンパイルされ、そこで書かれた複数のコードに分かれている関数はリンクされ、実行ファイルとなります。ところで、このファイルにwrite(2)やprintf(3)自身のバイナリコードは含まれているのでしょうか?答えはNOです(静的にコンパイルすることも可能なので、必ずしもNOとは言えません)。では、どのように自分で書いたコードのバイナリがそれらの関数を呼べているのかという答えが、共有ライブラリです。
sharedLibの多くは/var/libなどに拡張子.soで格納されているバイナリコードで、多くのプログラムは実行する直前に必要なsharedLibをメモリ空間に読み込み、参照・実行します。UNIX系のOSでは、コマンドライン上からLD_PRELOADなどを用いることで、読み込まれるsharedLibを制御することが可能です。これにより、同一のバイナリコードから読み込まれるプログラムやデータを変更することができ、操作を変更(何らかの割り込みをいれるなど)が可能になります。
0: 複数ファイルに分かれた関数の利用
Cでは以下のようにソースファイルを分けて関数を書き、コンパイルすることが可能です。
# include "lib.h"
int main()
{
hoge();
}
# include <stdio.h>
int static_value = 100;
int hoge(void)
{
printf("hoge%d\n", static_value);
}
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)
./a.out
# hoge100
main()
内のhoge()
が呼ばれました。ポイントは、lib.c
のhoge()
ではローカルのソースコードで定義したstatic_valueをきちんと参照できていることです。
1: SharedLibを使う
0章は静的にライブラリを埋め込んだ例といえます。この章では、hoge()を共有ライブラリとして使用します。
# sharedLib lib.soを作成
gcc -fPIC -o lib.so -shared lib.c
# sharedLib lib.soを参照しながらa.outを作成
gcc -o a.out main.c lib.so
# そのまま実行しようとするとlib.soがないといわれる
./a.out
# error while loading shared libraries: lib.so
# sharedLibの存在するディレクトリを指定
LD_LIBRARY_PATH=. ./a.out
# hoge100
一見、先ほどと変わらないように見えます(実際、出力は同じです)。ここで、sharedLibの中身を書き換えてみます。先ほど作ったlib.soを消し、別のソースコードからlib.soを再生成します。
# include <stdio.h>
int static_value = 1000;
int hoge(void)
{
printf("This is LIB2! %d\n", static_value);
}
rm lib.so
gcc -fPIC -o lib.so -shared lib2.c
LD_LIBRARY_PATH=. ./a.out
# This is LIB2! 1000
ポイントは、main.cを再度コンパイルをしていないのに、hoge()で実行(出力)される内容が変わったことです。sharedLibはコンパイル時ではなく、実行時にso(shared object)を読み込みます。このため、本体のコードや他のライブラリコードに影響を与えることなく(すべてをリンクやコンパイルしなおす必要なく)、一部のライブラリのみを更新することができます。
言い換えると、既にコンパイルされた実行ファイルを更新することなく、プログラムの動作を変更できます。
2: プログラム内からshareLibのシンボルを読み込み、書き換える
sharedLibは実行時にその変数や関数をシンボルとして、プログラム側に読み込まれて認識されます。これは参照可能であり、編集可能です。
# include "lib.h"
# include <stdio.h>
# include <dlfcn.h>
int main()
{
void *dl_handle;
int *value;
int (*func) (void);
// sharedLibをdl_handleとして開く
dl_handle = dlopen("./lib.so", RTLD_NOW);
// "hoge()"をfuncとして読み込む(関数へのポインタが返される)
func = dlsym(dl_handle, "hoge");
// 変数static_valueをvalueとして読み込む(変数へのポインタが返される)
value = dlsym(dl_handle, "static_value");
// sharedLibにあるはずの値を直接読み込む!
printf("value is %d\n", *value);
hoge();
// sharedLib内の値を直接書き換える
*value = 200;
// hoge()と同値。soの関数を直接読んでいる
(*func)();
dlclose(dl_handle);
}
rm lib.so
gcc -fPIC -o lib.so -shared lib.c
gcc main_load.c lib.so -ldl
LD_LIBRARY_PATH=. ./a.out
# value is 100 > 直接値が読めている
# hoge100 > これは普通の実行
# hoge200 > so内の変数を書き換えたうえで、関数を直接読んでいる
挙動は非常にシンプルです。このプログラムは、本来、ソースコード上で触れる必要のない(通常は、コンパイル時に指定するから)lib.soをdlsym(3)で読み込んでいます。そして、本来、気にするはずのない、static_valueを参照し、表示しています。次に、通常通り、hoge()を実行し、期待する出力を得ます。
そして、共有ライブラリ内に埋め込まれているstaticな変数static_valueを200に変更し、再度、hoge()をこれも関数へのポインタを読み出す形で実行しています。
まず、soに含まれるシンボルを調べます。これには、nm - list symbols from object files
を使います。
[kanai@www:34582]nm lib.so | egrep (hoge|static_value)
00000000000006b0 T hoge
0000000000201028 D static_value
T:text section
, D:data section
を意味します。lib.soからそれぞれのシンボルを知ることができます。
sharedLibは実行時に読み込まれて、各シンボルのコードを適切なエリアに配置します。つまり、hoge()はテキストセクションに配置され、static_valueはデータセクションに配置されます。dlsymは開いているsoのシンボルに対応するアドレスを返します。これにより、テキストセクションのfuncは実行することができ、データセクションのvalueは参照して書き換えることができました。
4: 本来loadされるべきSharedLibraryの関数Hook
この記事の本題です。LD_PRELOADを使用することで、 通常のsharedLibに先駆けて、指定したsharedLibを読み込むことができます。 後述しますが先駆けて
というのがポイントで、あとで本来のsharedLibも読み込まれます。これによって、 既存のバイナリのシステムコールに任意の処理を割り入れることができます。
4-1: systemCallのoverride
# include <stdio.h>
size_t write(int d, const void *buf, size_t nbytes)
{
printf("write called.\n");
}
# include <unistd.h>
int main()
{
write(0, "hoge\n", 5);
}
では、実行します。write_hook_testを実行した後、それにLD_PRELOAD=./write_hook.so
をつけて実行してみます。
gcc write_hook_test.c
gcc -shared -o write_hook.so write_hook_override.c
./a.out
# hoge
LD_PRELOAD=./write_hook.so ./a.out
# write called. > 出力が変わった!
LD_PRELOADにより、write_hook.soが読み込まれ、writeコマンドがwrite_hook_overrideのwriteに置き換えられてしまったことが分かります!
4-2: systemCallに割り込む
さて、システムコールを上書きすることができました。しかし、これでは、write_hook.soのwriteで関数が上書きされてしまい、 本来の動作が行われません。(つまり、hogeが出力されません)
ここで、RTLD_NEXTを用いて元の処理に戻してみます。
# include <stdio.h>
# define __USE_GNU
# include <dlfcn.h>
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_next.so write_hook_next.c -ldl
# write was called.
# hoge
dlsym(RTLD_NEXT,...)
は
Thus, if the function is called from the main program, all the shared libraries are searched.
の通り、結果として 通常loadされるべきsharedLibからを検索して、そのポインタを返します。
このように、LD_PRELOADを行うことで既に存在するバイナリの挙動を変えることができました。適切な範囲での活用をしていきましょう!
もっと気になる人は man 8 ld.soをしてみてください。
付録
a. OSXの場合
LD_PRELOADがありません。DYLD_INSERT_LIBRARIESを使ってください
b. arの利用
ar を使うとsoも1つのアーカイブに固めることができます。
ar r lib.a lib.o lib.o
nm lib.a
lib.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T hoge
U printf
0000000000000000 D static_value
lib.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T hoge
U printf
0000000000000000 D static_value
c.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
d.静的なコードに割り込めるのか?
例えば、以下のコードがあります。
# include <stdio.h>
int static_value = 100;
int hoge(void)
{
printf("hoge%d\n", static_value);
}
main(){ hoge(); }
このhogeをLD_PRELOAD
で割り込めるでしょうか?結論から言えば無理です。LD_PRELOADはあくまで、実行時(の準備の)sharedLibのシンボル探索に割り込みます。このため、静的な関数は割り込むことができません。(コンパイルされたときに決定されているテキストエリアのコードにjmpするから)