こんにちは。みなさんもウェブアプリをリリースしたあとに同業者にソースごとパクられたことってありますよね。難読化しても難読化されたまま同業者のサーバで動くので困ったものです。そこで、私がとった解析しずらい対策をまとめてみたいと思います。
HTMLにはauthorメタタグ
よくあるMETAタグで権利者を明記します。これは権利の主張もそうですが、JavaScript自体に権利者が認定した権利者でなければ無限ループを起こすという処理のためにも使用します。逆に、権利者が我々にあるという状態でパクってもらう分にはよしと割り切ります。
<meta name="author" content="OreOre">
ランダムで無限ループを起こす
毎回決まったタイミングで発生したらプログラマはデバッグして対策しやすいです。なので、数%の確率で、数秒〜数百秒後にランダムで発生させるようにすると「あれ?うまく動いているじゃん」「あれ?動かなくなった」となります。プログラマが一番イヤな再現性がバラバラで低いというのを実現します。
無限ループするコードを非同期で読み込む
以下の例はconsole.log(1)
を実行していますがwhile(1);
にすると返ってこなくなります。
var script = document.createElement("script");
script.src = "data:text/javascript;base64,Y29uc29sZS5sb2coMSk=";
document.head.appendChild(script);
開発ツールでもブレークポイントを設定しにくいのがポイントです。
独自の難読化を行う
無限ループを発生させるコードに独自の難読化を仕掛けます。要は何をやっているんか分かりにくくするためです。eval(src)
と書いてしまうと「あ、ここで実行しているな」と一目瞭然だったりします。
s = ...長い処理でスクリプト生成(この処理自体も適度に難読化)
a=[a=338403347140888..toString(31)][a=a+a[5]][a](s)();
わかりやすく書くと、こうなります。
//aに"constructo"を代入
//constructoという文字を31進数から10進数に変換すると338403347140888になる
//最後のrは精度不足で作れないので諦める
a=338403347140888..toString(31);
//aに"constructo" + "r"を代入
//諦めた最後のrを6文字目から取り出して結合
a = a + a[5];
//Functionを取り出して(String.prototype.constructor.constructor)、文字列を実行
//Function(s)と同じ
a["constructor"]["constructor"](s);
ちなみにツールでの難読化はやめましょう。デコーダが大抵あります。
無限ループのトリガーを工夫する
上記例では無限ループするスクリプトを動的にDataURLでロードしていますが、z=1
というコードにしつつ、全く別の所でwindow.zの値を監視して1になったら無限ループするというのもかなり追いにくくなるしょう。
ソース上に分散させる
1箇所に無限ループを発生させるコードをまとめてしまうと追いやすくなります。Webpackなどでビルドする際に、無限ループ発生に関わるコードの関数をバラバラな位置に登場するようにするとより追いにくくなります。
例えばscriptタグの生成と、srcの設定と、documentへ追加、それぞれを別々のモジュールにしつつ全然関係ない処理で最初に参照されるようにします。
特にWebpackとuglifyでビルドすると、何気ない処理が実は無限ループに関わっている、ということがより一層分かりにくくなります。
その他工夫など
- システム上重要そうに見えるフラグと無限ループのフラグを共有する
- metaタグのauthorではなく、location.hrefなどで判定する
- 無限ループではなくビットコインの発掘コードを発動させる
- メインではなくWebWorkerで無限ループさせる
- WebAssemblyで無限ループさせる
追記
はてブで面白いアイデアを頂いたので追記しますが、土日限定でトラブルを起こすようにするのもいいですね。サービスによっては土日はよく売り上げがあがるにも関わらず、エンジニアは休みを取っているということが多々あります。問い合わせが土日にきて、月曜日にエンジニアが調査したら再現しない。という感じになります。