document

HTML5 & JavaScript side
twitter: @y_imaya

難読化されたJavaScriptコードを読む

はじめに

何らかの事情で JavaScript のコードを読み、その挙動について調査しなくてはいけないということはよくあると思います。
そんなとき、難読化やMinifyなどによって複雑怪奇に見えるコードに遭遇したという経験をもつひとも多いのではないでしょうか。

この記事はそんなコードにぶつかった際に、どうすれば良いのかを自分なりにまとめてみました。

また、jjencode 作者のはせがわようすけさんのJavaScript難読化読経というスライドを見て、
作者がネタバレしてるんだからこの辺の知見について書いても大丈夫だろうという気持ちで書き始めたことも一応記しておきます。

ツールによって難読化されたコードの読み方

まずはコードリーディング出来る状態にするため、よく見る2つのツールによる難読化のデコード方法について書いておきます。

Javascript Obfuscator

Javascript Obfuscatoreval(function(p,a,c,k,e,d){... から始まる特徴的なコードにエンコードするツールです。
このエンコードされた JavaScript コードは、先頭の evalconsole.log にするだけでデコードすることが可能です。
パッと見でのコードリーディングを防ぎたいというくらいの効果しかありません。

jjencode, aaencode, etc.

コードを読もうとしたとき、記号プログラミングの手法によって読みにくくなっていることがあります。
そうした場合は、開発ツールのコンソールを使ってデコードするところから始めます。
難読化ツールとしては jjencode が有名です。
これらの難読化ツールは基本的に number.constructor.constructor を使うことで Function を引き出し、Function(実行したいスクリプト本体文字列) とすることで任意のコードを実行しています。
booleanstring を使っても同様のことが出来るので、Number がハズレだったら StringBoolean を試すとよいでしょう)

蛇足かもしれませんが説明しておきますと、number.constructorNumber であり、Number.constructorFunction となります。

(0).constructor
=> Number() { [native code] }

(0).constructor.constructor
=> Function() { [native code] }

つまり、下記のように Number.constructorconsole.log にしてやることで、実行しようとしているコードを確認することができます。

Number.constructor = console.log.bind(console);

また、コードが何らかの悪意を持っている可能性がある場合は、下記のように debugger 文を使って実行を中断しましょう。(あるいは、例外を投げて失敗させても良いでしょう)

Number.constructor = function() {
    console.log(arguments);
    debugger;
};

記号プログラミングによる難読化手法は、より詳細な説明をはせがわようすけさんが資料を公開しているのでそちらを参照してください。

このように、jjencode などのエンコーダによる難読化は簡単にデコードできるため、解読を防ぐ用途での効果は期待しない方がよいでしょう。
実際、jjencodeには下記のように注意書きがされています。

Be aware

Using jjencode for actual attack isn't good idea.
- Decode easily. jjencode is not utilitarian obfuscation, just an encoder.
- Too characteristic. Detected easily.
- Browser depended. The code can't run on some kind of browsers.

Minify/Compile/Transpile されたコードの読み方

ここから、本来のコードリーディングの作業に入っていきます。
Minify/Compile/Transpile(以下、まとめてMinifyと表記します) されたコードをどのように追っていくかという話題です。
当然、SourceMap は提供されてないことを前提とします。

Pretty Print ツールによるソースコードの整形

まずは、ごちゃごちゃしているコードを Pretty Print (読みやすい形に変換)します。
Google Chrome などの開発者ツールについている Pretty Print でも良いのですが、後々のために Pretty Print した別のファイルにしておきます。

例えば、下記のようなコードを

function hello(a){alert("Hello, "+a)}hello("New user");

このようにきれいに整形してくれます。(例は Closure Compiler のオンラインサービス版の初期コード)

function hello(a) {
    alert('Hello, ' + a);
}
hello('New user');

Pritty Print する方法はいくつかありますが、自分は esprimaescodegen による Pretty Print スクリプトを愛用しています。

IDEによるリファクタリング

Pretty Print してしまえばあとは頑張ってコードを読んでいくだけなのですが、そのまま読んでいくのは人類には厳しいのでIDEのサポートを受けながらコードを読みやすい形に修正しながら読み進めていきます。
なぜIDEを使用するかというといくつかの理由がありますが、lintを使うなど他の環境でも同様のことができるなら問題ありません。
自分は WebStorm や Visual Studio を使っています。

unused variable の除去

未使用の変数は見つけ次第どんどん削除していきましょう。

dead code の除去

未使用の変数と同様に、到達しないコードもIDEが教えてくれるので削除していきます。

変数名/function名のリネーム

Minifyされたコードは変数やfunction名が a とか b といった短い名前に変更されています。
しかも、別のスコープでまた a とか b となっていくので、どれがどれだかややこしくなっています。
これをIDEのリファクタリング機能のリネームを活用することで分かりやすい名前にしていきます。
(どういう名前にするかは、コードをよく読んで推測するしかない)
単なる置換よりも、スコープなどを適切に判断してくれるIDEのリファクタリング機能の方が便利です。

変数の使いまわしの分離

Closure Compiler ではファイルサイズ優先の Minify を行うため、元々は別の変数だったものを同じ変数を再度利用することがあります。
(オプションで変更することは可能だが、オプションの変更を行うには自分でビルドしなおすか外部ツールを使用するしかありません)
そういった変数を見つけ出して、それぞれ別の変数に分離します。
簡単にですが見分けたい場合を書いておくと、数値をいれてる変数に文字列を入れるなど、別のものを同じ変数に入れていたら注意して読んだ方が良いでしょう。

6to5 に末尾呼び出し最適化が入ったと思ったら一瞬でなくなった

一昨日の記事を書いた後、末尾呼び出し最適化の実装にバグがあって速攻で消されたようです。

  • v3.5.0: 末尾呼び出し最適化の実装が入る
  • v3.5.1: 他のTailオブジェクトと混同して判定されてしまうため、Tail.prototype._isTailDescriptor というのを付けて対処
  • v3.5.2: _tailCall をつかった末尾呼び出し最適化がコメントアウト
  • v3.5.3: v3.5.0 以前の末尾再帰最適化の実装に戻す

少し見たところ、以下のようなケースでうまく動かないようです。

function foo() {
  return "foo";
}

function fooWrapper() {
  return foo();
}

function test() {
  var str = fooWrapper();
  if (str !== "foo") {
    throw new Error();
  }
}

function testWrapper() {
  return test();
}

testWrapper();

なぜこのケースでうまく動かないのか見ていきます。
このコードを 6to5(v3.5.1) で変換したものがこちらです。

"use strict";

var _tailCall = (function() {
    function Tail(func, args, context) {
        this.func = func;
        this.args = args;
        this.context = context;
    }
    Tail.prototype._isTailDescriptor = true;
    var isRunning = false;
    return function(func, args, context) {
        var result = new Tail(func, args, context);
        if (!isRunning) {
            isRunning = true;
            do {
                result = result.func.apply(result.context, result.args);
            } while (result instanceof Tail || result && result._isTailDescriptor);
            isRunning = false;
        }
        return result;
    };
})();

function foo() {
    return "foo";
}

function fooWrapper() {
    return _tailCall(foo);
}

function test() {
    var str = fooWrapper();
    if (str !== "foo") {
        throw new Error();
    }
}

function testWrapper() {
    return _tailCall(test);
}

testWrapper();

呼び出し順序は以下の順序です。

testWrapper
  _tailCall(test)
    test
      fooWrapper
        _tailCall(foo)

ここで問題となるのが _tailCall(foo) です。
これはもともと return foo(); なので "foo" が返るはずです。
しかし、この呼び出し順序では最初の _tailCall(test) が呼ばれた時に isRunnningtrue になるため、 _tailCall(foo) は Tail オブジェクトを返します。
var str = fooWrapper();str の値が Tail オブジェクトになってしまうため、変換前の結果と異なってしまいます。

今回のように他の末尾呼び出ししている function の戻り値を変数に代入するようなケースでもうまく動くような修正が入らなければ、再び末尾呼び出し最適化を有効にするのは難しそうな気配を感じます。
(現在はもともと入ってた末尾再帰最適化での高速化を行っているようです)
そのほかにもパフォーマンスの問題もあるので大変そうですが、はやく再び有効になると良いなあと思います。

6to5 に末尾呼び出し最適化が実装されたので調べてみた

@azu_re さんのツイートで知ったのですが、6to5 に末尾呼び出し最適化が追加されました。

ECMAScript 6 compatibility table によると ES6 transpiler では初めてテストを通った実装のようです。

6to5 ではどのような ECMA−262 5th のコードに変換して実現しているのか気になったので調査してみました。

末尾呼び出し最適化って何?

その前に「末尾呼び出し最適化って何?」って人のために簡単に説明しておきます。
通常、再帰するようなコードを書くとこんな感じになります。

function f(num) {
  var acc = arguments[1] || 0:
  return num <= 0 ? acc : f(num - 1, acc + num);
}

var result = f(5);
console.log(result);

この場合だと 5+4+3+2+1 を計算するコードですね。
f の呼び出しに注目すると、f(5) の中で f(4, 5) が呼ばれ、その中でさらに f(3, 9) が呼ばれ…という感じになっています。
この呼び出しをインデントで表すと以下のようになります。

result = f(5);
    return f(4, 5);
        return f(3, 9);
            return f(2, 12);
                return f(1, 14);
                    return f(0, 15);
                        return 15;

このように、再帰するたびにコールスタックが増えていきます。
コールスタックが増えるたびに f のスコープの変数なども保持していなくてはならないため、メモリをどんどん消費していきます。
(なお、ブラウザによりますがコールスタックがあまりに増えすぎると例外を投げて止まります)

それはよくないということで、return するときに function 呼び出している場合は現在のスコープを破棄して、呼び出している function の戻り値をそのまま呼び出し元に返すようにします。
これを末尾呼び出し最適化といいます。

あまり正確ではありませんが、前述の呼び出しに末尾呼び出し最適化を適用すると以下のようになります。
呼び出し自体は変わっていないのですが、呼び出し元がずっと同じイメージです。

result = f(5);
    return f(4, 5);
    return f(3, 9);
    return f(2, 12);
    return f(1, 14);
    return f(0, 15);
    return 15;

実際に変換してみる

元となるコードは下記の通り。

function f(num, acc=0) {
  return num <= 0 ? acc : f(num - 1, acc + num);
}

var result = f(5);
console.log(result);

これを 6to5 で変換したのがこちら。
_tailCall が一行になっていて読みにくいので Google Chrome の開発者ツールで整形しています。

"use strict";

var _tailCall = (function() {
    function Tail(func, args, context) {
        this.func = func;
        this.args = args;
        this.context = context;
    }
    var isRunning = false;
    return function(func, args, context) {
        var result = new Tail(func, args, context);
        if (!isRunning) {
            isRunning = true;
            do {
                result = result.func.apply(result.context, result.args);
            } while (result instanceof Tail);
            isRunning = false;
        }
        return result;
    };
})();

function f(num) {
    var acc = arguments[1] === undefined ? 0 : arguments[1];
    if (num <= 0) {
        return acc;
    } else {
        return _tailCall(f, [num - 1, acc + num]);
    }
}

var result = f(5);
console.log(result);

まず、return num <= 0 ? acc : f(num - 1, acc + num); の条件演算子が最適化され、retrun accreturn _tailCall(f, [num - 1, acc + num]); に分解されています。

ここからが末尾呼び出し最適化のキモで、_tailCall というメソッドが何を行っているのか見ていきます。

_tailCallfunc, args, context の3つの引数をとります。
func は元の function, args はその引数, context はおそらくレシーバを指しています。
これらは元となる function を function.prototype.apply で実行するのに不可欠だからです。
これらのコンテキストを Tail というオブジェクトにしています。

最初の呼び出しだけ do-while のループに入るようにして、次回以降はそのまま Tail オブジェクトを返すようにしています。
これは何をしているのでしょうか?
このコード例を元に条件文や変数を都度展開して追っていくと以下のようになります。

1. result = f(5);
    // f(5, 0)
    2. return _tailCall(f, [4, 5]);
        3. result = new Tail(f, [4, 5], undefined)
        4. result = result.func.apply(undefined, [4, 5])
            // f(4, 5);
            5. return _tailCall(f, [3, 9]);
                6. result = new Tail(f, [3, 9], undefined);
                7. return result; // Tail Object
        8. result = result.func.apply(undefined, [3, 9]);
            // f(3, 9);
            9. return _tailCall(f, [2, 12]);
                10. result = new Tail(f, [2, 12], undefined);
                11. return result; // Tail Object
        12. result = result.func.apply(undefined, [2, 12]);
            // f(2, 12);
            13. retrun _tailCall(f, [1, 14]);
                14. result = new Tail(f, [1, 14], undefined);
                15. return result; // Tail Object
        16. result = result.func.apply(undefined, [1, 14]);
            // f(1, 14);
            17. retrun _tailCall(f, [0, 15]);
                18. result = new Tail(f, [0, 15], undefined);
                19. return result; // Tail Object
        20. result = result.func.apply(undefined, [0, 15]);
            // f(0, 15);
            21. return 15; // not Tail Object
        22. return 15;

これを見るとわかるように、2回目以降の _tailCall 呼び出しはその場で function を呼び出すのではなく、Tail オブジェクトを返すようにすることでコールスタックが深くなっていくのを避けています。

追記(2015/02/10 13:00)

末尾呼び出し最適化の実装が不完全だったため v3.5.2 以降でこの実装は除外されました。
詳しくは別途記事を書いたのでそちらを見てください。

zlib.js の Inflate 実装を JSX-lang に移植しました

はじめに

JSX 速いという話を聞いたので、zlib.js を移植したらどうなるのか興味があったので試しました。

お詫び

JSXがリリースされた直後に少し触ってみたのですが、最初期のバージョンでは素のJSとの連携が取りにくかったので、自分の用途には少しマッチしないなと敬遠気味でした。
その認識は最近まで続いていたのですが、現在は export なども出来るようになっているのでライブラリなんかも簡単に作れるようになってます。
(おそらく実装されたのは 2013-04-30 にリリースされた v0.9.27 ?)

何度か飲み会などで「素のJSとの連携が取りにくいからなー」とか言ってしまって誤解を与えてしまったのをここでお詫びします。
(export できるのは @tkihira さんに出来ますよと言われてやっと気がついたので、身近にJSXの熟練者がいなかったのが勘違いを持続した一因だとおもってます。JSX詳しい人ってどのへんに生息してるんでしょうか…)

悩んだところ

Array.<number> と Uint8Array の適切な扱い

zlib.js は Array.<number> と Uint8Array どちらも扱えるようになっているので、JSXにおいてもその仕様は維持したいと思っていました。
しかし、Array と Typed Array はどちらも Arraylike だけど継承関係にないので扱いにくい感じです。
結局は以下のように ByteArray というラッパーモジュールを作って、それを通すことで解決しました。
この ByteArray は constructor を持たずすべてのメソッドが static function なので、このモジュールに対する副作用は存在しないため、おそらくコンパイラが適切に最適化してくれるだろうなと推測して書きました。
(出力されたJSを確認したところ、期待通りこのモジュールの function call はきちんと展開されていました。便利)

final class ByteArray {
  static function subarray(array: Array.<number>, start: number, end: number): Array.<number> {
    return array.slice(start, end);
  }

  static function subarray(array: Uint8Array, start: number, end: number): Uint8Array {
    return array.subarray(start, end);
  }
  ...
}

JS のライブラリとして使えるように export

以下のように __export__ 修飾子を付ければ良いようです。
また、ここでも Array.<number>Uint8Array の扱いに困りましたが、とりあえず別のプロパティとしてあつかって振り分ける事にしました。

import '../src/inflate.jsx';

__export__ class ZlibInflate {
  var array: Inflate.<Array.<number>>;
  var uint8: Inflate.<Uint8Array>;
  var typed: boolean;

  function constructor(input : variant, options : Map.<variant>) {
    if (input instanceof Array.<number>) {
      this.array = new Inflate.<Array.<number>>(input as __noconvert__  Array.<number>);
      this.typed = false;
    } else if (input instanceof Uint8Array) {
      this.uint8 = new Inflate.<Uint8Array>(input as __noconvert__ Uint8Array);
      this.typed = true;
    } else {
      throw new Error('invalid input');
    }
  }

  function decompress() : variant {
    return this.typed ? this.uint8.decompress() : this.array.decompress();
  }
}

そして、これを以下のように jsxjsx-linker を使ってビルドする。

$ jsx --release --minify export/inflate.jsx | ./node_modules/.bin/jsx-linker -t export-global --stdin -o inflate.js

これだけでライブラリとして使えるようになります。便利。

性能

どきどきしながら計測したんですが、zlib.js とあまり違いは見られませんでした。
以下のページで確認できます。

また、ファイルサイズにおいても Array.<number>Uint8Array を受け付ける設計になっていたので JSX 版はそれぞれの型でメソッドを生成する必要があるため、zlib.js と比べて単純に倍程度のファイルサイズになってしまいました。
余計なコードが入っている訳ではないのでこの出力結果はかなり良いと思います。

終わりに

そこそこ使い慣れている JavaScript と、初心者状態である JSX の実装を比較するのはあまりフェアではないような気がしますが、今回は JavaScript とあまり変わらない結果になってしまいました。
JSX 熟練者の方々からのツッコミなどお待ちしております。

#寿司js で Docker について雑に話してきました

#寿司js で Docker について雑に話してきました

寿司だ寿司だとつられていったらなぜかLTする雰囲気だったので慌てて資料をつくって、最近やっていたDockerでの環境構築について簡単に話してきました。

以下のような話題が話されていた気がします。すでにうろ覚えです。

  • promises
    • http://swipe.to/0990
    • 状態は一方向で基本的に使い捨て
    • 例外処理がきちんと出来る
    • 別に実装は含まれなくてもいいけど標準仕様は決めてほしい
      • 各実装でAPIばらばらでつらい
  • なごやLTとは
    • 突っ込みが無限に入るけど時間はゆるい
    • 東京も大きいところ以外は大体ゆるい
  • E2Eテストどうする
  • フレームワーク
    • Vue.js 流行ってる(ごく一部で)
  • hue (電球の方)
    • 微調整とか出来て良い
  • Firefox OS 国内でどれくらいシェアとるつもりなのか
    • キャリアの本気度が分からないと本腰をいれていいのか迷う
    • Tizen...
    • キャリア向けの説明資料とか見たい
    • 今度出る端末のスペックはゲーム動くレベルなの?
    • asm.js 効いてる…?
  • package.json などが更新されたら自動で npm install 等したいけどどう?
  • Docker めっちゃはまる
  • 寿司で学ぶ Web Components
    • x-sushi 要素とは...
    • 属性の有無で皿が消えたりする
    • Web Components が良いのは他のCSSの影響を受けない
    • 今後コンポーネントベースのUIフレームワーク増えそう
    • Polymer 使っておけばバックエンドがいつの間にか Web Components に切り替わってるとかありそう
記事検索
最新コメント
最新トラックバック
カテゴリ別アーカイブ
タグクラウド
QRコード
QRコード
  • ライブドアブログ