バイトコード混同によるPythonコードの保護


Find an English version of this topic here. 転載は原文の住所を保留してくださいhttp://blog.csdn.net/ir0nf1st/article/details/61650984
<0 x 00>はじめに
Python開発者は、コード内の技術的秘密をどのように保護するかという難題に直面することが多い.筆者が試みたいくつかのPythonコード保護ツールは、この目標を効果的に実現することが難しいか、有効であるが無視できない欠点がある.最近筆者もこの問題に直面し,有効な解決策が見つからない場合に,バイトコード混同器を独自に開発せざるを得ない.本稿ではまず,一般的なPythonコード保護メカニズムといくつかの比較的入手しやすいPythonコード保護ツールについて簡単な解析を行い,次にバイトコード混同によるPythonコード保護の技術原理を示した.
<0 x 01>ソースコード混同
筆者は2つのソースコード混同ツールを試みたことがある.一つはpyminifierで、もう一つはオンラインソース混同サービスを提供しています.http://pyob.oxyry.com/.この2つのツールの動作方法は類似しており、クラス名/関数名/変数名を再命名し、pyminifierは一部のPython定数(例えばTrue/False/Nene)を乱すこともできるが、コードの論理と制御フローは変更されていない.混同されたソースコードを読むことは読者の目にとって破壊であり、理解上の困難をもたらしたが、簡単な改名は基本的なテキストの検索と置換に対抗することもできない.ソースコードの混同は名前の置き換えだけを工夫すれば、コード保護を実現するのは縁木が魚を求めるのと同じだ.筆者は,ソースコード混同がコード保護を実現するには,ターゲットプログラムの抽象構文ツリー(Abstract Syntax Tree)を抽出し,構文ツリーを修正し,修正後の構文ツリーに基づいて新しいソースコードを生成しなければならないと考えている.しかし、このような作業量は、コンパイラを実装するよりも少なくありません.この英語の文章はAST分析に基づくPythonソースコードの混同方法をより深く紹介しており、興味のある読者は参考にすることができる.以下はpyminifierの試用結果で、読者は名前の置き換えがソースコードを有効に保護できるかどうかを評価することができます.http://pyob.oryry.com提供するサービスはpyminifierよりも簡単で、ここでは試用効果は提供されません.混同前のサンプルコード:
class SampleClass:
    def __init__(self):
        self.data = None

    def method1SampleClass(self, arg):
        self.data = arg

def function_with_if(arg):
    if arg == True:
        pass
    else:
        pass

def function_with_if1(arg=True):
    if arg == True:
        print('True')
    else:
        print('False')

def function_with_if2():
    if True:
        print('True')
    else:
        print('False')

def function_with_try_except1():
    try:
        data = 1/0
    except:
        print('Constructed Control Flow')

def function_with_try_except2():
    try:
        pass
        print('Constructed Control Flow')
    except:
        pass

global_var1, global_var2, global_var3
pass

a = SampleClass()
a.method1SampleClass()
function_with_if(False)
function_with_if1()
del global_var1, global_var2, global_var3

混同されたコード:
class N:
y=None
T=True
H=False
 def __init__(P):
  P.data=y
 def b(P,R):
  P.data=R
def x(R):
 if R==T:
  pass
 else:
  pass
def L(arg=T):
 if arg==T:
  print('True')
 else:
  print('False')
def I():
 if T:
  print('True')
 else:
  print('False')
def d():
 try:
  n=1/0
 except:
  print('Constructed Control Flow')
def E():
 try:
  pass
  print('Constructed Control Flow')
 except:
  pass
global_var1,global_var2,global_var3
pass
a=N()
a.method1SampleClass()
x(H)
L()
del global_var1,global_var2,global_var3
# Created by pyminifier (https://github.com/liftoff/pyminifier)

特筆すべきはpyminifierがNone/True/Falseを乱すときにバグがあるようだ.
class N:
y=None
T=True
H=False
 def __init__(P):
  P.data=y
 def b(P,R):
  P.data=R
def x(R):
 if R==T:
  pass
 else:
  pass

ここで、変数y/T/Hの役割ドメインはclass N内部にあるが、以下の関数はclass Nの方法ではなく、関数におけるT/Hの参照はその役割ドメインを超えている.
def L(arg=T):
 if arg==T:
  print('True')
 else:
  print('False')
def I():
 if T:
  print('True')
 else:
  print('False')

