hidden symbolの関数呼び出しをフックする

Linuxでは一般的なライブラリ関数の呼び出しはLD_PRELOADを使ってフックして、別の動作をさせる(オーバーライドする)ことができます。
(参考: LD_PRELOADでBrainf*ck - Okiraku Programming


しかし、フックできる関数には条件があって、PLT(procedure linkage table)と呼ばれるテーブルを経由して呼び出されているものに限られます。
ライブラリによってはhidden symbolといって、同一ライブラリ内での関数呼び出しがstatic linkになっている場合があり、こうしたものはフック対象外になります。(リンカスクリプトでそういう指定が可能になっている。)
一例としては、libcに含まれる malloc(3) 関数は内部で mmap(2) や brk(2) を呼び出しますが、こうしたライブラリ内部から自身の関数呼び出しする場合は一部static linkとなっておりPLTを経由しないので、LD_PRELOADではオーバーライドできません。
(普通にプログラムが mmap(2) や brk(2) を読んでいる場合にはPLT経由になるのでフック可能です。)


こうしたstaticな関数を元のプログラムやライブラリを改変せずにオーバーライドしたい場合があります。
(例えば他のベンダが作っているライブラリの動作がマズいのだが既にメンテされてなくてソースもない場合とか)
そんなときは、無理やりコードの一部を書き換えてフックを挿入してしまうという手があります。


具体的には、jmp命令をフックしたいライブラリ関数の先頭に埋め込み、LD_PRELOADで挿入したフック用のライブラリの関数に飛ばしてしまえばOK。
シングルスレッド限定ではありますが、フック内部で埋め込んだjmp命令を元の命令列に戻してから元の関数を呼び出せば、前処理/後処理をすることも可能です。


いくつかの関数をフックするために何度も同じような処理を書くのが面倒だったため、C++とマクロでフックを簡単に埋め込めるようにする仕掛けを作ってみました。
このコードはx86_64でのみ動作します(命令列で jmp *(%rip); を埋め込んでいるため)。また、左記の命令は14byteあるため、14byte以上の関数でないと正常にオーバーライドできません(でないと隣にある関数を破壊してしまう。。)


例えばbrkをフックするコードは以下。ヘッダやMakefileを含む全体は以下のgistに。
https://gist.github.com/1519275

hook_brk.cpp

#include "LibHook.h"    // 面倒なことを全部押し込んだヘッダ
#include <stdio.h>
#include <unistd.h>

// フック関数
int brk_hook(void *addr)
{
	int ret = brk(addr);
	printf("brk(%p) => %d\n", addr, ret);
	return ret;
}
HookInstaller(brk, brk_hook);  // brk(2) を brk_hook()でオーバーライド

このように書くと、対象プログラムのmain関数の実行前(つまりconstructor実行時)に brk の先頭にjmp命令が埋め込まれ、brkを呼び出すとjmp命令を外した後に brk_hook が呼ばれるようになります。brk_hook内でbrkを呼び出しても無限ループになったりせずにオリジナルのbrkが実行されます。brk_hookからreturnすると、jmp命令が再び埋め込まれたのち、hookからの返り値がbrkの返り値であるかのように返されます。*1

これをビルドするには、

g++ -fPIC -shared -o hook_brk.so hook_brk.cpp -std=c++0x -ldl

のようにします。なおC++11 なのはフックの自動挿入/解除の記述を簡単にするためにvariadic templateを使って関数型の指定を自動でやらせているため。フック挿入自体*2とは本質的には関係ありません。


何度か malloc するプログラムを、
malloc_hook.c

#include <stdlib.h>
int main(void)
{
	free(malloc(256<<10));
	free(malloc(256<<10));
	return 0;
}

これを

gcc malloc_hook.c && LD_PRELOAD=hook_brk.so ./a.out

のようにビルド & 実行すると、

brk((nil)) => 0
brk(0x240d000) => 0

とフック関数内でprintfしているメッセージが表示され、mallocからの呼び出しでもフックできていることが確認できます。*3


なおフックは

    NSHook_brk::hook.uninstall();

とかやると途中で解除することも可能です。


一種の自己書き換えのような無茶をしているので安全とは言いがたいですが、いざとなったらこのようなことも可能だという例として。


全コード
Hook hidden symbol call (x86_64) · GitHub

ところで

C++で関数ポインタを扱うテンプレートを書く際、template引数に指定された関数型から、その返り値や引数の型をtemplate引数にもつテンプレートクラスAをインスタンス化したい場合って、一般的な方法ってあるのでしょうか。

よくわからなかったので、しかたなくダミーのtemplate関数を用意して、

// インスタンス化したいテンプレートクラス
template <class RET, class... ARGS>
class A {
};

// ダミー関数
template <class RET, class... ARGS>
A<RET,ARGS...> resolve_ftype(RET(*f)(ARGS...))
{
	throw "This function must not be called.";
	return A<RET,ARGS...>();
}

int func(const char*);

typedef decltype(resolve_ftype(func)) A_T;  // => A_T は A<int, const char*> のこと

などとしているのですが、もっといい方法はないのかな。

*1:マルチスレッドだと他スレッドが関数実行中にjmp命令で上書きする恐れがあるので不可。jmp命令の自動挿入/解除を止めれば大丈夫ですが、この場合はオリジナル関数は破壊されてしまうため、自分で1から再実装する必要があります。

*2:要するにdlopen/dlsymで関数アドレスを取得して、オリジナルの命令列を保存した後、フックへのjmp命令列を構築しmemcpyで上書き、という処理です。

*3:ちなみにフック内のprintf内でもバッファ確保のためmallocが呼ばれることがありますので、brkに再突入することがありますが、その場合はjmp命令がはずれているのでフックは呼ばれません。