0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python の exe 配布は安全か?逆解析手法とビジネスロジック漏えいの実例

0
Last updated at Posted at 2026-08-22

この記事の目的と注意点

注意:本記事は、PyInstaller で作成した exe の解析方法を説明するものですが、攻撃手順の支援や悪用の手順を示すものではありません。

本記事の目的は、業務ロジックや重要な判定をクライアント側に埋め込む設計がいかに危険かを、技術的に理解してもらうための啓発です。

また、ここで扱うコードは、再現性のために簡略化したサンプルであり、実際の機密情報や本番ロジックを対象にしたものではありません。

背景

  • 仕事で PyInstaller で exe 配布する機会があり、PyInstaller の仕様を確認していた
  • 確認したところ、ビジネスロジックの漏えいにつながる可能性があり、実際に起こりうるかどうかを調査した

本稿では、PyInstaller で生成した実行ファイルがどのように Python のロジックを保持しているのか、そしてそのロジックがどこまで逆解析可能なのかを、実例を交えて確認する。

はじめに

PyInstaller は、Python スクリプトを実行可能な Windows の exe に変換するための便利なツールだ。だが、PyInstaller で生成した exe には Python のバイトコードが埋め込まれており、展開や逆アセンブルによってロジックを再構成できる場合がある。

本記事では、PyInstaller で生成した exe を対象に、どのような手順でリバースエンジニアリングが進むのか、そしてどこにリスクがあるのかを整理する。

1. PyInstaller の基本動作

PyInstaller は、Python プログラムとその依存ライブラリを 1 つの実行ファイルにまとめる。onefile 形式では、実行時に一時展開用ディレクトリが作成され、そこに Python ランタイムや zip アーカイブが展開される。

このとき、ユーザーの Python ソースそのものがそのまま見えるわけではないが、.pycPYZ アーカイブとして一時ディレクトリやメモリ上に出力される。

2. 逆解析に使われる典型的な手順

PyInstaller の exe を解析する流れは、実際には「EXE 本体を直接デコンパイルする」よりも、EXE 内に埋め込まれた Python アーカイブを展開してから pyc を取り出すというものが中心になる。

