Edited at

平成も終わりかけの今、あえてのPHPソースコードの暗号化


ソース見たい人へ

github:php_source_guard


何故にPHP暗号化?

 PHP暗号化。一昔前はそれなりに流行っていた話題のようですが、今は下火の模様。PHP暗号化するくらいなら、最初からgolang使った方が効率がいいからでしょうか?

 何故に平成も終わりかけの今、PHP難読化なのかと申しますと。

 のため、これまで「ZendGuard」でPHPソースを保護していた会社さんが、改めてPHP暗号化を考える時期に来たからなのです。弊社のように!


どういった手法があるの?

PHPのソースコードの難読化・暗号化

 詳しい話は上を読んでもらえると分かりやすいんですが。大まかに分類すると、


  • 空白や改行を消し、変数を読みづらい形に変換する「難読化」

  • ソースコードを暗号化し、拡張モジュールで復号する「暗号化」

 になるのかなと思います。

 「ZendGuard」の後釜と考えると、難読化よりは暗号化が好ましい。現状だと「ionCube PHP Encoder」一択になるようですが。そこはプログラマ。最終的には製品を買うにしても、自分で実装してみたいのが人情というもの。


どういう方向性でやる?

 PHPはスクリプト言語です。PHPで書かれたソースコードを、実行時にZendエンジンがコンパイルしているわけですから、暗号化したソースを突っ込んでコンパイル時に復号させれば、PHP暗号化は実装できるはず。

 じゃあ、それ、どうやんの? って考えてみたところ、既に同じ思想で実装を済ませている方がいました。

PHP Screw

 これ、このまま使えるんじゃないの? 「せっかくだから作りたい」という殊勝な心掛けを投げ捨てて、そんなことを考えたりもしたのですが。残念な点がいくつかある。


  • PHP5用なのでPHP7とは拡張モジュールの変数・関数が地味に違う

  • 独自の暗号化を利用しているので、できたらOpenSSLにしたい

 ということなので、恥ずかしい話、「PHP Screw」の出入り口をPHP7に置き換えて、内部の暗号化をOpenSSLに変更してみました。

 ちなみに、PHP拡張モジュールを開発するにあたっては、↓のサイトを参考にしました。わからんところは、こっちを読んでください。何故か、PHP拡張モジュールの公式ドキュメントはどこにもないんですよ。公式は拡張モジュール作らせる気、あるの? 本当に?

PHP Extension 開発入門


どういう実装になるの?

 まず、出入り口。PHP_MINIT_FUNCTION というのは、ようするに拡張モジュールのコンストラクタです。ここにコンパイル関数を入れ替える仕掛けを突っ込みます。読めば分かるでしょうけど、オリジナルを取っておいて、代わりに自作をねじ込んでいます。

    PHP_MINIT_FUNCTION(source_guard)

{
CG(compiler_options) |= ZEND_COMPILE_EXTENDED_INFO;

org_compile_file = zend_compile_file;
zend_compile_file = source_guard_compile_file;

return SUCCESS;
}

 次に、PHP_MSHUTDOWN_FUNCTION。デストラクタです。一応コンパイル関数に本来の関数を戻しています。正直、意味があるのかどうか、分かりません。が、「PHP Screw」の実装がそうなっていたので、そうしておこうかな、って。

    PHP_MSHUTDOWN_FUNCTION(source_guard)

{
CG(compiler_options) |= ZEND_COMPILE_EXTENDED_INFO;
zend_compile_file = org_compile_file;

return SUCCESS;
}

 で、自作のコンパイル関数の実装に入ります。突っ込まれたソースコードは file_handle の中に入っています。送り込まれたコードを復号して、それをオリジナルのコンパイル関数に渡して終了、という感じ。

 初期化処理は省略しましたが、処理で使う変数を全部pl_bufferっていう構造体に突っ込んで、開いたFileを閉じたり、確保したメモリを解放したりしています。こういうのを一括管理したがるのは、Cで組み込みやってた癖なんでしょうな。

 正直、詳しくはよく分からないんですが、file_handle の中身はPHPにどう読み込まれるかによって異なるようです。調べた方がいいんでしょう。が、正直ソースコードを復号するにあたっては、どれで来ようと大した問題じゃなくて。どんなタイプで来ても全部閉じて、なかったことにして、file_handle->filename から新たにファイルデータを取得する方式になっております。

    ZEND_API zend_op_array *source_guard_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC)

{
// 初期化
source_init();

DEBUG_FPRINTF("%s", "処理開始\n\n");

// OPEN チェック
pl_buffer.fp_r = fopen(file_handle->filename, "r");
if (!pl_buffer.fp_r) {
DEBUG_FPRINTF("%s", "ERROR:ERR_FAIL_OPEN_FILE\n\n");
// 初期化
source_init();
return org_compile_file(file_handle, type);
}

// 復号
if (source_decrypt() != ERR_NOTHING) {
// 初期化
source_init();
return org_compile_file(file_handle, type);
}

// 個別の処理が必要になった時用
switch (file_handle->type) {
case ZEND_HANDLE_FILENAME:
case ZEND_HANDLE_STREAM:
case ZEND_HANDLE_MAPPED:
default:
// do nothing
break;
case ZEND_HANDLE_FD:
close(file_handle->handle.fd);
break;
case ZEND_HANDLE_FP:
fclose(file_handle->handle.fp);
break;
}

file_handle->type = ZEND_HANDLE_FP;
file_handle->opened_path = NULL;
file_handle->handle.fp = pl_buffer.fp_w;

// 初期化
source_init();

return org_compile_file(file_handle, type);
}

 ファイルの読み出し処理と復号処理を分けたのは、暗号化技術が発展していった時に、そこだけ取り替えたいことがあるだろう、と思ったからです。とりあえず、AES-256-CBCになってますが。DESにするなり、モードだけCTRにでもするなり、好きにしてって感じ。

    int source_decrypt_openssl(const unsigned char* data, const size_t datalen, char* dest, const size_t destlen)

