Mathematicaプログラムの難読化・隠しメッセージ埋め込み手法の検討

Mathematicaのプログラムを難読化し、またそこに隠しメッセージを埋め込む手法をいくつか検討してみた。

円周率をモンテカルロ法で求めるプログラムを難読化し、隠しメッセージを埋め込む例をいくつか考えた。

そのままCompress

Compressに元のソースコードを式の形のまま渡す方法。

Compressに渡す前に評価されることを防ぐため、Unevaluatedで包んで渡す。

また、コメント((* ... *)は消えてしまうので、隠しメッセージは出力に影響しない文字列リテラルとして埋め込む。

難読化

Compress[Unevaluated["隠しMessage"; 4.*^-6 Count[RandomReal[1, {1*^6, 2}], a_ /; a.a < 1]]]

これを実行して以下の文字列を得る。

"1:eJxTTMoPSmNiYGAoFgISzvm5BfmleSmuFQVFqcXFmfl5wcJA4RgrSzNHgxgrYwNTc1+geGJ6KkQPK5AIycxNLS7qfbt1wfdjF+\
wQ4s5Ag0ogXC4gEZSYl5KfG5SamJPJCORCJFiAhE9mcUmmgxM/QyYTXJwTrD8vJbME6ASIEDuQCEgsKUktyisGGZCYxgCzySknMS8byUCgCyE8ZiDhkl8CUQ8hwZYDAIRsNw0="

実行

得られた文字列を使って実際に円周率をモンテカルロ法で求めるには以下を実行する。

Uncompress["1:eJxTTMoPSmNiYGAoFgISzvm5BfmleSmuFQVFqcXFmfl5wcJA4RgrSzNHgxgrYwNTc1+geGJ6KkQPK5AIycxNLS7qfbt1wfdjF+\
wQ4s5Ag0ogXC4gEZSYl5KfG5SamJPJCORCJFiAhE9mcUmmgxM/QyYTXJwTrD8vJbME6ASIEDuQCEgsKUktyisGGZCYxgCzySknMS8byUCgCyE8ZiDhkl8CUQ8hwZYDAIRsNw0="]

復元

元のソースコードはUncompressの第2引数にHoldUnevaluatedHoldFormを追加することで復元できる。 Uncompressの仕様を知っている必要があるので少し復元難易度は高め。

Uncompress["1:eJxTTMoPSmNiYGAoFgISzvm5BfmleSmuFQVFqcXFmfl5wcJA4RgrSzNHgxgrYwNTc1+geGJ6KkQPK5AIycxNLS7qfbt1wfdjF+\
wQ4s5Ag0ogXC4gEZSYl5KfG5SamJPJCORCJFiAhE9mcUmmgxM/QyYTXJwTrD8vJbME6ASIEDuQCEgsKUktyisGGZCYxgCzySknMS8byUCgCyE8ZiDhkl8CUQ8hwZYDAIRsNw0=", \
Hold]

これを実行すると、隠しメッセージを含む以下の文字列が得られる。

Hold["隠しMessage"; 4.*10^-6 Count[RandomReal[1, {1000000, 2}], a_ /; a.a < 1]]

文字列をCompress

ソースコードを文字列として表記したものに対してCompressを行う。

文字列なのでコメント(* ... *)として隠しメッセージを埋め込み可能。 一方で文字列リテラルの表記時にはエスケープが必要。

難読化

Compress["(*隠しMessage*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"]
"1:eJxTTMoPCnZlYGDQ0IqxsjRzNIixMjYwNfdNLS5OTE/V0jTR04rTNVNwzi/NK4kOSsxLyc8NSk3MiTbUqTbUijPTMaqN1UmM17dO1Eu0MYwFAEGrFkI="

実行

Uncompressの第2引数にToExpressionを付けて文字列を評価することで実行できる。

Uncompress["1:eJxTTMoPCnZlYGDQ0IqxsjRzNIixMjYwNfdNLS5OTE/V0jTR04rTNVNwzi/NK4kOSsxLyc8NSk3MiTbUqTbUijPTMaqN1UmM17dO1Eu0MYwFAEGrFkI=", \
ToExpression]

復元

復元するにはUncompressの第2引数をなくせばよいので、隠しメッセージの存在に気づくのは比較的容易。

Uncompress["1:eJxTTMoPCnZlYGDQ0IqxsjRzNIixMjYwNfdNLS5OTE/V0jTR04rTNVNwzi/NK4kOSsxLyc8NSk3MiTbUqTbUijPTMaqN1UmM17dO1Eu0MYwFAEGrFkI="]
"(*隠しMessage*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"

文字列をBase64エンコード

元のコードを文字列にした後、ExportStringを使ってbase64エンコーディングする。

base64以外に、"UUE"(uuencode)を使うことや、base64エンコーディング前にgzipやbzip2で圧縮することも可能。

難読化

ExportString["(*隠しMessage*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]", "Base64"]
"KCpcOjk2YTBcOjMwNTdNZXNzYWdlKik0LipeLTYgQ291bnRbUmFuZG9tUmVhbFsxLHsxKl42LDJ9XSxhXy87YS5hPDFd"

実行

ImportString["KCpcOjk2YTBcOjMwNTdNZXNzYWdlKik0LipeLTYgQ291bnRbUmFuZG9tUmVhbFsxLHsxKl42LDJ9XSxhXy87YS5hPDFd", "Base64"]

元のコードをStringFormatで判定したとき(この例ではStringFormat["(*隠しMessage*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"])に、結果がPackageBinaryであれば、ImportStringだけで実行できる。

StringFormatの結果がTextのときは、評価してもらうにはToExpression@ImportStringにする必要あり。

復元

ImportStringの第2引数を{"Base64", "Text"}{"Base64", "String"}にすることで復元できる。 Mathematicaの知識が必要なので復元難易度は高め。

ImportString["KCpcOjk2YTBcOjMwNTdNZXNzYWdlKik0LipeLTYgQ291bnRbUmFuZG9tUmVhbFsxLHsxKl42LDJ9XSxhXy87YS5hPDFd", {"Base64", "Text"}]
"(*\\:96a0\\:3057Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"

マルチバイト文字の復元にはもう少し工夫が必要。

StringReplace[
 ImportString["KCpcOjk2YTBcOjMwNTdNZXNzYWdlKik0LipeLTYgQ291bnRbUmFuZG9tUmVhbFsxLHsxKl42LDJ9XSxhXy87YS5hPDFd", {"Base64", "Text"}], 
 RegularExpression["\\\\:([0-9a-f]{4})"] :> FromCharacterCode[FromDigits["$1", 16]]]
"(*隠しMessage*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"

その他、シェルからbase64 -dを使っての復元も可能。

$ echo 'KCpcOjk2YTBcOjMwNTdNZXNzYWdlKik0LipeLTYgQ291bnRbUmFuZG9tUmVhbFsxLHsxKl42LDJ9XSxhXy87YS5hPDFd' | base64 -d
(*\:96a0\:3057Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]

文字列を数値化

Mathematicaは任意精度の数値を扱えるので、ソースコードの文字列を巨大な整数にしてしまう。

難読化

FromDigits[ToCharacterCode["(*Hidden Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"], 128]
229025575843061500623667720149804056387348841423920634106087079222372159235189920877325936071698569549777922771072905304603495249156317

実行

ToExpression@FromCharacterCode@IntegerDigits[
 229025575843061500623667720149804056387348841423920634106087079222372159235189920877325936071698569549777922771072905304603495249156317, 
 128]

復元

ToExpressionを取ることで元のコードを復元可能なので、難易度としては一番容易。

FromCharacterCode@IntegerDigits[
 229025575843061500623667720149804056387348841423920634106087079222372159235189920877325936071698569549777922771072905304603495249156317, 
 128]
"(*Hidden Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"

マルチバイト文字のコメントや文字列埋め込みはエラーが発生する。

コード中の12865536に変えればマルチバイト文字も扱えるが、難読化後の文字数が大きく増えてしまう。

文字列をBase64エンコードした後に数値化

上記2つの組み合わせで、復元の難易度を上げる。

難読化

FromDigits[ToCharacterCode@ExportString[
 "(*Hidden Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]","Base64"], 128]
2629108794144899453206565807086524923112452908759067533058518\
6181764566380851641685704793089640780996201346372998332701326457951078\
92857724507826978543035873377491375992673611568234835697290

実行

ImportString[FromCharacterCode@IntegerDigits[
 2629108794144899453206565807086524923112452908759067533058518618176\
4566380851641685704793089640780996201346372998332701326457951078928577\
24507826978543035873377491375992673611568234835697290, 128], "Base64"]

"Base64"という文字列があるのが気になるので、ここも数値化してしまうと以下。

ImportString[FromCharacterCode@IntegerDigits[
 2629108794144899453206565807086524923112452908759067533058518618176\
4566380851641685704793089640780996201346372998332701326457951078928577\
24507826978543035873377491375992673611568234835697290, 128], IntegerString[683248828, 36]]

復元

復元のためにはImportStringの第2引数を{"Base64", "Text"}にする必要があり、知識が必要。

ImportString[FromCharacterCode@IntegerDigits[
 2629108794144899453206565807086524923112452908759067533058518618176\
4566380851641685704793089640780996201346372998332701326457951078928577\
24507826978543035873377491375992673611568234835697290, 128], {"Base64", "Text"}]
"(*Hidden Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]"

Compressして得た文字列を数値化

Compressとの組み合わせも可能。

難読化

FromDigits[ToCharacterCode@Compress@Unevaluated["隠しMessage"; 4.*^-6 Count[RandomReal[1, {1*^6, 2}], a_ /; a.a < 1]], 128]
47224907448643461475265088304432125824433960941734592263479665963636068878549156319601496150674939543046241216351891323501758922386376405555\
39419466703945617677752873083017382003874101197375102487690082325907181737700875326800175602588529877917615505274828352328524963877963948153\
70007822692008952608741014933162209546168886313396376869558127053938117595375491826819346105617431232359869428669660450805679620041160759413\
4115154251728302145825976783417854583251715238155343820178593703129897021

実行

Uncompress@FromCharacterCode@IntegerDigits[
 47224907448643461475265088304432125824433960941734592263479665963636068878549156319601496150674939543046241216351891323501758922386376405\
55539419466703945617677752873083017382003874101197375102487690082325907181737700875326800175602588529877917615505274828352328524963877963948\
15370007822692008952608741014933162209546168886313396376869558127053938117595375491826819346105617431232359869428669660450805679620041160759\
4134115154251728302145825976783417854583251715238155343820178593703129897021, 128]

復元

Uncompressの第2引数にHoldUnevaluatedHoldFormなどを加えることで復元できる。

日本語などマルチバイト文字も問題ない。

Uncompress[FromCharacterCode@IntegerDigits[
 47224907448643461475265088304432125824433960941734592263479665963636068878549156319601496150674939543046241216351891323501758922386376405\
55539419466703945617677752873083017382003874101197375102487690082325907181737700875326800175602588529877917615505274828352328524963877963948\
15370007822692008952608741014933162209546168886313396376869558127053938117595375491826819346105617431232359869428669660450805679620041160759\
4134115154251728302145825976783417854583251715238155343820178593703129897021, 128], Hold]
Hold["隠しMessage"; 4.*10^-6 Count[RandomReal[1, {1000000, 2}], a_ /; a.a < 1]]

関数化

「Compressして得た文字列を数値化」の処理を関数化する。

Attributes[Obfuscate] = {HoldFirst};
Obfuscate[exp_] := "Uncompress[FromCharacterCode[IntegerDigits[\n" <> 
  ToString[FromDigits[ToCharacterCode@Compress@Unevaluated[exp], 128]] <> ",\n128]]]"
Obfuscate["隠しMessage"; 4.*^-6 Count[RandomReal[1, {1*^6, 2}], a_ /; a.a < 1]]
"Uncompress[FromCharacterCode[IntegerDigits[
47224907448643461475265088304432125824433960941734592263479665963636068878549156319601496150674939543046241216351891323501758922386376405555\
39419466703945617677752873083017382003874101197375102487690082325907181737700875326800175602588529877917615505274828352328524963877963948153\
70007822692008952608741014933162209546168886313396376869558127053938117595375491826819346105617431232359869428669660450805679620041160759413\
4115154251728302145825976783417854583251715238155343820178593703129897021, 128]]]"

