LD_PRELOADを使って、GNU/Linuxに含まれる(ほぼ)全てのコマンドで、Brainf*ckを実行可能にしてみます。
LD_PRELOADとは
Linuxでは、同じ名前の関数(などのシンボル)を持つ複数のライブラリがダイナミックリンクされている場合、最初に見つかった方が使われる、ということになっています。この性質を利用して、無理やりユーザが指定したライブラリを最初に読み込ませておき、システム標準のライブラリ(libc,libX等々)の関数を、自作するなどした別の関数に置き換えてしまう機能がLD_PRELOADです。
この機能の応用例としては、X Window関数の呼び出しをフックし、関数呼び出しをトレースするものや、一つのX関数呼び出しを二つのウィンドウに対して同時に呼び出すことで、同じ内容のウィンドウを二つ表示する xmultiwin といった物が作られているようです(マルチモニタ使用時に両方のモニタにウィンドウを表示させるのに便利らしい)。
この機能を利用するには、環境変数のLD_PRELOAD,もしくは /etc/ld.so.preload 内に先に読み込ませたいライブラリのパスを書いておけばOKです。
詳しくは、「BINARY HACK」の「60. LD_PRELOADで共有ライブラリを差し換える」および「61. LD_PRELOAD で既存の関数をラップする」が参考になります。
試してみる
以前、以下の記事では、SystemTapを使ってLinuxカーネル内のシステムコールのレベルで動作を乗っ取っていました。
SystemTapでBrainf*ckを実装してみる - Okiraku Programming
これは、拡張子が.bfのファイルを読み込んだ際に、ファイルの内容をBrainf*ckソースとみなして実行させ、その実行結果をアプリケーションに返すというモノです。
今回は、同じことをLD_PRELOADを使ってやってみます。違いとしては
- 気軽&簡単に試せる
- Hackがユーザランドで完結する
- root権限も不要
- バグってもカーネルごと落ちたりしないので、キワドさ半減…
- でもスタティックリンクされているバイナリには対応できない
という辺りでしょうか。
本エントリ末尾のコードを hook_brainfuck.c として保存し、
% gcc -g -fPIC -shared -o hook_brainfuck.so -ldl hook_brainfuck.c
のようにしてライブラリをコンパイルします。そして、
% cat hello.bf
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.
[-]>++++++++[<++++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.
+++.------.--------.[-]>++++++++[<++++>-]<+.[-]++++++++++.
という感じのファイルを作り、先ほど作ったライブラリを以下のようにLD_PRELOADを指定(bash,zshの場合)した上で読み込むと、
% export LD_PRELOAD=./hook_brainfuck.so
% ./cat hello.bf
**BF** target fd: 3
**BF** execute [185]
**BF** done [13]
Hello World!
あら不思議。Brainf*ckとして実行した結果が読み出されましたね。
(**BF**というのは動作確認用のメッセージです。)
id:shinichiro_hさんのcalも実行できますよ(下記ページのソースの後ろに「:2009 01」を追記して実行しています)。
http://d.hatena.ne.jp/shinichiro_h/20070811
$ cat cal.bf
**BF** target fd: 3
**BF** execute [2041]
**BF** done [105]
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
なお、unset LD_PRELOADすれば元に戻ります。
コード
Brainf*ckインタプリタは前書いたモノの流用です。例によって入力を受け取るのが面倒なので、ソース中に「:」区切りで入力を書き添えるという仕様になっています。
gcc拡張の __attribute__( (constructor) ) を指定することで、対象プログラム起動時に__get_origs()が実行されます。ここでオリジナルのopen(2)/read(2)を保存しています。
対象プログラムがopenを呼ぶと、下記ソース中のopenが呼ばれるため、そこからオリジナルのopen(2)を呼び直しています。このときファイル名が条件を満たせばbrainf*ck実行対象としてマークしています。
なおopen(2)の実体は環境によって open, open64 の二つが有ったり無かったりするので、若干変更が必要かもしれません(下記では両方ともフックしています)。
なお、dlsym(3)はlibdlの関数で、システム本来のopen(2)/read(2)関数を取得するために使用しています。この辺がポイント。
RTLD_NEXTは自分(現在のライブラリ)の次のライブラリから関数を取得してこいという意味になります。詳しくはmanやBINARY HACK 61節を見てください。
以下コード。 http://codepad.org/7vkAqBWt
続きを読む