Javascriptでどのように文字や数値を難読化するか
SHINOVI (opens new window) ではさまざまなデータをJavascript向けに難読化しているので、ここではその中のいくつかのロジックについてまとめます。
# 変数の記号表現
いくつかの変数は下記のように記号で表すことができます。
console.log(![]) // false
console.log(!![]) // true
console.log(+[{}]) // NaN
console.log([]/[]) // NaN
console.log(!![]/[]) // Infinity
console.log(-!![]/[]) // -Infinity
console.log([][[]]) // undefined
console.log(~~[]) // 0
console.log(+![]) // 0
console.log(-~[]) // 1
console.log(-~-~[]) // 2
console.log(-~-~-~[]) // 3
2
3
4
5
6
7
8
9
10
11
12
13
実際に多くの難読化ツールで採用されている方法です。
# 文字列から特定の文字の取得
Javascriptでは、多くの変数は簡単に文字列に変換することができます。
console.log("" + false) // "false"
console.log("" + true) // "true"
console.log("" + undefined) // "undefined"
2
3
先程の記号表現と組み合わせると下記のようになります。
console.log(""+![]) // "false"
console.log(""+!![]) // "true"
2
さらに、Javascriptでは添字をつけるだけで文字列から特定の文字を取得できるため、先程の表現と合わせると下記のように必要な文字を抜き出すことができます。
console.log((""+![])[0]) // f
console.log((""+![])[1]) // a
console.log((""+![])[2]) // l
console.log((""+![])[3]) // s
console.log((""+![])[4]) // e
console.log((""+[][[]])[0]) // u
console.log((""+[][[]])[1]) // n
console.log((""+[][[]])[2]) // d
console.log((""+[][[]])[3]) // e
console.log((""+[][[]])[4]) // f
console.log((""+[][[]])[5]) // i
console.log((""+[][[]])[6]) // n
console.log((""+[][[]])[7]) // e
console.log((""+[][[]])[8]) // d
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 数を表現する
ある数を表現するために、より小さい数に分解する方法を考えます。
# 足し算
console.log(1 + 2 + 3) // 6
# 掛け算
console.log(2 * 2 * 2 * 3) // 24
さらに2のn乗はビット演算子で表すことができます。
console.log(3 * (2 ** 3)) // 24
console.log(3 << 3) // 24
2
# ビット演算子
上記に書いたように、2のn乗はビット演算子 <<
で表すことができます。
console.log(1 << 0) // 1
console.log(1 << 1) // 1 * 2 = 2
console.log(1 << 2) // 1 * 2 * 2 = 4
console.log(1 << 3) // 1 * 2 * 2 * 2 = 8
2
3
4
&
や |
を使えば、より多彩な表現が可能です。
// 14 -> 1110
// 7 -> 111
// 6 -> 110
console.log(14 & 7) // 6
// 15 -> 1111
console.log(14 | 7) // 15
2
3
4
5
6
7
8
# 演算子を組み合わせる
これらの方法を組み合わせることで下記のように表現することができます。
console.log(((1|2|4)+1)<<2 ) // 32
# 記号を組み合わせる
さらに数字を記号に置き換えることで下記のように表現することができます。
console.log(((-~[]|-~-~[]|(-~-~[]<<-~[]))+-~[])<<(-~-~[])) // 32
# 10進数と36進数の相互変換
Javascriptでは10進数と36進数の相互変換は簡単に行えます。
console.log((35).toString(36)) // "z"
console.log(parseInt("z" , 36)) // 35
2
下記のようにすることで、aからzまでのすべてのアルファベットを取得することができます。
console.log((62009168814).toString(36)) // "shinovi"
console.log(parseInt("shinovi" , 36)) // 62009168814
2
ただ、この方法で36進数を扱う場合、大きな数を扱うため、桁溢れが発生しないように注意が必要です。MDN (opens new window) ではNumber型は内部的には64ビットのdouble型と書かれてあり、実際に Number.MAX_SAFE_INTEGER
は (2^53 - 1)
であると記載されています。SHINOVI (opens new window) では数値をより安全に扱うため、32ビットの符号付き整数の範囲に収まるように設計しています。
# 2バイト以上の文字について
上記のように数値を表現することができましたので、漢字などの2バイト以上の文字についても String.fromCodePoint
を使うことで下記のように表現することができます。
String.fromCodePoint(23665,26412) // 山本
// The `apply` method is usable for an array of strings.
String.fromCodePoint.apply(null, [23665,26412]) // 山本
2
3
4