Go to list of users who liked
More than 5 years have passed since last update.
こんにちは。みなさんもウェブアプリをリリースしたあとに同業者にソースごとパクられたことってありますよね。難読化しても難読化されたまま同業者のサーバで動くので困ったものです。そこで、私がとった解析しずらい対策をまとめてみたいと思います。
前提
多機能な画面をJavaScriptでゴリゴリ作ったのにもかかわらず、HTMLやCSS、JavaScriptファイル一式を自社サーバにまるごとコピーして、ライセンス表記だけ書き換えて使うような業者を罠にはめるということを想定しています。
当然通信をリバースエンジニアリングする人もいるので、自社サーバでは防げないという前提です。
HTMLにはauthorメタタグ
よくあるMETAタグで権利者を明記します。これは権利の主張もそうですが、JavaScript自体に権利者が認定した権利者でなければ無限ループを起こすという処理のためにも使用します。逆に、権利者が我々にあるという状態でパクってもらう分にはよしと割り切ります。
<metaname="author"content="OreOre">
ランダムで無限ループを起こす
毎回決まったタイミングで発生したらプログラマはデバッグして対策しやすいです。なので、数%の確率で、数秒〜数百秒後にランダムで発生させるようにすると「あれ?うまく動いているじゃん」「あれ?動かなくなった」となります。プログラマが一番イヤな再現性がバラバラで低いというのを実現します。
無限ループするコードを非同期で読み込む
以下の例はconsole.log(1)
を実行していますがwhile(1);
にすると返ってこなくなります。
varscript=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で無限ループさせる
追記
はてブで面白いアイデアを頂いたので追記しますが、土日限定でトラブルを起こすようにするのもいいですね。サービスによっては土日はよく売り上げがあがるにも関わらず、エンジニアは休みを取っているということが多々あります。問い合わせが土日にきて、月曜日にエンジニアが調査したら再現しない。という感じになります。
更に追記
authorを取得して比較するだけであれば、以下のようなコードでいけますが、はっきり言ってバレバレです。
varauthor=document.querySelector("meta[name=author]").getAttribute("content");if(author=="OreOre Inc."){//ここで発動準備}
短く簡易的な独自なアルゴリズムでハッシュ化したauthorと固定の数字を比較したほうがバレにくいでしょう。
functiontest(c,h){c+="".repeat(4-c.length%4)for(vari=0,l=c.length,v=0x12345678,a=c.charCodeAt.bind(c);i<l;i+=4){//入力された文字でソルト0x12345678のxorを8bitごとに求めるv^=(a(i)&0xff)<<24;v^=(a(i+1)&0xff)<<16;v^=(a(i+2)&0xff)<<8;v^=(a(i+3)&0xff);//xorshift32と同じ計算でランダムな数字にするv^=v<<13;v^=v>>17;v^=v<<15;}returnv==h;}test(author/*"Ore Ore Inc."*/,-658575506);
このtest関数をuglifyすると以下のようになります。
functiontest(t,e){for(varr=0,n=(t+="".repeat(4-t.length%4)).length,a=305419896,h=t.charCodeAt.bind(t);r<n;r+=4)a^=(255&h(r))<<24,a^=(255&h(r+1))<<16,a^=(255&h(r+2))<<8,a^=255&h(r+3),a^=a<<13,a^=a>>17,a^=a<<15;returna==e}test(author,-658575506);
このコードだと、authorの値が"Ore Ore Inc."
であるか比較しているようには見えにくくなります。このコードに似たものをすでに実戦投入しています。このコードをfalseを取り出すためだけに一部業務ロジックで利用すると、確実に消せないコードになります。
開発環境で発生させないようにする(追記)
URLがlocalhostだったりIPアドレスだったりポート番号が80以外の場合には無視するというのも有効です。この場合「開発環境では問題ない」を実現しやすくなります。
曜日判定の隠蔽(追記)
普通にnew Date().getDay()
を実行して判定するとバレバレであるため、
//土日判定(JST)if((Date[30704..toString(36)]()/864e5-1.625)%7<2){//バグってたので修正//土日(JST)の場合のみ、左辺が0か1になる。//月曜日は2にになるのでこれで判定が可能。}
こんな感じのコードすると一見何をやっているかわからない感じになります。
evalの保護(追記)
window.eval = console.log
を実行されると、eval
部分でソースが出力されてしまいます。evalの書き換え前に実行できるのであれば、
Object.defineProperty(window,"eval",{configurable:false,writable:false,value:eval,})
これで保護しましょう。また、もしevalが書き換えられた場合は、
//evalが関数かつnativeなeval関数であることを確認して実行if(typeofeval=="function"&&eval.toString()=="function eval() { [native code] }"){eval(ソース);}
のようなコードで難読化されたコードが露見できないようにできます。
このコード自体も独自の難読化はしておきましょう。
普通のDOM操作に見える処理を利用してフックしてトリガー(追記)
どう見ても普通のDOM操作に見える以下の処理ですが、innerHTMLの処理を書き換えることで、例えば"bad"を代入するとそれをトリガーに何かをすることができます。
書き換え処理は予め別の場所で仕込んでおいて、innerHTMLへの代入は全然違うところで行うと隠蔽がしやすくなります。
document.body.innerHTML=xxx?"ok":"bad";
以下はinnerHTMLのフック処理
//ElementのinnerHTMLアクセッサを取得varinnerHTML=Object.getOwnPropertyDescriptor(Element.prototype,"innerHTML")//セッターを取得varoriginSet=innerHTML.set;//セッターを書き換えinnerHTML.set=function(string){//特定の値の場合をフックして何かするif(string=="bad"){alert(1);}//オリジナル処理を実行returnoriginSet.call(this,string);};//セッターを設定Object.defineProperty(Element.prototype,"innerHTML",innerHTML);
開発ツールのトラッキング(追記)
開発ツールが開いていることを検出するスクリプトがあります。開発ツールが開かれたら、トラップを発動させないようにすると、さらに対応しにくくなります。
デバッガでアタッチしたのをゆるく検知する(追記)
問題の動作を確認しようとしてデバッガでアタッチをすると、その間イベントループが停止します。その挙動を利用して、イベントループの時間が一定以上かかった場合はデバッガによるアタッチがあったと判断します。
純粋にたまたまPCが重たいときにはトラップが発動しなくなりますが、トラップが発動するわけではないので、この場合は問題ないと考えます。
varlast=Date.now();vartimer=setInterval(function(){varnow=Date.now();//どう考えても1イベントループで5秒もかかる処理がないのであれば//とりあえず5秒をしきい値にif(now-last>5000){//デバッガなどが原因で5秒以上停止があったとするconsole.log("トラップの発動をキャンセルする")clearTimeout(timer);return;}last=now;},1000);
アンチウィルスソフトの検出を誘う(追記)
無限ループやビットコインのマイニングの他に、アンチウィルスソフトを反応させることで、ウィルスが仕込まれたサイトだとユーザに誤解させてサイトから離脱させる手段です。
検出テストとしてEICARテストファイルと呼ばれるものがあります。この決められた内容のファイルについて、アンチウィルスソフトは必ず検出できなければならないとあります。
このファイルと同等な内容を返すURLは以下のとおりです。
data:application/octet-stream;base64,WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=
以下のようなコードでダウンロードさせることもできます。
//クロスブラウザ対応コードではないので調整してくださいvara=document.createElement("a");a.download="eicar.com";a.href="data:.....";a.click();
環境によってはアンチウィルスソフトが「ウィルスを検出しました!」と表示されます。
マルウェアなどで最近利用されている難読化ツール(追記)
最近メールの添付などで送られるJavaScriptで使用されている難読化ツールです。マルウェアは大体JScript判定を行って「特定のURLからexeファイルをダウンロードして実行するコマンドライン」をシェルで起動するということをやっています。
文字列なども難読化されるのですが、難読化された文字列を復号する処理において、過剰なスタックの消費とdebugger構文のインジェクトが行われるため、原則デバッグできないと考えて良いです。
ただ、こういうツールを使うと解析は防げても、明らかにコードを守りたいという意図が丸見えになってしまうため、使い方によっては一発で回避されます。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme