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引数にHold
やUnevaluated
、HoldForm
を追加することで復元できる。
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]"]
)に、結果がPackage
やBinary
であれば、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]"
マルチバイト文字のコメントや文字列埋め込みはエラーが発生する。
コード中の128
を65536
に変えればマルチバイト文字も扱えるが、難読化後の文字数が大きく増えてしまう。
文字列を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引数にHold
、Unevaluated
、HoldForm
などを加えることで復元できる。
日本語などマルチバイト文字も問題ない。
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