今回の実例では、抽出ツールとして C:\SourceCode\Python\pyinstaller_binaly_dump\pyinstxtractor.py を使った。このスクリプトの取得元は、GitHub の extremecoders-re/pyinstxtractor リポジトリである(https://github.com/extremecoders-re/pyinstxtractor)。

  1. EXE から PyInstaller アーカイブの抽出
  • pyinstxtractor.py を使って、exe 内に埋め込まれた PYZpyc を取り出す
  1. エントリポイントの特定
  • main.pyc / calc.pyc を探し、どれが実際の処理本体かを特定する
  • pyinstxtractor.pyPossible entry point: というログを出すため、pyiboot01_bootstrap.pycpyi_rth_inspect.pyc のような PyInstaller 実行時補助ファイルと、実際のアプリ本体である main.pyc を見分ける
  • 実際の抽出ログでは、次のような出力が現れる
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: main.pyc

このログは、PyInstaller のランタイム補助ファイルとアプリ本体コードを区別する手がかりになる。pyiboot01_bootstrap.pycpyi_rth_inspect.pyc は PyInstaller 自体の起動用コードであり、main.pyc が実際の処理本体であると推定しやすい。

  1. Python バイトコードの逆アセンブル
  • marshal.load() でコードオブジェクトを取り出し、dis.dis() によって CPython VM 命令列を表示する
  1. 実行フローの復元
  • 関数呼び出し、比較演算、ジャンプ命令を追って、元のロジックを人間が読める形に再構成する

ここで重要なのは、PyInstaller の exe は「そのままの機械語を逆コンパイルする」のではなく、Python の bytecode を取り出して解析することがほとんどだという点だ。

したがって、エントリポイントの特定は、単にファイルが見つかっただけでなく、「どれが PyInstaller の補助コードで、どれがアプリの処理本体か」を見分ける作業でもある。

3. なぜ PyInstaller で exe 化しても秘匿にならないのか

PyInstaller は「配布用の実行ファイル化」を目的としており、秘密保持を前提にした難読化ではない。結果として、次のような特徴がある。

  • Python のバイトコードが内部に残る
  • pyc ファイルが展開可能である
  • dis で opcode を読むことができる
  • Python 3.11 / 3.12 のようなバージョン差分を合わせて解析できる

つまり、ロジック自体が完全に隠蔽されるわけではなく、読み解く人がいれば再構成可能だ。

4. 実際に起こりうるリスク

4.1 ビジネスロジックの漏えい

PyInstaller で exe 化したアプリは、見た目は実行ファイルに見えていても、内部には Python の bytecode としてロジックが残る。抽出と逆アセンブルができれば、単なる計算処理だけでなく、業務ルール、閾値、判定条件、補正式、価格計算の仕組みまで再構成される可能性がある。

これは「アプリのロジックが外部に漏れる」という意味に直結する。四則演算などの単純な機能であっても、そこにビジネスルールが混ざっていれば、たとえ一見して普通の計算アプリに見えても、その判定基準や計算モデルは追跡可能だ。

特に、次のような内容は逆解析で取り出されやすい。

  • 閾値や境界条件
  • 誤差補正や変換式
  • 料金計算や割引のロジック
  • 判定の分岐条件
  • 重要な係数や定数

つまり、PyInstaller でクライアントに配布した exe に業務ロジックを埋め込むことは、事実上、ロジックをユーザー側に見せているのと同じだ。攻撃者が一度解析を始めれば、現実的には「元のソースを読む」よりも簡単に、実行ロジックを再構築できるケースが多い。

このため、PyInstaller で製品機能そのものをクライアント配布物に入れてしまう設計は、ビジネスロジックの秘匿性という観点では非常に危険である。業務上重要な判定や計算は、クライアント側ではなくサーバー側で処理し、クライアントには UI や入力補助程度の役割に留めるべきだ。

5. 実例: magic_fomul_calc を対象にした逆コンパイル

以下は、作成した magic_fomul_calc.exe を対象にして行った逆解析の例だ。

5.1 解析の流れ

今回の検証では、まず pyinstxtractor.py で EXE 内の PYZ / pyc を抽出し、main.pyccalc.pyc を特定したうえで、marshal.load()dis.dis() により Python VM バイトコードを出力した。

この手順は、PyInstaller の EXE をそのまま逆コンパイルするのではなく、埋め込まれた Python バイトコードを取り出して解析するという流れに一致する。

5.2 実際に出力された VM コード(全件)

以下は、pyinstxtractor.py により抽出した main.pyccalc.pyc から marshal.load()dis.dis() によって出力された VM コードの全体である。

#### C:\SourceCode\Python\pyinstaller_binaly_dump\magic_fomul_calc.exe_extracted\main.pyc ####

== <module> ==
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (0)
             4 LOAD_CONST               1 (('calculate',))
             6 IMPORT_NAME              0 (calc)
             8 IMPORT_FROM              1 (calculate)
          10 STORE_NAME               1 (calculate)
          12 POP_TOP

  4          14 LOAD_CONST               5 (('return', None))
             16 LOAD_CONST               3 (<code object run at 0x000001A470C3E0D0, file "main.py", line 4>)
             18 MAKE_FUNCTION            4 (annotations)
             20 STORE_NAME               2 (run)

  20          22 LOAD_NAME                3 (__name__)
             24 LOAD_CONST               4 ('__main__')
             26 COMPARE_OP              40 (==)
             30 POP_JUMP_IF_FALSE        8 (to 48)

  21          32 PUSH_NULL
             34 LOAD_NAME                2 (run)
             36 CALL                     0
             44 POP_TOP
             46 RETURN_CONST             2 (None)

  20     >>   48 RETURN_CONST             2 (None)

Disassembly of <code object run at 0x000001A470C3E0D0, file "main.py", line 4>:
  4           0 RESUME                   0

  5           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_CONST               1 ('Simple Calculator')
             14 CALL                     1
             22 POP_TOP

  6          24 LOAD_GLOBAL              1 (NULL + print)
             34 LOAD_CONST               2 ('入力例: 12 + 3  /  0.1 mf 3000  /  終了: q')
             36 CALL                     1
             44 POP_TOP

  8          46 NOP

  9     >>   48 LOAD_GLOBAL              3 (NULL + input)
             58 LOAD_CONST               3 ('> ')
             60 CALL                     1
             68 LOAD_ATTR                5 (NULL|self + strip)
             88 CALL                     0
             96 STORE_FAST               0 (text)

  10          98 LOAD_FAST                0 (text)
          100 LOAD_ATTR                7 (NULL|self + lower)
          120 CALL                     0
          128 LOAD_CONST               4 ('q')
          130 COMPARE_OP              40 (==)
          134 POP_JUMP_IF_FALSE        1 (to 138)

  11         136 RETURN_CONST             0 (None)

  13     >>  138 LOAD_FAST                0 (text)
          140 LOAD_ATTR                9 (NULL|self + split)
          160 CALL                     0
          168 UNPACK_SEQUENCE          3
          172 STORE_FAST               1 (left)
          174 STORE_FAST               2 (operator)
          176 STORE_FAST               3 (right)

  14         178 LOAD_GLOBAL             11 (NULL + float)
          188 LOAD_FAST                1 (left)
          190 CALL                     1
          198 STORE_FAST               4 (a)

  15         200 LOAD_GLOBAL             11 (NULL + float)
          210 LOAD_FAST                3 (right)
          212 CALL                     1
          220 STORE_FAST               5 (b)

  16         222 LOAD_GLOBAL             13 (NULL + calculate)
          232 LOAD_FAST                4 (a)
          234 LOAD_FAST                2 (operator)
          236 LOAD_FAST                5 (b)
          238 CALL                     3
          246 STORE_FAST               6 (result)

  17         248 LOAD_GLOBAL              1 (NULL + print)
          258 LOAD_FAST                6 (result)
          260 CALL                     1
          268 POP_TOP

  8         270 JUMP_BACKWARD          112 (to 48)

  == run ==
  4           0 RESUME                   0

  5           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_CONST               1 ('Simple Calculator')
             14 CALL                     1
             22 POP_TOP

  6          24 LOAD_GLOBAL              1 (NULL + print)
             34 LOAD_CONST               2 ('入力例: 12 + 3  /  0.1 mf 3000  /  終了: q')
             36 CALL                     1
             44 POP_TOP

  8          46 NOP

  9     >>   48 LOAD_GLOBAL              3 (NULL + input)
             58 LOAD_CONST               3 ('> ')
             60 CALL                     1
             68 LOAD_ATTR                5 (NULL|self + strip)
             88 CALL                     0
             96 STORE_FAST               0 (text)

  10          98 LOAD_FAST                0 (text)
          100 LOAD_ATTR                7 (NULL|self + lower)
          120 CALL                     0
          128 LOAD_CONST               4 ('q')
          130 COMPARE_OP              40 (==)
          134 POP_JUMP_IF_FALSE        1 (to 138)

  11         136 RETURN_CONST             0 (None)

  13     >>  138 LOAD_FAST                0 (text)
          140 LOAD_ATTR                9 (NULL|self + split)
          160 CALL                     0
          168 UNPACK_SEQUENCE          3
          172 STORE_FAST               1 (left)
          174 STORE_FAST               2 (operator)
          176 STORE_FAST               3 (right)

  14         178 LOAD_GLOBAL             11 (NULL + float)
          188 LOAD_FAST                1 (left)
          190 CALL                     1
          198 STORE_FAST               4 (a)

  15         200 LOAD_GLOBAL             11 (NULL + float)
          210 LOAD_FAST                3 (right)
          212 CALL                     1
          220 STORE_FAST               5 (b)

  16         222 LOAD_GLOBAL             13 (NULL + calculate)
          232 LOAD_FAST                4 (a)
          234 LOAD_FAST                2 (operator)
          236 LOAD_FAST                5 (b)
          238 CALL                     3
          246 STORE_FAST               6 (result)

  17         248 LOAD_GLOBAL              1 (NULL + print)
          258 LOAD_FAST                6 (result)
          260 CALL                     1
          268 POP_TOP

  8         270 JUMP_BACKWARD          112 (to 48)

#### C:\SourceCode\Python\pyinstaller_binaly_dump\magic_fomul_calc.exe_extracted\PYZ.pyz_extracted\calc.pyc ####

== <module> ==
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (0)
             4 LOAD_CONST               1 (None)
             6 IMPORT_NAME              0 (math)
             8 STORE_NAME               0 (math)

  4          10 LOAD_CONST               2 ('a')
             12 LOAD_NAME                1 (float)
             14 LOAD_CONST               3 ('b')
             16 LOAD_NAME                1 (float)
             18 LOAD_CONST               4 ('return')
             20 LOAD_NAME                1 (float)
             22 BUILD_TUPLE              6
             24 LOAD_CONST               5 (<code object add at 0x000001A470F9E3F0, file "calc.py", line 4>)
             26 MAKE_FUNCTION            4 (annotations)
             28 STORE_NAME               2 (add)

  8          30 LOAD_CONST               2 ('a')
             32 LOAD_NAME                1 (float)
             34 LOAD_CONST               3 ('b')
             36 LOAD_NAME                1 (float)
             38 LOAD_CONST               4 ('return')
             40 LOAD_NAME                1 (float)
             42 BUILD_TUPLE              6
             44 LOAD_CONST               6 (<code object sub at 0x000001A470F9E4C0, file "calc.py", line 8>)
             46 MAKE_FUNCTION            4 (annotations)
             48 STORE_NAME               3 (sub)

  12          50 LOAD_CONST               2 ('a')
             52 LOAD_NAME                1 (float)
             54 LOAD_CONST               3 ('b')
             56 LOAD_NAME                1 (float)
             58 LOAD_CONST               4 ('return')
             60 LOAD_NAME                1 (float)
             62 BUILD_TUPLE              6
             64 LOAD_CONST               7 (<code object mul at 0x000001A470F9E590, file "calc.py", line 12>)
             66 MAKE_FUNCTION            4 (annotations)
             68 STORE_NAME               4 (mul)

  16          70 LOAD_CONST               2 ('a')
             72 LOAD_NAME                1 (float)
             74 LOAD_CONST               3 ('b')
             76 LOAD_NAME                1 (float)
             78 LOAD_CONST               4 ('return')
             80 LOAD_NAME                1 (float)
             82 BUILD_TUPLE              6
             84 LOAD_CONST               8 (<code object div at 0x000001A470F9E660, file "calc.py", line 16>)
             86 MAKE_FUNCTION            4 (annotations)
             88 STORE_NAME               5 (div)

  20          90 LOAD_CONST               9 ('slip')
             92 LOAD_NAME                1 (float)
             94 LOAD_CONST              10 ('b_factor')
             96 LOAD_NAME                1 (float)
             98 LOAD_CONST              11 ('c_factor')
          100 LOAD_NAME                1 (float)
          102 LOAD_CONST              12 ('d_factor')
          104 LOAD_NAME                1 (float)
          106 LOAD_CONST              13 ('e_factor')
          108 LOAD_NAME                1 (float)
          110 LOAD_CONST               4 ('return')
          112 LOAD_NAME                1 (float)
          114 BUILD_TUPLE             12
          116 LOAD_CONST              14 (<code object pacejka_magic_formula at 0x000001A470C61370, file "calc.py", line 20>)
          118 MAKE_FUNCTION            4 (annotations)
          120 STORE_NAME               6 (pacejka_magic_formula)

  33         122 LOAD_CONST               9 ('slip')
          124 LOAD_NAME                1 (float)
          126 LOAD_CONST              15 ('peak_force')
          128 LOAD_NAME                1 (float)
          130 LOAD_CONST               4 ('return')
          132 LOAD_NAME                1 (float)
          134 BUILD_TUPLE              6
          136 LOAD_CONST              16 (<code object magic_formula_with_default_tire at 0x000001A470FA0300, file "calc.py", line 33>)
          138 MAKE_FUNCTION            4 (annotations)
          140 STORE_NAME               7 (magic_formula_with_default_tire)

  44         142 LOAD_CONST               2 ('a')
          144 LOAD_NAME                1 (float)
          146 LOAD_CONST              17 ('operator')
          148 LOAD_NAME                8 (str)
          150 LOAD_CONST               3 ('b')
          152 LOAD_NAME                1 (float)
          154 LOAD_CONST               4 ('return')
          156 LOAD_NAME                1 (float)
          158 BUILD_TUPLE              8
          160 LOAD_CONST              18 (<code object calculate at 0x000001A471000030, file "calc.py", line 44>)
          162 MAKE_FUNCTION            4 (annotations)
          164 STORE_NAME               9 (calculate)
          166 RETURN_CONST             1 (None)

Disassembly of <code object add at 0x000001A470F9E3F0, file "calc.py", line 4>:
  4           0 RESUME                   0

  5           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP                0 (+)
          10 RETURN_VALUE

Disassembly of <code object sub at 0x000001A470F9E4C0, file "calc.py", line 8>:
  8           0 RESUME                   0

  9           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP               10 (-)
          10 RETURN_VALUE

Disassembly of <code object mul at 0x000001A470F9E590, file "calc.py", line 12>:
  12           0 RESUME                   0

  13           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP                5 (*)
          10 RETURN_VALUE

Disassembly of <code object div at 0x000001A470F9E660, file "calc.py", line 16>:
  16           0 RESUME                   0

  17           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP               11 (/)
          10 RETURN_VALUE

Disassembly of <code object pacejka_magic_formula at 0x000001A470C61370, file "calc.py", line 20>:
  20           0 RESUME                   0

  27           2 LOAD_FAST                1 (b_factor)
             4 LOAD_FAST                0 (slip)
             6 BINARY_OP                5 (*)
          10 STORE_FAST               5 (bx)

  28          12 LOAD_FAST                3 (d_factor)
             14 LOAD_GLOBAL              1 (NULL + math)
             24 LOAD_ATTR                2 (sin)

  29          44 LOAD_FAST                2 (c_factor)
             46 LOAD_GLOBAL              1 (NULL + math)
             56 LOAD_ATTR                4 (atan)
             76 LOAD_FAST                5 (bx)
             78 LOAD_FAST                4 (e_factor)
             80 LOAD_FAST                5 (bx)
             82 LOAD_GLOBAL              1 (NULL + math)
             92 LOAD_ATTR                4 (atan)
          112 LOAD_FAST                5 (bx)
          114 CALL                     1
          122 BINARY_OP               10 (-)
          126 BINARY_OP                5 (*)
          130 BINARY_OP               10 (-)
          134 CALL                     1
          142 BINARY_OP                5 (*)

  28         146 CALL                     1
          154 BINARY_OP                5 (*)
          158 RETURN_VALUE

Disassembly of <code object magic_formula_with_default_tire at 0x000001A470FA0300, file "calc.py", line 33>:
  33           0 RESUME                   0

  35           2 LOAD_GLOBAL              1 (NULL + pacejka_magic_formula)

  36          12 LOAD_FAST                0 (slip)

  37          14 LOAD_CONST               1 (10.0)

  38          16 LOAD_CONST               2 (1.9)

  39          18 LOAD_FAST                1 (peak_force)

  40          20 LOAD_CONST               3 (0.97)

  35          22 KW_NAMES                 4 (('slip', 'b_factor', 'c_factor', 'd_factor', 'e_factor'))
             24 CALL                     5
             32 RETURN_VALUE

Disassembly of <code object calculate at 0x000001A471000030, file "calc.py", line 44>:
  44           0 RESUME                   0

  45           2 LOAD_FAST                1 (operator)
             4 LOAD_CONST               1 ('+')
             6 COMPARE_OP              40 (==)
          10 POP_JUMP_IF_FALSE       12 (to 36)

  46          12 LOAD_GLOBAL              1 (NULL + add)
             22 LOAD_FAST                0 (a)
             24 LOAD_FAST                2 (b)
             26 CALL                     2
             34 RETURN_VALUE

  47     >>   36 LOAD_FAST                1 (operator)
             38 LOAD_CONST               2 ('-')
             40 COMPARE_OP              40 (==)
             44 POP_JUMP_IF_FALSE       12 (to 70)

  48          46 LOAD_GLOBAL              3 (NULL + sub)
             56 LOAD_FAST                0 (a)
             58 LOAD_FAST                2 (b)
             60 CALL                     2
             68 RETURN_VALUE

  49     >>   70 LOAD_FAST                1 (operator)
             72 LOAD_CONST               3 ('*')
             74 COMPARE_OP              40 (==)
             78 POP_JUMP_IF_FALSE       12 (to 104)

  50          80 LOAD_GLOBAL              5 (NULL + mul)
             90 LOAD_FAST                0 (a)
             92 LOAD_FAST                2 (b)
             94 CALL                     2
             102 RETURN_VALUE

  51     >>  104 LOAD_FAST                1 (operator)
          106 LOAD_CONST               4 ('/')
          108 COMPARE_OP              40 (==)
          112 POP_JUMP_IF_FALSE       12 (to 138)

  52         114 LOAD_GLOBAL              7 (NULL + div)
          124 LOAD_FAST                0 (a)
          126 LOAD_FAST                2 (b)
          128 CALL                     2
          136 RETURN_VALUE

  53     >>  138 LOAD_FAST                1 (operator)
          140 LOAD_CONST               5 ('mf')
          142 COMPARE_OP              40 (==)
          146 POP_JUMP_IF_FALSE       12 (to 172)

  54         148 LOAD_GLOBAL              9 (NULL + magic_formula_with_default_tire)
          158 LOAD_FAST                0 (a)
          160 LOAD_FAST                2 (b)
          162 CALL                     2
          170 RETURN_VALUE

  55     >>  172 LOAD_GLOBAL             11 (NULL + ValueError)
          182 LOAD_CONST               6 ('unsupported operator')
          184 CALL                     1
          192 RAISE_VARARGS            1

  == add ==
  4           0 RESUME                   0

  5           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP                0 (+)
          10 RETURN_VALUE

  == sub ==
  8           0 RESUME                   0

  9           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP               10 (-)
          10 RETURN_VALUE

  == mul ==
  12           0 RESUME                   0

  13           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP                5 (*)
          10 RETURN_VALUE

  == div ==
  16           0 RESUME                   0

  17           2 LOAD_FAST                0 (a)
             4 LOAD_FAST                1 (b)
             6 BINARY_OP               11 (/)
          10 RETURN_VALUE

  == pacejka_magic_formula ==
  20           0 RESUME                   0

  27           2 LOAD_FAST                1 (b_factor)
             4 LOAD_FAST                0 (slip)
             6 BINARY_OP                5 (*)
          10 STORE_FAST               5 (bx)

  28          12 LOAD_FAST                3 (d_factor)
             14 LOAD_GLOBAL              1 (NULL + math)
             24 LOAD_ATTR                2 (sin)

  29          44 LOAD_FAST                2 (c_factor)
             46 LOAD_GLOBAL              1 (NULL + math)
             56 LOAD_ATTR                4 (atan)
             76 LOAD_FAST                5 (bx)
             78 LOAD_FAST                4 (e_factor)
             80 LOAD_FAST                5 (bx)
             82 LOAD_GLOBAL              1 (NULL + math)
             92 LOAD_ATTR                4 (atan)
          112 LOAD_FAST                5 (bx)
          114 CALL                     1
          122 BINARY_OP               10 (-)
          126 BINARY_OP                5 (*)
          130 BINARY_OP               10 (-)
          134 CALL                     1
          142 BINARY_OP                5 (*)

  28         146 CALL                     1
          154 BINARY_OP                5 (*)
          158 RETURN_VALUE

  == magic_formula_with_default_tire ==
  33           0 RESUME                   0

  35           2 LOAD_GLOBAL              1 (NULL + pacejka_magic_formula)

  36          12 LOAD_FAST                0 (slip)

  37          14 LOAD_CONST               1 (10.0)

  38          16 LOAD_CONST               2 (1.9)

  39          18 LOAD_FAST                1 (peak_force)

  40          20 LOAD_CONST               3 (0.97)

  35          22 KW_NAMES                 4 (('slip', 'b_factor', 'c_factor', 'd_factor', 'e_factor'))
             24 CALL                     5
             32 RETURN_VALUE

  == calculate ==
  44           0 RESUME                   0

  45           2 LOAD_FAST                1 (operator)
             4 LOAD_CONST               1 ('+')
             6 COMPARE_OP              40 (==)
          10 POP_JUMP_IF_FALSE       12 (to 36)

  46          12 LOAD_GLOBAL              1 (NULL + add)
             22 LOAD_FAST                0 (a)
             24 LOAD_FAST                2 (b)
             26 CALL                     2
             34 RETURN_VALUE

  47     >>   36 LOAD_FAST                1 (operator)
             38 LOAD_CONST               2 ('-')
             40 COMPARE_OP              40 (==)
             44 POP_JUMP_IF_FALSE       12 (to 70)

  48          46 LOAD_GLOBAL              3 (NULL + sub)
             56 LOAD_FAST                0 (a)
             58 LOAD_FAST                2 (b)
             60 CALL                     2
             68 RETURN_VALUE

  49     >>   70 LOAD_FAST                1 (operator)
             72 LOAD_CONST               3 ('*')
             74 COMPARE_OP              40 (==)
             78 POP_JUMP_IF_FALSE       12 (to 104)

  50          80 LOAD_GLOBAL              5 (NULL + mul)
             90 LOAD_FAST                0 (a)
             92 LOAD_FAST                2 (b)
             94 CALL                     2
          102 RETURN_VALUE

  51     >>  104 LOAD_FAST                1 (operator)
          106 LOAD_CONST               4 ('/')
          108 COMPARE_OP              40 (==)
          112 POP_JUMP_IF_FALSE       12 (to 138)

  52         114 LOAD_GLOBAL              7 (NULL + div)
          124 LOAD_FAST                0 (a)
          126 LOAD_FAST                2 (b)
          128 CALL                     2
          136 RETURN_VALUE

  53     >>  138 LOAD_FAST                1 (operator)
          140 LOAD_CONST               5 ('mf')
          142 COMPARE_OP              40 (==)
          146 POP_JUMP_IF_FALSE       12 (to 172)

  54         148 LOAD_GLOBAL              9 (NULL + magic_formula_with_default_tire)
          158 LOAD_FAST                0 (a)
          160 LOAD_FAST                2 (b)
          162 CALL                     2
          170 RETURN_VALUE

  55     >>  172 LOAD_GLOBAL             11 (NULL + ValueError)
          182 LOAD_CONST               6 ('unsupported operator')
          184 CALL                     1
          192 RAISE_VARARGS            1

5.3 対応する演算ロジックの出力例

上記の出力から、main.pycinput() で入力を受け取り、split()left operator right を分割していることが確認できる。また、calc.pyc 側では mf という演算子が calculate() の分岐に含まれていることが確認できる。さらに、ここで pacejka_magic_formulamagic_formula_with_default_tire といった関数名と係数が確認できる。これはタイヤ特性の数式モデルとして有名なマジックフォーミュラであると推測でき、タイヤ特性の数式モデルそのものが、実装コードと同じように読み取れていることが分かる。

もしこの数式がビジネス上の重要な評価式であった場合、あるいはほかに他社に知られたくないアルゴリズムであった場合、実行ファイルの中に埋め込まれているだけで、その式や係数、その計算ルールが流出していたということを意味する。

5.4 元コードとの比較: AI 復元結果と実在のソースの一致

ここでは、実際に元になった calc.py と、VM バイトコードから AI が復元したコードを比較する。ポイントは、「意味としてほぼ同等のロジックが読み取れる」という点だ。

元のソースコード(実際の calc.py):

import math


def add(a: float, b: float) -> float:
 return a + b


def sub(a: float, b: float) -> float:
 return a - b


def mul(a: float, b: float) -> float:
 return a * b


def div(a: float, b: float) -> float:
 return a / b


def pacejka_magic_formula(
 slip: float,
 b_factor: float,
 c_factor: float,
 d_factor: float,
 e_factor: float,
) -> float:
 bx = b_factor * slip
 return d_factor * math.sin(
     c_factor * math.atan(bx - e_factor * (bx - math.atan(bx)))
 )


def magic_formula_with_default_tire(slip: float, peak_force: float) -> float:
 # 代表的な係数例: B=10.0, C=1.9, E=0.97
 return pacejka_magic_formula(
     slip=slip,
     b_factor=10.0,
     c_factor=1.9,
     d_factor=peak_force,
     e_factor=0.97,
 )


def calculate(a: float, operator: str, b: float) -> float:
 if operator == "+":
     return add(a, b)
 if operator == "-":
     return sub(a, b)
 if operator == "*":
     return mul(a, b)
 if operator == "/":
     return div(a, b)
 if operator == "mf":
     return magic_formula_with_default_tire(a, b)
 raise ValueError("unsupported operator")

AI で VM バイトコードから復元したコード:

import math


def add(a: float, b: float) -> float:
 return a + b


def sub(a: float, b: float) -> float:
 return a - b


def mul(a: float, b: float) -> float:
 return a * b


def div(a: float, b: float) -> float:
 return a / b


def pacejka_magic_formula(slip: float, b_factor: float, c_factor: float, d_factor: float, e_factor: float) -> float:
 bx = b_factor * slip
 return d_factor * math.sin(c_factor * math.atan(bx - e_factor * (bx - math.atan(bx))))


def magic_formula_with_default_tire(slip: float, peak_force: float) -> float:
 return pacejka_magic_formula(
     slip=slip,
     b_factor=10.0,
     c_factor=1.9,
     d_factor=peak_force,
     e_factor=0.97
 )


def calculate(a: float, operator: str, b: float) -> float:
 if operator == '+':
     return add(a, b)
 elif operator == '-':
     return sub(a, b)
 elif operator == '*':
     return mul(a, b)
 elif operator == '/':
     return div(a, b)
 elif operator == 'mf':
     return magic_formula_with_default_tire(a, b)
 else:
     raise ValueError('unsupported operator')

比較すると、次の点でほぼ一致していることが分かる。

  • add, sub, mul, div が四則演算として定義されている
  • mf 演算子が magic_formula_with_default_tire() にマッピングされている
  • pacejka_magic_formula() が、b_factor, c_factor, d_factor, e_factor を使って計算している
  • main.py 側が text.split()left operator right を分割し、float() で数値化している

もちろん、AI による復元コードは元のソースの空白やコメント、引数の書式、ifelif の書き方などが多少異なるが、ロジックの本体と制御構造は実際のソースと同じものを指していることが分かる。

この比較から、PyInstaller で配布された EXE 内の Python バイトコードが、適切な手法で逆解析されれば、コードの骨格をかなり正確に再構成できることが確認できる。逆に言えば、ビジネスロジックをクライアント配布物に埋め込むことは、攻撃者にとって十分に可視化できる対象になるということだ。

6. なぜ対策が難しいのか

次のような対策はあるが、完全な防止は難しい。

  • 難読化をかける
  • Cython / Nuitka でネイティブ化する
  • 実行時にサーバー側で判定する
  • 重要ロジックをクライアントに置かない

ただし、いずれも「完全に安全」とまでは言えない。特に PyInstaller で配布する方式は、逆解析の観点で見れば、秘匿目的の保護には向いていない。

7. まとめ

PyInstaller による exe 化は開発効率が高い一方、「実行可能にした」だけで「秘匿した」わけではないという設計上の前提がある。

特に Python では、pycdis によってロジックを読み解くことが可能であり、ビジネスロジックを含む exe をクライアント配布物に置くことはリスクが高い。

業務上重要なロジックは、クライアント側に埋め込まず、サーバー側で管理・判定する設計に切り替えるのが適切だ。

8. 参考

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?