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

# コード
[github:source_crypt](https://github.com/ofbear/source_crypt)
[github:source_guard](https://github.com/ofbear/source_guard)

# 準備段階
## PHP難読化の目的
- CentOS8系で難読化したい場合、手段がない
    - CentOS8はPHP7
    - PHP難読化最大手「ZendGuard」はPHP7非対応
        - 今後も対応しないことを[宣言済み](https://blog.zend.com/2016/10/10/zend-guard-and-php-7/#.XIo2Y7jgpXg)
    - その他「ionCube PHP Encoder」などのツールあるが、速度面に不安

## PHP難読化の先行研究
[PHPのソースコードの難読化・暗号化](https://qiita.com/fallout/items/f3c3b9a5a3117908d303)

要約すると、下記になる

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

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

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

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

# 実装段階
## PHPモジュール開発参考資料
[PHP Extension 開発入門](https://prev.net-newbie.com/phpext/index.html)

## PHP拡張モジュールの開発環境の作成
### パッケージインストール
``` sh
dnf  install -y wget bzip2 gcc
```

### PHPのソースコード取得
``` sh
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
```

### テンプレート作成
``` sh
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
```

### コンパイル
``` sh
make
```

### 確認
``` sh
make test
```

### iniファイルを追加
``` sh
vi [任意のモジュール名].ini

    ; Enable source_guard extension module
    extension=source_guard
```

### デバッグ
``` sh
make

make test

php -d extension=modules/[任意のモジュール名].so [任意のモジュール名].php
```

### 本番
``` sh
make

make install

systemctl restart httpd

php -m

systemctl restart httpd
```

## 実装コード
### コンパイル処理の置き換え
```c
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;
}
```

```c
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
    - デストラクタ
    - 元々のコンパイラ関数の入れなおし

### ファイル処理
```c
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
        - 実データへメモリ上のファイルデータを渡す
- 元々のコンパイル関数へ投げる

### 復号処理
``` c
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;
}
```

```c
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もコンパイラ関数に割り込むため、競合する