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

PHPのソースコード(以下**コード**と略します)を、暗号化したり難読化する方法についての記事です。**便宜上**、各単語は以下のような意味合いで使用しています。

- 暗号化(Encrypt)
	- コードを秘匿する事を目的として、複雑で可逆な変換をかける事。(Blowfish等のイメージ)

- エンコード(Encode)
	- コードを別の形にする事を目的として、単純で可逆な変換をかける事。(Base64やURLエンコード等のイメージ)

- 難読化(Obfuscate)
	- コードとしてはそのまま実行可能だが、人間が見ると非常に理解し難い状態にする事。(軽量化よりは頑張るイメージ)

- 軽量化(Minify)
	- コードの実行結果に影響しないコメント・改行・スペース等を、単純にサクっと削除する事。結果的に難読化にもなる。

しばらくPHP界隈から離れていたため、古い知識や記憶に頼って書いている部分もありますので、ご了承ください。

--------------------------------------------------------------------------------

## 暗号化

### ionCube PHP Encoder

- http://www.ioncube.com/
- サーバーに拡張モジュールをインストールする必要があります。
	- php.iniを編集しWebサーバを再起動するだけです。
- オプションで、難読化してから暗号化する事もできます。
- 最近はプリインストールされているレンタルサーバーもあるようです。

相変わらずこの製品一択ではないでしょうか。(10年近く前から選択肢が変わってない気が…)

日本に代理店があったのですが、2017年1月31日をもって代理店契約が終了したようです。(この記事を書く際に初めて知りました)

※この手のあまりメジャーではない(日本語ローカライズすらされていない)ソフトウェアを日本の代理店から購入するメリットは、購入手続きが日本語で済む事ぐらい[^1]だと思いますので、**本家で安く買えば良いだけ**です。~~ボッタクられる人がいなくなるので、むしろ良かったのでは…。~~

[^1]: 購入後の日本語サポートが充実している等の何らかの付加価値があれば話は別ですが、日本の代理店を通して割高になった海外のソフトウェアを購入する際は、**本当にその価格差に見合う価値があるのか**を見極めた方が良いでしょうね！

--------------------------------------------------------------------------------

### その他の製品や無料ツール

2009年頃の話で申し訳ありませんが、国内外の様々なツールで暗号化されたコードを、パズル感覚で復元して遊んだ事があります。

暗号化したコードを `eval()` で評価する方法なので、オリジナルのコードを簡単に復元できてしまうものも多くありました。[^2]

[^2]: `eval()` で評価するという事は、意図的にエラーを発生させ、スタックトレース(バックトレース)でコードを復元できてしまう可能性もあるという事です。

暗号化というよりは、エンコードに近いと思っていた方が良いかもしれません。実際、コードをgzipやDeflate等で圧縮した後に、Base64やUuencoding等をかけ、適当に文字列を並び替えた程度のツールも当時は多くありました。

現在はもう少し進歩したツールもあるのかもしれませんが、PHPのみで復号するタイプのツールには、**過度の期待はしない方が良い**と思います。

もし**使用する場合は、難読化と併用する**事を強くオススメします。(ツールによっては、難読化を同時に行えるものもあるようです。)

--------------------------------------------------------------------------------

## 難読化

### 無料ツール(ライブラリ)

