コード
github:source_crypt
github:source_guard
準備段階
PHP難読化の目的
- CentOS8系で難読化したい場合、手段がない
- CentOS8はPHP7
- PHP難読化最大手「ZendGuard」はPHP7非対応
- 今後も対応しないことを宣言済み
- その他「ionCube PHP Encoder」などのツールあるが、速度面に不安
PHP難読化の先行研究
要約すると、下記になる
- 難読化
- コードを読みづらくする
- 空白を消す
- 改行を消す
- 変数名・関数名を記号やランダム文字列に置き換える
- コードを読みづらくする
- 暗号化
- コードを暗号化する
- 暗号鍵を隠匿する必要あり
- 暗号化手法を隠匿する必要あり
- コードを暗号化する
PHP難読化ツール実装の方向性
- PHP拡張モジュールによる復号
- 暗号化パターンが望ましい
- 難読化はツールで解析可能
- ZendGuardが暗号化パターン
- PHPはスクリプト言語と拡張モジュールの連携で動作
- スクリプト言語部分はフルオープン
- 拡張モジュールはC言語のバイナリで解読難易度高い
- 暗号鍵を隠匿可
- 暗号化手法を隠匿可
- 暗号化自体は別途ツール開発
- 暗号化パターンが望ましい
想定ツールの先行研究
- 先行ツール:PHP Screw
- おそらく「ZendGuard」も同じ設計
- 単純なコンセプトのため、先行ツールがありそうだった
- 調べたらあった
- 難点
- PHP5用でPHP7とは拡張モジュールの変数・関数が違う
- 独自の暗号化を利用している
- OpenSSLにしたい
- 未来により良い暗号手法が出たら差し替えたい
- 結論
- 先行ツール「PHP Screw」の思想を継承
- PHP7用にリファイン
- 難点の改修
- 先行ツール「PHP Screw」の思想を継承
想定ツールの設計
- コードの復号
- PHP拡張モジュールを利用
- PHPコードのコンパイル処理の置き換え
- コンパイル時に任意の文字列があったら復号
- 暗号鍵はコンパイル時に同梱
- 暗号化手法は切り替え可能
- OpenSSL
- PHPコードのコンパイル処理の置き換え
- PHP拡張モジュールを利用
- コードの暗号化
- 復号処理と同じコードを利用
- バグを減らすため
- 復号処理と同じコードを利用
実装段階
PHPモジュール開発参考資料
PHP拡張モジュールの開発環境の作成
パッケージインストール
dnf install -y wget bzip2 gcc
PHPのソースコード取得
mkdir /home/[ユーザ名]/php-dev
cd /home/[ユーザ名]/php-dev
wget -O php-7.3.10.tar.bz2 http://jp2.php.net/get/php-7.3.10.tar.bz2/from/this/mirror
tar xjf php-7.3.10.tar.bz2
cd /php-7.3.10/ext
テンプレート作成
php ext_skel.php --ext [任意のモジュール名]
cd /[任意のモジュール名]
vi config.m4
dnl(コメントアウト)を外す
dnl PHP_ARG_ENABLE(my_ext, whether to enable my_ext support,
dnl Make sure that the comment is aligned:
dnl [ --enable-my_ext Enable my_ext support])
phpize
./configure --enable-[任意のモジュール名]
開発すべきコード
[任意のモジュール名].c
php_[任意のモジュール名].h
コンパイル
make
確認
make test
iniファイルを追加
vi [任意のモジュール名].ini
; Enable source_guard extension module
extension=source_guard
デバッグ
make
make test
php -d extension=modules/[任意のモジュール名].so [任意のモジュール名].php
本番
make
make install
systemctl restart httpd
php -m
systemctl restart httpd
実装コード
コンパイル処理の置き換え
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(source_guard)
{
CG(compiler_options) |= ZEND_COMPILE_EXTENDED_INFO;
zend_compile_file = org_compile_file;
return SUCCESS;
}
- PHP_MINIT_FUNCTION
- コンストラクタ
- 元々のコンパイラ関数の保存とオリジナル関数への置き換え
- PHP_MSHUTDOWN_FUNCTION
- デストラクタ
- 元々のコンパイラ関数の入れなおし
ファイル処理
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);
}
- メモリ初期化
- ファイル展開
- file_handle->filenameからパスを取得
- メモリにファイル内容を展開
- 復号処理
- 後述
- ファイルタイプ設定
- ファイルは展開のされ方によってデータ形式が異なる
- 調査していない
- デバッグ中の実動作確認だと直接開いた場合とrequireで読み込まれた場合でタイプが異なっていた
- ファイルタイプをファイルポインタへ上書き
- ファイルタイプへZEND_HANDLE_FP
- 実データへメモリ上のファイルデータを渡す
- ファイルは展開のされ方によってデータ形式が異なる
- 元々のコンパイル関数へ投げる
復号処理
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;
}
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;
}
- 復号結果ファイルのメモリ確保
- 復号実行
- openssl
- aes256
- 取り換え可能
- openssl
- 復号データであるか否かの確認
- 任意の文字列が含まれているか否か
今後の課題
- 処理が重い
- ファイル展開のたびに復号を行う
- PHP7のコードキャッシュ対象外
- OPcacheもコンパイラ関数に割り込むため、競合する