Encode (おまけ)

難読化

Encodeは一時ファイルを作成する必要あり。

tmp1 = FileNameJoin[{$TemporaryDirectory, "temp.txt"}];
tmp2 = FileNameJoin[{$TemporaryDirectory, "temp_enc.txt"}];
Export[tmp1, "(*Hidden Message*)4.*^-6 Count[RandomReal[1,{1*^6,2}],a_/;a.a<1]", "Text"];
Encode[tmp1, tmp2];
Import[tmp2]
"(*!1N!*)mcm
j<]ahZS>kA6>`B)A>gkBmqf3\\@<,rR9d>-J[i!5&u|2Y YC@hh_Hk/HgDS50`La'  "

実行

Getでファイルを読み込むことで実行できる。

tmp3 = FileNameJoin[{$TemporaryDirectory, "enc.txt"}];
Export[tmp3, "(*!1N!*)mcm\nj<]ahZS>kA6>`B)A>gkBmqf3\\@<,rR9d>-J[i!5&u|2Y YC@hh_Hk/HgDS50`La'  ", "Text"];
Get[tmp3]

復元

ドキュメントに「Mathematica には,エンコードされたファイルをもとの形式に変換するための機能は組み込まれていない.」と記載があり、復元方法はない。

そのため埋め込んだ隠しメッセージを表示することはできない。

難読化パッケージ

GitHubに以下の難読化パッケージを公開

330k/mathematica-obfuscator: Mathematica code obfuscation package