読者がpyminifierを使用する場合は、この問題に注意する必要があります.
<0 x 02>Pythonコードを実行可能ファイルにパッケージ
py 2 exe、PyInstallerはPythonコードとPython実行環境(Python解釈器、アプリケーション依存の標準モジュールなど)を実行可能ファイルにパッケージ化し、Pythonコードを事前にPythonをインストールしていないターゲットマシンで実行することができます.py 2 exeはPythonコードとその依存ファイルをzipパッケージにパッケージ化し、解凍するとすべてのファイルが逆コンパイルされるのを待っていることがわかります.PyInstallerはpy 2 exeよりも安全で、PythonコードのAES暗号化をサポートしていますが、明文のAES鍵もパッケージファイルに格納されています.もう一つの選択肢はCythonです.これはPythonをCに拡張するモジュールで、開発者はPythonで直接Cのような文法を使って開発したり、間接的にC言語を使って開発したりすることができます.開発者が開発したCモジュールはPythonコードで呼び出すことができ、同時にこのCモジュールは実行環境上native binary code(x 86 Windowsプラットフォーム上ではx 86_PE形式、ARM Linuxプラットフォームではarm_elf、arm_eabiまたはその他)である.ある程度native binary codeは逆方向エンジニアリング者に対してより高い技術要求を提出し,逆方向エンジニアリングの難易度を増加させ,開発者コードの保護を実現した.しかし本質的には、Pythonコードではなく開発者のCコードにすぎない.Cythonを使用する欠点も明らかで、C言語の開発の難易度はPythonより著しく高く、C言語の開発モジュールもソフトウェア全体がプラットフォームをまたぐ特性を失った.Cythonがもたらす欠点を気にしないなら、Cythonを使ってCコードを保護するのは良い選択です.
<0 x 03>プライベートPython Bytecodeコマンドセットの使用
同じバージョンのPythonの場合、Pythonコンパイラ、解釈器、逆アセンブリ、および逆コンパイラは同じBytecodeコマンドセットを使用します.異なるバージョンのPythonでは、異なるBytecodeコマンドセットが使用されています.これもPython 2の理由です.Xコンパイラで生成されたpycファイルはPython 3にはできません.x解釈器が実行される理由の1つ.プライベートBytecodeコマンドセットを使用すると、通常のPython反アセンブリと反コンパイラは、プライベートPythonコンパイラによって生成されたpycファイル上で動作せず、Pythonコードを保護することになります.このような代価は、あなたのPythonアプリケーションがあなたのプライベートPython解釈器でしか実行できないことです.
<0 x 04>バイトコード混同
バイトコード混同は、通常の反アセンブリと反コンパイラを簡単に騙すことができ、コードの正常な実行に影響を与えない.次の例では、Uncompyle 6反コンパイラとdis反コンパイラを騙す方法を示します.
#     Python   sample1.py
print 'Hello World'

コンパイル:
python -m py_compile sample1.py

コンパイル後のsample 1.pycはPython内蔵disモジュール逆アセンブリを使用します.
>>> import marshal,dis
>>> fd = open('sample1.pyc', 'rb')
>>> fd.seek(8)
>>> sample1_code_obj = marshal.load(fd)
>>> fd.close()
>>> dis.dis(sample1_code_obj)
  1           0 LOAD_CONST               0 ('Hello World')
              3 PRINT_ITEM
              4 PRINT_NEWLINE
              5 LOAD_CONST               1 (None)
              8 RETURN_VALUE
>>>

以上のアセンブリコード筆者の肉眼反アセンブリの結果は以下の通りである.
0 LOAD_CONST     0 ('Hello World') #  co_consts[0]   ,co_consts[0]        'Hello World'
3 PRINT_ITEM                       #     sys.stdout, print 'Hello World'
4 PRINT_NEWLINE                    #     sys.stdout,    print           
5 LOAD_CONST     1 (None)          #  co_consts[1]   ,co_consts[1]   None
8 RETURN_VALUE                     #         ,             

次にsample 1を修正します.pycは、プログラムエントリに絶対ジャンプ命令を追加し(UltraEdit 16進挿入機能を使用してpycファイルを修正することができ、”JUMP_ABSOLUTE 3”Python 2.7に対応するバイトコードは0 x 71 0 x 03 0 x 00である.code string内容を修正すると同時にcode stringの長さを修正し、ここでは3バイト命令を追加する)、内蔵disモジュールを使用して逆アセンブリした結果は以下の通りである.
  1           0 JUMP_ABSOLUTE            3                 #    
        >>    3 LOAD_CONST               0 ('Hello World')
              6 PRINT_ITEM
              7 PRINT_NEWLINE
              8 LOAD_CONST               1 (None)
             11 RETURN_VALUE

