August 2, 2018

PHP Extension を作って関数をフックしてみる

夏休みに観る Netflix オススメ映画を募集しています。


PHP Extension で sha1() をフックして引数を表示するところまでやってみたのでメモ。

1. PHP Extension を作る準備

PHPのソースコード一式を落としてきてコンパイルする。

$ cd php7.2.8
$ ./configure
$ make

ext_skel を使うことでテンプレートが生成される。
--extname で好きな名前を指定する。

$ cd ext
$ ./ext_skel --extname=hook
Creating directory hook
Creating basic files: config.m4 config.w32 .gitignore hook.c php_hook.h CREDITS EXPERIMENTAL tests/001.phpt hook.php [done].

To use your new extension, you will have to execute the following steps:

1.  $ cd ..
2.  $ vi ext/hook/config.m4
3.  $ ./buildconf
4.  $ ./configure --[with|enable]-hook
5.  $ make
6.  $ ./sapi/cli/php -f ext/hook/hook.php
7.  $ vi ext/hook/hook.c
8.  $ make

Repeat steps 3-6 until you are satisfied with ext/hook/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.

これで準備は整った。

2. phpinfo() に表示してみる

PHP には 標準 PHP 拡張モジュール関数 というものがある。

phpinfo() にモジュールの情報を表示するには PHP_MINFO_FUNCTION を使えば良いらしい。

ext/hook/hook.c に以下を記述する。

PHP_MINFO_FUNCTION(hook)
{
  php_info_print_table_start();
  php_info_print_table_header(2, "this is hook module", "enabled");
  php_info_print_table_end();
}

続いて ext/hook/config.m4 の以下の部分をコメントアウトする。

PHP_ARG_ENABLE(hook, whether to enable hook support,
Make sure that the comment is aligned:
[  --enable-hook           Enable hook support])

ビルドする。

$ cd ext/hook
$ phpize
$ ./configure
$ make

すると、 modules/hook.so が生成されるので、 php.ini で読み込ませてPHPを実行してみる。

extension=/path/to/ext/hook/modules/hook.so

build-in server で確認してもいいし、単純に php -r 'phpinfo();' で確認してもよい。

$ php -r 'phpinfo();'
...
hook

this is hook module => enabled
...

ちゃんと読み込めていることが分かる。

3 sha1() をフックしてみる

なぜ sha1() を選んだか。それは僕が最近「機動戦士ガンダム THE ORIGIN シャア・セイラ編 IV 運命の前夜」を観たから…

関数をフックするにあたって、フック対象の関数のアドレスを知る必要がある。
関数テーブルには CG マクロを利用してアクセスすることができる。

CG(function_table)

zend api には zend_hash_str_find_ptr という便利なものがあり、ハッシュテーブルから指定した関数を取得してくれる。

つまり以下のようになる。

zend_function *original_function;
original_function = zend_hash_str_find_ptr(CG(function_table), "sha1", strlen("sha1"));

Hook するためには自分で用意した関数に向ける必要がある。
もちろん、正しく引数を受け取る必要があるため、まず sha1() のソースを見てみる。

PHP_FUNCTION(sha1)
{
	zend_string *arg;
	zend_bool raw_output = 0;
	PHP_SHA1_CTX context;
	unsigned char digest[20];

	ZEND_PARSE_PARAMETERS_START(1, 2)
		Z_PARAM_STR(arg)
		Z_PARAM_OPTIONAL
		Z_PARAM_BOOL(raw_output)
	ZEND_PARSE_PARAMETERS_END();

	PHP_SHA1Init(&context);
	PHP_SHA1Update(&context, (unsigned char *) ZSTR_VAL(arg), ZSTR_LEN(arg));
	PHP_SHA1Final(digest, &context);
	if (raw_output) {
		RETURN_STRINGL((char *) digest, 20);
	} else {
		RETVAL_NEW_STR(zend_string_alloc(40, 0));
		make_digest_ex(Z_STRVAL_P(return_value), digest, 20);
	}

}

PHP_FUNCTION(hackers_function) は、次のような宣言に展開される。

void zif_hackers_function(INTERNAL_FUNCTION_PARAMETERS)

hmhm. これを使えばうまくいけそうな気がする。

void zif_sha1_hook_function(INTERNAL_FUNCTION_PARAMETERS)
{
  php_printf("sha1 hooked!\n");
}

あとはオリジナルの sha1() を上記の関数 zif_sha1_hook_function に向ければOK.

zend_function *original_function;
original_function = zend_hash_str_find_ptr(CG(function_table), "sha1", strlen("sha1"));
original_function->internal_function.handler = zif_sha1_hook_function;

上記のコードを PHP_MINIT_FUNCTION に追記する。
PHP_MINIT_FUNCTION() にはモジュールをロードしたときに最初に呼び出される処理を書くもの。

コンパイルして実行…ドキドキ

$ php -r "var_dump(sha1('wei'));"
sha1 hooked!
NULL

kita–
ちゃんとフックされた

4. 引数を表示してみる

続いて sha1() の引数を表示してみる。
これはもとの sha1() と内容を合わせてあげれば良いのでコピペ。

void zif_sha1_hook_function(INTERNAL_FUNCTION_PARAMETERS)
{
  php_printf("sha1 hooked!\n");
  zend_string *arg;
  zend_bool raw_output = 0;
  ZEND_PARSE_PARAMETERS_START(1, 2)
		Z_PARAM_STR(arg)
		Z_PARAM_OPTIONAL
		Z_PARAM_BOOL(raw_output)
	ZEND_PARSE_PARAMETERS_END();
  // ZSTR_VAL で文字列を取り出す
  php_printf("sha1 arg is %s, length is %d", ZSTR_VAL(arg), ZSTR_LEN(arg));
}

実行結果は以下。

$ php -r "var_dump(sha1('wei'));"
sha1 hooked!
sha1 arg is wei, length is 3NULL

ちゃんとフックして引数を取得することができた。


うまくやれば各種変数 $_SESSION$_GET などを取得できるので、夏休みにでもやってみよう。
あと、例えば PHP_MINIT_FUNCTION は root で動く(!)ので PHP_MSHUTDOWN_FUNCTION とうまく使うことで、PHPが起動するたびに動的にモジュールを読み込ませ、PHPが終了するときには PHP.ini からその設定は消えている…みたいな誰得技もできる(バックドアかよ…)

ref

このエントリーをはてなブックマークに追加

© Kouhei Morita 2018