30分程探してみたのですが、私の探し方が悪いのか、まともにメンテされてそうなのが[PHP Classes](https://www.phpclasses.org/)にある、「PHP Application Packer」というクラスぐらいしか見つかりませんでした。

- https://www.phpclasses.org/package/9574-PHP-Create-packages-of-project-files-for-distribution.html

他に良いものを見つけたら、またこの記事に追加したいと思います。

コードを軽量化する事は当然として、最大のキモは、「ユーザー定義のクラス名・関数名・定数名・変数名等が理解し難い文字列に置換されているかどうか」だと思います。

--------------------------------------------------------------------------------

### 無ければ作ればいいじゃない

…という事で、自作してみました。

#### 指針

- 軽量化には、[コマンドラインオプション](http://php.net/manual/ja/features.commandline.options.php) `php --strip <file>` を使う。

- 構文解析には [token_get_all()](http://php.net/manual/ja/function.token-get-all.php) を使う。
	- [パーサトークンの一覧](http://php.net/manual/ja/tokens.php)

- テンポラリ領域での処理として [SplTempFileObject](http://php.net/manual/ja/class.spltempfileobject.php) を使う。

- クラス名や変数名の変換先は不可逆な文字列にする。
	- [定義済の変数](http://php.net/manual/ja/reserved.variables.php)は対象外にする。
	- 今回は面倒で [sha1()](http://php.net/manual/ja/function.sha1.php) を通した結果を適当にサプレスして頭に1文字追加しただけ。これは非常にマズいので他の方法を取るべき。

- 文字列や数値は16進表現に変換する。

#### 結果

```php
//-- コメント
$str = 'ＡＢＣＤＥ';
$int = 1234;

/**
 * コメント
 */
class Hoge
{
    public static function fuge($str, $int)
    {
        if (!mb_strlen($str)) { die('$strが空だよ'); }
        return mb_strtolower($str, 'UTF-8') . strval($int * 987);
    }
}

var_dump(Hoge::fuge($str, $int));    // ａｂｃｄｅ1217958
```

↓ 一部改行を入れてますが実際は1行のコードです ↓

```php
$V16a5efcaec4 = "\xEF\xBC\xA1\xEF\xBC\xA2\xEF\xBC\xA3\xEF\xBC\xA4\xEF\xBC\xA5"; $Vdeea1144fc3 = 0x4D2;
class Fd58e1a60037 { public static function Fc239d01c4e6($V16a5efcaec4, $Vdeea1144fc3) {
if (!mb_strlen($V16a5efcaec4)) { die("\x24\x73\x74\x72\xE3\x81\x8C\xE7\xA9\xBA\xE3\x81\xA0\xE3\x82\x88"); }
return mb_strtolower($V16a5efcaec4, "\x55\x54\x46\x2D\x38") . strval($Vdeea1144fc3 * 0x3DB); } }
var_dump(Fd58e1a60037::Fc239d01c4e6($V16a5efcaec4, $Vdeea1144fc3));
```

#### 問題点

- 面倒くさい(笑)
	- 正直舐めてました。[パーサトークン](http://php.net/manual/ja/tokens.php)だけで100種類以上あるので、キチンと書こうとすると死ねそうです。めちゃくちゃ増えていて驚きました。

- ウイルスとして検知される(笑)
	- おそらく16進数表現に変換した部分が、一部のウイルス対策ソフトに引っかかるのだと思います。

- スペースを完全には取り切れない
	- `php --strip <file>` だと完全には取り切れないようなので、こだわるなら他の方法にする必要がありそうです。

もう少しキチンとしたコードになったら、GitHub行きにしたいと思いますが、いつになるやら…。どなたかチャレンジしてみてください。

書き方にこだわらなければ、下記のように `switch case` を使って泥臭く処理していくような流れが理解しやすいと思います。(今回私が取った方法です)

```php
$functions = [];

foreach ($tokenAll as $token) {
    if (is_string($token)) {
        continue;
    }

    list($id, $name, $line) = $token;

    $isFunction = false;

    switch($id) {
        case T_FUNCTION:
            $isFunction = true;
            break;

        case T_STRING:
            if ($isFunction) { $functions[] = $name; }
            break;

        default:
            $isFunction = false;
    }
}
```

--------------------------------------------------------------------------------

## OPcacheのファイルベースキャッシュを使ってみては！？

「Pythonのバイトコンパイル(.pyc)みたいなものなら使えるのでは？」と思って調べていると、既に全く同じ発想をした人がSlideShareへ投稿していました。

- [OPcacheの新機能ファイルベースキャッシュの内部実装を読んでみた](https://www.slideshare.net/hnw/opcache)

結論だけ書くと、今回の目的での実用は難しそうですね…残念。

--------------------------------------------------------------------------------

## ベストプラクティス？

- 個人：サーバーの知識を身に付けて、自分専用のサーバーで運営しコードを秘匿する必要がないようにする。
	- Webサーバ構築経験がない方は、これを機にチャレンジしてみてはどうでしょうか？
- 法人：サーバー構築案件も一緒に取り、コードを秘匿する必要がないようにする。
	- 他業者にボッタクられているクライアントもたくさんいます。クラウドやVPS等を活用して安く済ませてあげれば、Win-Winです。[^3]
<br><br>
- 拡張モジュールをインストールできる環境の場合は、ionCube PHP Encoder。
<br><br>
- 無料の難読化ツールを使用する。暗号化までするかどうかはケースバイケースだが、強度には期待しない。

[^3]: 理想論だ。現実はそう甘くはない！という方もいるかもしれませんが、勉強不足のせいで提案ができない・取れる筈の案件を取れない営業さんもたくさん居ます。

--------------------------------------------------------------------------------

## オマケ：とある有料ツールで暗号化したコードを復号してみる

有料のツールで暗号化したコードでも、少ない手順で復号できてしまうケースもあるという、警告の意味を込めて載せておきます。※クラックする事が目的ではなく、また、コードをそのまま載せるとウイルスとして検知される可能性もありますので、コードは大部分を省略しています。

### 0.暗号化されたコード

```php
<?php $OOO000000=urldecode('%66%67%36%73%62%65%68%70%72%61%34%63%6f%5f%74%6e%64');$OOO0000O0=$OOO000000{4}.$OOO000000{9}.$OOO000000{3};$OOO0O0O00=__FILE__;$OO00O0000=0x6fc; 
eval($OOO0000O0('JE8wMDBPME8wMD...E8wKTs='));return;?>
tbF@Oa...WPLFMazfBx...VuR~e\cxn^
```

### 1.eval() の評価部を探してデコードをかける

最後が「=」で終わっている事からBase64でエンコードされている可能性が高いので、デコードしてみると、以下のようなコードが現れます。

```php
$O000O0O00=$OOO000O00($OOO0O0O00,'rb');
$O0O00OO00($O000O0O00,0x48d);
$OO00O00O0=$OOO0000O0($OOO00000O($O0O00OO00($O000O0O00,0x17c),'EnteryouwkhRHYKNW...VvXxZz0123456789+/=','ABCD...wxyz0123456789+/'));
eval($OO00O00O0);
```

### 2.アナグラムを解きほぐす

```php
$fp = fopen(__FILE__, 'rb');    // ファイル自身を開いて
fread($fp, 1165);               // 0x48d(1165)byte読み飛ばし
$secret = fread($fp, 380);      // 更にそこから0x17c(380)byte読み込んだ部分をコードの元とする
$code = base64_decode(strtr($secret, 'EnteryouwkhRHYKNW...VvXxZz0123456789+/=', 'ABCD...wxyz0123456789+/'));
```

```php
//-- 上記 $code を更に解く
$OO00O00O0=str_replace('__FILE__',"'".$OOO0O0O00."'",$OOO0000O0($OOO00000O($O0O00OO00($O000O0O00,$OO00O0000),'EnteryouwkhRHYKNW...VvXxZz0123456789+/=','ABCD...wxyz0123456789+/')));
fclose($O000O0O00);
eval($OO00O00O0);
```

### 3.復号完了

```php
//-- 中略
$secret = fread($fp, 1788);    // 更に0x6fc(1788)byte読み込んだ部分をコードの元とする
$code = base64_decode(strtr($secret, 'EnteryouwkhRHYKNW...VvXxZz0123456789+/=', 'ABCD...wxyz0123456789+/'));

echo $code;
```

5ステップで復号が完了しました。難読化は行われていなかったので、完全にオリジナルのコードが復元されています。

以下のページに掲載されている、WordPressのセキュリティホールを突いて感染するウイルスの方が、ずっと手が込んでいるのではないでしょうか…。

- [スパマのスパムスクリプトを解析してみる : 難読化されたPHPスクリプトに挑む](http://postd.cc/dissecting-spammers-spam-script/)