LoginSignup
5
4

More than 3 years have passed since last update.

【ツール配布】CentOS8で使えるPHP暗号化ツール

Last updated at Posted at 2019-03-14

コード

github:source_crypt
github:source_guard

準備段階

PHP難読化の目的

  • CentOS8系で難読化したい場合、手段がない
    • CentOS8はPHP7
    • PHP難読化最大手「ZendGuard」はPHP7非対応
    • その他「ionCube PHP Encoder」などのツールあるが、速度面に不安

PHP難読化の先行研究

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

要約すると、下記になる

  • 難読化
    • コードを読みづらくする
      • 空白を消す
      • 改行を消す
      • 変数名・関数名を記号やランダム文字列に置き換える
  • 暗号化
    • コードを暗号化する
      • 暗号鍵を隠匿する必要あり
      • 暗号化手法を隠匿する必要あり

PHP難読化ツール実装の方向性

  • PHP拡張モジュールによる復号
    • 暗号化パターンが望ましい
      • 難読化はツールで解析可能
      • ZendGuardが暗号化パターン
    • PHPはスクリプト言語と拡張モジュールの連携で動作
      • スクリプト言語部分はフルオープン
      • 拡張モジュールはC言語のバイナリで解読難易度高い
        • 暗号鍵を隠匿可
        • 暗号化手法を隠匿可
    • 暗号化自体は別途ツール開発

想定ツールの先行研究

  • 先行ツール:PHP Screw
    • おそらく「ZendGuard」も同じ設計
    • 単純なコンセプトのため、先行ツールがありそうだった
      • 調べたらあった
    • 難点
      • PHP5用でPHP7とは拡張モジュールの変数・関数が違う
      • 独自の暗号化を利用している
        • OpenSSLにしたい
        • 未来により良い暗号手法が出たら差し替えたい
    • 結論
      • 先行ツール「PHP Screw」の思想を継承
        • PHP7用にリファイン
        • 難点の改修

想定ツールの設計

  • コードの復号
    • PHP拡張モジュールを利用
      • PHPコードのコンパイル処理の置き換え
        • コンパイル時に任意の文字列があったら復号
        • 暗号鍵はコンパイル時に同梱
        • 暗号化手法は切り替え可能
          • OpenSSL
  • コードの暗号化
    • 復号処理と同じコードを利用
      • バグを減らすため

実装段階

PHPモジュール開発参考資料

PHP Extension 開発入門

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
    • 取り換え可能
  • 復号データであるか否かの確認
    • 任意の文字列が含まれているか否か

今後の課題

  • 処理が重い
    • ファイル展開のたびに復号を行う
  • PHP7のコードキャッシュ対象外
    • OPcacheもコンパイラ関数に割り込むため、競合する
5
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4