読者がアセンブリコードについて一定の認識を持っている場合、ここでの絶対ジャンプはPython仮想マシンがこのプログラムを実行するのにほとんど影響しないことがわかります(命令実行サイクルを増やす以外は).しかし、この絶対ジャンプは逆コンパイラをだますことに成功します.Uncompyle 6を使用した逆コンパイルの結果は次のとおりです.
<<< Error: Decompiling stopped due to <class 'uncompyle6.semantics.pysource.ParserError'>

pycファイルが逆コンパイルされない場合、初級の解読者はこれに止まるかもしれませんが、経験のあるエンジニアには十分ではありません.同じように、通常の反アセンブリも動作しないようにしなければなりません.下のアセンブリコードで上のsample 1を加工し続けます.pyc.
|   1           0 JUMP_ABSOLUTE        [71 06 00]     6 
|               3 LOAD_CONST           [64 FF FF] 65535 (FAKE!)
|         >>    6 LOAD_CONST           [64 00 00]     0 (Hello World)
|               9 PRINT_ITEM           [47 -- --]
|              10 PRINT_NEWLINE        [48 -- --]
|              11 LOAD_CONST           [64 01 00]     1 (None)
|              14 RETURN_VALUE         [53 -- --]

以上の第2の命令はcode object定数テーブルの第65535項をスタックトップにロードすることを意味する.上記sample 1.pycでは、定数テーブルの長さが2で、下付き65535が定数テーブルの範囲を超えているため、これは不正な命令です.しかし、第1の絶対ジャンプが存在するため、第2の命令は永遠に実行されません.通常の逆アセンブリはdisのように有用な情報を全力で列挙するが、実際に実行される制御フローは理解できず、逆アセンブリが第2の命令を逆アセンブリしようとすると、code object定数テーブルの65535番目の項目を読み取り、「tuple index out of range」の意外なものを投げ出してみる.Python内蔵disモジュールのエラーメッセージは以下の通りです.
>>> fd = open('sample1.pyc', 'rb')
>>> fd.seek(8)
>>> import marshal,dis
>>> sample1_code_obj = marshal.load(fd)
>>> dis.dis(sample1_code_obj)
  1           0 JUMP_ABSOLUTE            6
              3 LOAD_CONST           65535
Traceback (most recent call last):
  File "", line 1, in 
  File "C:\Python27\lib\dis.py", line 43, in dis
    disassemble(x)
  File "C:\Python27\lib\dis.py", line 96, in disassemble
    print '(' + repr(co.co_consts[oparg]) + ')',
IndexError: tuple index out of range
>>>

現在、Uncompyle 6とdisはだまされており、コードは有効に保護されています.
<0 x 05>より多くのバイトコード混同技術
<0 x 05 0 x 01>ダミーブランチ
開発者はわざと複雑な分岐構造を構築することができるが,特定の分岐のみを上書きすることを予め条件を設けて実現することで,制御フロー解析ソフトウェアを用いても手動逆方向者の時間と精力を効果的に浪費することができる.
#flag          
#             
#              
if flag is condition:
    normal_processing()
else
    useless_but_complicated_obfuscating_code()
    or_even_invalid_code()
try:
    some_processing()
    raise_exeception = __import__('module_does_not_exist')
    #          'ImportError'except  
    useless_but_complicated_obfuscating_code()
except:
    continue_normal_processing()
try:
    some_processing()
    raise_exeception = __import__('sys').non_exist_function()
    #          'AttributeError'except  
    useless_but_complicated_obfuscating_code()
except:
    continue_normal_processing()
try:
    some_processing()
    raise_exeception = 1/0
    #          'ZeroDivisionError'except  
    useless_but_complicated_obfuscating_code()
except:
    continue_normal_processing()

<0 x 05 0 x 02>オーバーラップ命令
オーバーラップ命令(Overlapping Instruction)は、変長命令を有するCISC機器(例えばX 86)に広く応用されている.オーバーラップ命令をx 86アセンブリで例示する.
# 1     
00: EB 01           jmp  3
02: 68 c3 90 90 90  push 0x909090c3

