LD_PRELOADでBrainf*ck

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 で既存の関数をラップする」が参考になります。

Binary Hacks ―ハッカー秘伝のテクニック100選

Binary Hacks ―ハッカー秘伝のテクニック100選

試してみる

以前、以下の記事では、SystemTapを使ってLinuxカーネル内のシステムコールのレベルで動作を乗っ取っていました。
SystemTapでBrainf*ckを実装してみる - Okiraku Programming
これは、拡張子が.bfのファイルを読み込んだ際に、ファイルの内容をBrainf*ckソースとみなして実行させ、その実行結果をアプリケーションに返すというモノです。


今回は、同じことをLD_PRELOADを使ってやってみます。違いとしては

  • 気軽&簡単に試せる
  • でもスタティックリンクされているバイナリには対応できない

という辺りでしょうか。


本エントリ末尾のコードを 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

/* Brainf*ck hook for open(2)/read(2)
 *
 * compile: gcc -fPIC -shared -ldl -o hook_brainfuck.so hook_brainfuck.c
 * usage example: LD_PRELOAD=./hook_brainfuck.so cat hoge.bf
 */

#include <dlfcn.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>

#define SIZE 4096

#ifndef RTLD_NEXT
#  define RTLD_NEXT ((void *) -1L)
#endif

static int (*open_orig)(const char *, int, mode_t) = NULL;
static int (*open64_orig)(const char *, int, mode_t) = NULL;
static ssize_t (*read_orig)(int, void *, size_t) = NULL;

static int target_fd = -1;

/* Brainf*ck execution */
static unsigned char *stack[SIZE];
static unsigned char heap[SIZE];
static unsigned char result[SIZE];

static int __brainfuck_exec(unsigned char *code, size_t len, size_t bufsize)
{
	unsigned char  **sp = stack;
	unsigned char *hp = heap+SIZE/2;
	unsigned char *ip = code;
	unsigned char *inp = (unsigned char *)strstr(code, ":");
	unsigned char *inlast = code + len;
	int rp = 0;
	unsigned char *last;
	int i;

	memset(heap, 0, SIZE);
	last = inp ? inp : inlast;
	if (inp)
		inp++;
	while (last > ip) {
	//printf("%d %d  %d %d  %d %c\n", hp, *hp, inp, inp?*inp:0, ip, *ip);
		switch (*ip++) {
		case '>':	hp++;		break;
		case '<':	hp--;		break;
		case '+':	(*hp)++;	break;
		case '-':	(*hp)--;	break;
		case '.':
			result[rp++] = *hp;
			if (rp == bufsize || rp == SIZE)
				goto out;
			break;
		case ',':
			if (!inp || inp >= inlast)
				*hp = 0;
			else
				*hp = *(inp++);
			break;
		case '[':
			if (!*hp) {
				i = 1;
				do {
					if (*ip == '[')	i++;
					if (*ip == ']' && !--i) break;
					ip++;
				} while(last > ip);
				ip++;
			}
			else	*sp++ = ip-1;
			break;
		case ']':	ip = *(--sp);	break;
		}
	}
out:
	memcpy(code, result, rp);
	return rp;
}


/* utility functions for hooking */

__attribute__((constructor)) void __get_origs() 
{
	open_orig = (int(*)(const char *, int, mode_t))dlsym(RTLD_NEXT, "open");
	open64_orig = (int(*)(const char *, int, mode_t))dlsym(RTLD_NEXT, "open64");
	read_orig = (ssize_t(*)(int, void *, size_t))dlsym(RTLD_NEXT, "read");
}

static void __hook_open(int ret, const char *pathname)
{
	int len;

	// set target_fd if pathname ends with ".bf"
	if (ret < 0)
		return;
	len = strlen(pathname);
	if (len < 3 || strcmp(pathname+len-3, ".bf"))
		return;
	target_fd = ret;
	fprintf(stderr, "**BF** target fd: %d\n", target_fd);
}


/* HOOKS for glibc */

int open64(const char *pathname, int flags, ...)
{
	int len, ret;
	mode_t mode;
	va_list ap;

	// call original open64(2)
	va_start(ap, flags);
	mode = (mode_t)va_arg(ap, mode_t);
	va_end(ap);

	ret = open64_orig(pathname, flags, mode);
	__hook_open(ret, pathname);
	return ret;
}

int open(const char *pathname, int flags, ...)
{
	int len, ret;
	mode_t mode;
	va_list ap;

	// call original open(2)
	va_start(ap, flags);
	mode = (mode_t)va_arg(ap, mode_t);
	va_end(ap);

	ret = open_orig(pathname, flags, mode);
	__hook_open(ret, pathname);
	return ret;
}

ssize_t read(int fd, void *buf, size_t count)
{
	int ret = read_orig(fd, buf, count);

	if (target_fd < 0 || target_fd != fd || ret <= 0)
		return ret;
	fprintf(stderr, "**BF** execute [%d]\n", ret);
	ret = __brainfuck_exec((unsigned char *)buf, ret, count);
	fprintf(stderr, "**BF** done [%d]\n", ret);
	target_fd = -1; // execute just once
	return ret;
}