Pythonコードを守るにはどうしたらよいのか?

仕事の関係で「pythonスクリプトを納品するが、コードが解読されてはならない、なんとかならないか」という相談を受けた。
イメージとしては、windowsの.exeでビルドされたアプリケーションのようなものにしたい、とのことだった。

結論から言うと、PyArmorというツールを使って比較的容易に、堅牢なプロテクトを施すことができた。

4つの難読化手法

「python obfuscate(=難読化)」で検索すると、様々なhowtoがヒットする。
いくつかの手法を試してみたが、どれもしっくりくるものがなく、最も信頼できるPython.orgの公式wikiから読み解くことにした。
このwikiを日本語に訳し、実際にツールを利用して難読化を試した検証結果を記載する。

1.バイトコード(.pyc)で機能を提供する


pythonコードのコンパイル結果であるpycは、pythonコードと同じ振る舞いをする。
pycはほとんどの開発者にとって読み取ることのできないコードである。
しかし、デコンパイルしてpythonコードを復元する方法が存在する

検証結果:
pycコードはいとも簡単に復元できたので、この選択するくらいなら難読化しなくていいと思います。

2.実行ファイル(.exe)で機能を提供する


pythonインタプリタとバイトコードを実行ファイルとして生成する方法がある。
この手法を取ることで、単にバイトコードに変換する以上の秘匿性が保たれる。また、探究心と熱意のある開発者であれば、ささいな労力でソースコードを保護することができる

検証結果:
PyInstallerというツールを使用し、サンプルプログラムを動作させようとしたが、機能せず。。。
”ささいな労力”は嘘だろ、と思った。

3.Saasで機能を提供する


もしかしたら、最も効果的な方法はソースコードを全く配布しないことかもしれない。
Googleは明らかにPythonを使用したサービスを提供しているが、ソースコードを隠すことに苦労などしていない

検証するまでもなし:
Sassとして、APIコールにレスポンスするシステムにするということなので、ソースコードを見せる必要がないです。
今回の要望には向いていないのでスキップ。

4.変数名やメソッド名を極端に読みづらい文字列に置き換える


検索エンジンで調べてみると、少なくとも3つは難読化ツールが存在するらしい。
これらは、機能として下記を提供する
・クラス名、変数名、メソッド名を置き換える
・コメントを削除する

さらに、PyArmorというツールに関しては下記の機能も提供されている
・DESでリテラルを暗号化する
・実行完了した一時バイトコードを即座に削除する

検証結果:後述

PyArmorについて

上記の4手法を検証し、下記の点が満たされていたため、PyArmorを採用することとした。
・単に1つのpythonスクリプトを難読化するだけでなく、プロジェクト単位で難読化できる
・難読化前と難読化後の振る舞いが完全に同じ
・理論上、ソースコードの復元ができない

実行方法

下記にサンプルプログラムを用意した

import math 
def sqr(x):
    return x * x
data = [66, 59, 62, 64, 63, 69, 70, 80, 73, 72] 
n = len(data)
print("n =%d" % n) 
print("Data...") 
i=1
for x in data:
    print("%5d:"%i+" %f"%x)
    i+=1 
sum = 0.0
for x in data: 
    sum += x
average = sum / n print("Average = %f" % average)

このサンプルプログラム(sample.py)を難読化する

pip install pyarmor 
pyarmor obfuscate sample.py

distフォルダが作成され配下に下記のファイルが作成される

dist /
_pytransform.dylib—– C言語をコンパイルして生成されたランタイム。macでpyarmor obfuscateコマンドを実行すると.dylibになり、Linuxだと.soになる。
license.lic————–有効期限を定めるライセンスファイル(デフォルトは無期限)
pytransform.key——–スクリプト実行キー
pytransform.py———ランタイム呼び出しスクリプト
sample.py————–難読化されたスクリプト

※太字で記載したファイルは、難読化対象のスクリプトが何件あろうと1件ずつ生成されるらしい

難読化されたスクリプトは、こんな感じ。

from pytransform import pyarmor_runtime # <- ランタイムを呼び出す
pyarmor_runtime()

__pyarmor__(__name__, __file__, b'\x06\x0f.........') # <-ひたすら16進数の文字列が続く。バイトコード化 + 置き換え処理されているため不可読。

これらのファイルは、下記の流れで実行される。
1. このようにユーザーは難読化されていることを意識する必要がない。python sample.pyを実行すれば良いだけである。
2.3. 難読化されたコードを実行するランタイムを呼び出す
4.ランタイムは、 ライセンスの有効期限・実行キーをチェックし、問題なければ実行する。

ランタイムの秘匿性

上記の説明をすると、飲み込みの早い人は、「じゃあ、ランタイムの中の処理が理解できたら、ソースコード復元できるじゃん」と思うかもしれない。
調べてみると確かに.dylibや.soをデコンパイルするツールが存在した。
Hopper Assemblerというツールでデコンパイルすると、下記のように非常に読みづらいアセンブリコードが14万行に渡って出力された。

99.99%のエンジニアは、「これは復元できないな」と考えるでしょう。
復元できた場合のリターンがよほどのものでない限り、その労力には見合わないはずです。

プロジェクトをまるごと難読化する際の注意点

こちらに公式のインストラクションがあります。
が、結構つまづくポイントが多いので、ここにメモしておきます。

つまづきポイント
– 難読化対象の各pythonファイルで、コメントがimport文より前に書いてあるとバグることがある。なので、コメントは全部消したほうがいい。

– 上記のインストラクションに従うと、プロジェクト全てのpythonファイルを置き換えたプロジェクトが生成される。が、.py以外のファイルは無視されるのと、.pyファイルがノード(ディレクトリの最下点)に入っていないディレクトリは生成すらされないので注意が必要。(下記が例です)


BEFORE
/app
–/staitic
—- index.html
–/python
—-model.py
—-test.py

AFTER
/app
–/python
—-model.py
—-test.py

– Linuxサーバーにデプロイする際は、.dilybファイルを.soファイルに置き換える。.soファイルは、ここからダウンロードできる

最後に

以上です。普段は「ソースコードをどれだけ読みやすくするか」を考えていますが、逆のアプローチは初めてでした。
あまり無いケースですが、自分の資産としてソースコードを守らなければならないときに、PyArmorは一つの選択としてアリだなーと思いました。