{
EVP_CIPHER_CTX ctx;
int f_len = 0;
int p_len = datalen;

DEBUG_FPRINTF("%s", "復号開始\n\n");

EVP_CIPHER_CTX_init(&ctx);

// 初期化
if (EVP_DecryptInit_ex(&ctx, EVP_aes_256_cbc(), NULL, PL_DEFAULT_CRYPTKEY, PL_INITIAL_VECTOR) != 1) {
DEBUG_FPRINTF("%s", "ERROR:EVP_EncryptInit_ex\n\n");
EVP_CIPHER_CTX_cleanup(&ctx);
return ERR_FAIL_EVP_INIT;
}

// 実行
if (EVP_DecryptUpdate(&ctx, (unsigned char *)dest, &p_len, data, datalen) != 1 ) {
DEBUG_FPRINTF("%s", "ERROR:EVP_DecryptUpdate\n\n");
EVP_CIPHER_CTX_cleanup(&ctx);
return ERR_FAIL_EVP_UPDATE;
}

// パディング処理
if (EVP_DecryptFinal_ex(&ctx, (unsigned char *)(dest + p_len), &f_len) != 1) {
DEBUG_FPRINTF("%s", "ERROR:ERR_FAIL_EVP_FINAL\n\n");
EVP_CIPHER_CTX_cleanup(&ctx);
return ERR_FAIL_EVP_UPDATE;
}
memset(dest + p_len + f_len, 0x00, datalen - p_len - f_len);

EVP_CIPHER_CTX_cleanup(&ctx);

DEBUG_FPRINTF("%s", "復号終了\n\n");

return ERR_NOTHING;
}

int source_decrypt()
{
struct stat stat_buf;
unsigned char resultfile[128];
int dec_code = ERR_NOTHING;

// ファイル読み出し
fstat(fileno(pl_buffer.fp_r), &stat_buf);
pl_buffer.datalen_enc = stat_buf.st_size;
pl_buffer.data_enc = calloc(1, sizeof(char) * pl_buffer.datalen_enc + 1);
fread(pl_buffer.data_enc, pl_buffer.datalen_enc, 1, pl_buffer.fp_r);

// [DEBUG] ファイルデータ出力
DEBUG_FPRINTF("datalen_enc:%d\n\n", pl_buffer.datalen_enc);
DEBUG_FPRINTF("data_enc:%s\n\n", pl_buffer.data_enc);

// 暗号化文字列確認
if (strstr((const char *)pl_buffer.data_enc, "<?php") != NULL) {
DEBUG_FPRINTF("%s", "NOCTICE:ERR_NOT_ENCRYPT\n\n");
return ERR_NOT_ENCRYPT;
}

// 復号後のメモリ確保
pl_buffer.datalen_raw = pl_buffer.datalen_enc;
pl_buffer.data_raw = calloc(1, sizeof(char) * pl_buffer.datalen_raw + 1);

// 復号
if ((dec_code = source_decrypt_openssl(pl_buffer.data_enc, pl_buffer.datalen_enc, pl_buffer.data_raw, pl_buffer.datalen_raw)) != ERR_NOTHING ) {
return dec_code;
}

DEBUG_FPRINTF("datalen_raw:%d\n\n", pl_buffer.datalen_raw);
DEBUG_FPRINTF("data_raw:%s\n\n", pl_buffer.data_raw);

// 先頭のツール名称の文字列確認
if (strstr((const char *)pl_buffer.data_raw, PL_TOOL_NAME) == NULL) {
DEBUG_FPRINTF("%s", "NOCTICE:ERR_NOT_SOURCE_GUARD\n\n");
return ERR_NOT_SOURCE_GUARD;
}

// ファイル出力
pl_buffer.fp_w = tmpfile();
fwrite(pl_buffer.data_raw, pl_buffer.datalen_raw, 1, pl_buffer.fp_w);

// 先頭へ移動
rewind(pl_buffer.fp_w);

return ERR_NOTHING;
}


今後の課題は?

 賢明な方ならお分かりだと思いますが。「毎回復号すんの?」っていう話があります。正直、めんどくさくて計っていないのですが、めちゃくちゃ遅いことだけは断言できる! 特にPHP7はソースキャッシュやデータキャッシュで高速に、が売りの一つみたいなもんですから。このままでは実用になりません。

 コンパイルに噛ませるという意味では、OPcache も途中まで全く同じ処理をしています。なので、OPcache にフォークして、この処理を突っ込むと、どうにか実用になるんじゃないか、というのが現状の所感です。

 が、PHP本業ではなくなってしまったので、これ以上深入りモチベーションがない! 現状はここまで。どなたか、気になる方がいたらやってみてください。そして、結果だけください!