# 1    
00: EB 01           jmp  3
03: C3              retn
# 2     
00: EB02                    jmp  4
02: 69846A40682C104000EB02  imul eax, [edx + ebp*2 + 0102C6840], 0x002EB0040

# 2    
00: EB02       jmp  4
04: 6A40       push 040
06: 682C104000 push 0x40102C
0B: EB02       jmp  0xF
# 3     
00: EBFF    jmp 1
02: C0C300  rol bl, 0

# 3    
00: EBFF    jmp 1
01: FFC0    inc eax
03: C3      retn

単一ジャンプ命令と比較して,オーバーラップ命令はジャンプに基づいて制御フローをさらに混同する技術手段であり,逆方向者に有効に対抗できる.Pythonバイトコードは、ARMのようなRISC命令と同様であり、その命令長は3バイトまたは1バイトであるが、任意にオーバーラップ命令を構築することができる.
# 1 Python     
 0 JUMP_ABSOLUTE        [71 05 00]     5 
 3 PRINT_ITEM           [47 -- --]
 4 LOAD_CONST           [64 64 01]     356
 7 STOP_CODE            [00 -- --]
# 1     
 0 JUMP_ABSOLUTE        [71 05 00]     5 
 5 LOAD_CONST           [64 01 00]     1
# 2 Python     
 0 EXTENDED_ARG         [91 00 64] 
 3 EXTENDED_ARG         [91 00 53]
 6 JUMP_ABSOLUTE        [71 02 00]
# 2     
 0 EXTENDED_ARG         [91 00 64] 
 3 EXTENDED_ARG         [91 00 53]
 6 JUMP_ABSOLUTE        [71 02 00]
 2 LOAD_CONST           [64 91 00]
 5 RETURN_VALUE         [53 -- --]

<0 x 06>手動逆工程に対抗
以上,機械(反コンパイラと反アセンブリ)をだます技術を示したが,人間をだます技術は存在しない.手動で逆方向を行いたい人にとって、唯一実行可能な手段は、逆方向の難易度と時間コストを増加させることです.より複雑な制御フローを導入すると、逆方向の難易度はわずかに増加しますが、あまり多くはありません.経験のある解読者は通常、コードに対して制御フロー分析ソフトウェアを使用します.コードの乱れは人間に対抗する道をもっと遠くに行くことができる.本物のアプリケーションコードは、pycファイルの1つまたは複数の文字列定数に暗号化されて格納することができ、プログラム実行時に暗号化されたアプリケーションコードを最初にスクランブル解除するスクランブル解除コードがあり、その後、本物のアプリケーションコードが実行される.丹念に設計されたスクランブルアルゴリズムは、解読者があなたのアプリケーションコードを静的に分析することに対抗することができます.次は簡単なコードスクランブルの例です.上のsample 1.pycを例に、スクランブルを行います.
>>> fd = open('sample1.pyc', 'rb')  
>>> fd.seek(8)  
>>> import marshal  
>>> co = marshal.load(fd)  
>>> fd.close()  
>>> code_string = marshal.dumps(co)  
>>> scrambled_code = code_string.encode('zlib').encode('base64')  
>>> print scrambled_code  
eJxLZoACRiB2AOJifiBRyMaQ8v9/CgODu0cKI0OwBhNIghtIeKTm5OQrhOcX5aT4aYC0oRHFXCAi  
MbcgJ9VIr6CyhAPItcnNTynNSbUD2VACUgQAIHcTlg==  

>>>

スクランブル後のコード列を次のdescrambleにコピーする.py中
scrambled_code_string='eJxLZoACRiB2AOJifiBRyMaQ8v9/CgODu0cKI0OwBhNIghtIeKTm5OQrhOcX5aT4aYC0oRHFXCAiMbcgJ9VIr6CyhAPItcnNTynNSbUD2VACUgQAIHcTlg=='  
exec __import__('marshal').loads(scrambled_code_string.decode('base64').decode('zlib'))  

descrmbleを実行します.py
>python descramble.py  
Hello World  

この簡単なスクランブルアルゴリズムを気にしないでください.この例はスクランブルの概念を示すだけです.
<0 x 07>後記
バイトコード混同(アセンブリ混同)はx 86プラットフォームですでに広く応用されており、新技術ではなく、Pythonに応用されているほか、バイトコード/アセンブリコードを使用するプログラミング言語も同様の原理でコード保護できるはずだ.