-
Notifications
You must be signed in to change notification settings - Fork 209
ASdeobf
Flash ( *.swf ) は、ツールを利用すれば容易にデコンパイルでき、処理に使われるソースコード( ActionScript, 以下 AS )を確認することができる。
しかし、時としてそれらのコードは難読化処理が施されており、一見デコンパイルに失敗したかのように見える謎の羅列になっていることがある。
これはその難読化コードをどうにか読み解いてみようという試みのまとめである。
慣れてくるとクロスワードパズルを解くような楽しさがあるので、ぜひ挑戦してみてほしい。
必要なもの、あったほうがいいものなどを挙げる。
- 難読化が施された ActionScript ソース・またはそれを含む Flash
-
Flash デコンパイラ
- hugflash, JPEXS Free Flash Decompiler など
- コードを取り出すために利用する
- hugflash はデコンパイル精度が高い
- JPEXS はGUIが親切であるが、肝心のソースコードのデコンパイルに失敗することがある
-
バイナリエディタ
- Stirling など
- 出力ファイルの検証に利用する
- コードを読みたいだけなら必要ない
-
FlashDevelop と Adobe AIR の実行・開発環境
- ASを実行できる
- 検算やファイル出力を行うときに利用
- インストール・使用方法は上記リンクを参照
- Flash で実行してもいいが、Adobe AIR ならファイル入出力や
trace()
が可能なのでそちらを推奨
-
エディタ
- Sublime Text, サクラエディタ など
- シンタックスハイライトと正規表現を利用した置換ができればよい
-
Google
- 上記ツールの導入・使用方法についてはこちらを参照
- 根気と好奇心
筆者は AS 初心者であるため、熟練者から見れば妙なことを書いているかもしれない。
厳密な仕様・用法については本家マニュアルを参照すること。
また、 ActionScript 3.0 を扱うものとする。
また、デコンパイル・難読化解除結果の取り扱いに関しては十分注意すること。
以下では、難読化コードを読むために必要な知識について紹介する。
他の言語をメインに扱っている初心者向けであるので、基本的文法・仕様が理解できているのであれば読む必要はない。
難読化コードを読む上で必須となるのが正規表現の知識である。
ASでは、以下のように表現される。
/.../( "Target String" )
new RegExp( "..." ).exec( "Target String" )
これはどちらも「先頭から3文字」を抽出するものであり、 "Tar"
が返される。
具体的な正規表現についてはASのマニュアルやサクラエディタのヘルプを参照すること。
なお、(某ゲームのスクリプトについては)実際に利用するものは少なく、以下の2つを覚えたうえであとは適宜調べる程度で十分である。
/.../ // 先頭からn文字(nはドットの数)
/...$/ // 末尾からn文字
// どちらもプロパティアクセスとして解釈される
this.member
this["member"]
// どちらもメソッド呼び出しとして解釈される
this.func();
this["func"]();
後者は文字列指定によって動的にアクセスすることができる。難読化にもってこいの機能である。
正確な仕様と詳細は公式マニュアルを参照すること。
また、hoge["constructor"]
とすることで、hoge
のクラス名を取得することができる。
401["constructor"] // == "[class Number]"
""["constructor"] // == "[class String]"
String["constructor"] // == "[class Class]"
"Script" の名前が示す通り、スクリプト系特有の緩い機能が多く存在する。
var key : Object = {};
key["["] = "Kitty on your lap"; //未定義の非英数字名メンバへの代入が可能
key[-1] = -1; //こんなこともできる
key[null] = null;
trace( /./( key[null] ) ); // "n" と表示される
このように、1つのオブジェクトに対してメンバを動的に追加していく手法はよく用いられる。
hugflash は大抵のコードを正確にデコンパイルできるが、一部では誤ったコードを生成することがある。
そのうちの一つとして、正規表現構文が挙げられる。 hugflash では以下のようなコードが出力される:
new RegExp( "..." )( "Target String" )
このままではコンパイルエラーになる(インスタンスに対して関数呼び出しを実行することはできない)。
そのため、エディタの正規表現置換を利用する。どちらでもよいが、リテラル形式のほうが読みやすい。
サクラエディタの場合は以下のように置換するとリテラル形式になる。
置換前:
new RegExp\("(.+?)"\)
置換後:
/$1/
また、一部の計算順序が正しくならないことがある。
一部の括弧が抜けてしまうことが原因と思われる。こちらも同様に置換しておくこと。
//デコンパイルした直後のコード
~this["constructor"]
!{}["constructor"]
//こちらが正しい
( ~this )["constructor"]
( !{} )["constructor"]
加えて、これらのコードを開発環境上で実行する際は、strict モードだとコンパイルできないことにも注意する。
FlashDevelop であれば、Project -> Properties -> Compiler Options -> Enable Strict Mode
を False
にすればコンパイルできるようになる。
ここでは手作業で難読化コードを読み解いていく手法について紹介する。
まず、以下のコードを見てほしい。
/.$/(/../(!(!{})))
これが何を意味するか、初見で理解できる人は少ないだろう。
このコードは以下のように解釈される。
/.$/( /../( !( !{} ) ) )
{} // object
!{} == false // 非 null のobject は bool にキャストすると true になる; その否定で false
!( !{} ) == true // さらに否定して true
/../( !( !{} ) ) == "tr" // 正規表現で先頭2文字をとって "tr"
/.$/( /../( !( !{} ) ) ) == "r" // さらに正規表現で末尾1文字をとって "r"
よって "r"
になる。
このようなものもある。同じく、順を追って考えてみる。
var a : int = /.$/( ~{} << ~[][{}] ) | ( ~this / ~this );
// 左側の括弧の中身
( ~{} << ~[][{}] )
~{} == -1 // int( object ) == 0; そのビット反転なので -1
~[][{}] == -1 // [][{}] == undefined; int( undefined ) は 0
( -1 << -1 ) == -2147483648 // 後述
/.$/( -2147483648 ) == "8" // 末尾1文字を取り出す
// 右側の括弧の中身
~this == -1 // int( object ) == 0
( -1 / -1 ) == 1
// 合わせて
"8" | 1 == 9 // 数値に変換される
// 最終的に
var a : int = 9;
以上から、 a
には 9
が代入される。
範囲外のシフトに関しては、第2項が shift % bit_length
( 負の時は結果に + bit_length
) として扱われるようである。
例えば、 int
であれば32bit長であるため、 a << -1
は a << 31
と等価である。
また、数値操作だけでなく、文字列操作も行うことがある。
[ /./( [][{}] ), /./( !{} ), /.$/( /../( {} ) ) ].join( "" )
/./( [][{}] ) == /./( undefined ) == "u"
/./( !{} ) == /./( false ) == "f"
/.$/( /../( {} ) ) ==
/.$/( /../( "[object Object]" ) ) == // String( {} ) == "[object Object]"
/.$/( "[o" ) == "o"
[ "u", "f", "o" ].join( "" ) == "ufo"
実際の難読化コードでは、これを応用してプロパティ・メソッド名を組み立て、 obj["property"]
のようにしてアクセスさせる用途で使われる。
型変換を多用するため、ルールを把握しておくこと。
以下に難読化によく使われる値や面白い値を示す。参考にしてほしい。
なお、値のチェックは実際に動作させて trace()
でみるのが一番楽なので、開発環境がある場合はそれを利用すると便利。
// 数値に変換できないものは 0 として扱われる
~this == -1
~undefined == -1
~null == -1
!{} == false
!( !{} ) == true
[][{}] == undefined
/./( [] ) == null
// 負数のシフト
-1 << -1 == -2147483648
-1 >> -1 == -1
-1 >>> -1 == 1
~this >>> this == 4294967295 // -1 >>> 0 と等価
// 正規表現の利用
/...$/( /...../( undefined ) ) == "def"
/./( String ) == "[" // String( String ) == "[class String]"
// 以降は厳密には文字列ではないが、基本的に正規表現と併用して文字列とみなして処理する
[{}] == "[object Object]"
[]["constructor"] == "[class Array]"
""["constructor"] == "[class String]"
/./["constructor"] == "[class RegExp]"
this["constructor"] == "[class (this のクラス名)]"
( ~this )["constructor"] == "[class Number]" // -1 の class
[]["constructor"]["constructor"] == "[class Class]"
"."["constructor"]( false )["length"] == 5 //new String( false ) == "false"
/./(/.. /({})) ==
/./(/.. /( "[object Object]" )) ==
/./( "ct " ) == "c" //正規表現にスペースを含めることでクラス文字列の中間にマッチさせる
31["toString"]( 32 ) == "v" // Number.toString( radix ) を利用する
{ a : function( a : int, b : int ) : int { return a + b; } }.a == "function Function() {}" // 関数の出力
/..$/( { /*(上に同じ)*/ }.a ) == "{}"
実際に使われているコードに似せた難読化コードを以下に示す。
decrypt_obf
は入力バイト列と出力バイト列を受けとり、入力列に何らかの処理を施して出力列に出力するものである。
主にオブジェクト key
に対して操作を行っていることは分かるだろう。実際の難読化でも用いられる手法である。
解読できるか試してみてほしい。
なお、これは人力で生成したものであるが、実際は何らかのツールで出力しているものと思われる。
package
{
import flash.utils.ByteArray;
public class challenge
{
public function challenge()
{
public function challenge()
{
}
public function decrypt_obf( input : ByteArray, output : ByteArray ) : void {
var key : Object = {
a : function( a : int, b : int ) : int {
return a + b;
},
b : function( a : int, b : int ) : int {
return a - b;
},
c : function( a : int, b : int ) : int {
return a * b;
},
d : function( a : int, b : int ) : int {
return a / b;
},
e : function( a : int, b : int ) : int {
return a % b;
},
f : function( ...rest ) : String {
return rest.join( "" );
},
g : function( a : int, b : int ) : int {
return a ^ b;
},
h : function( a : int ) : String {
return String.fromCharCode( a );
}
};
key[this] = key[/.$/(/...../([][{}]))](/./(/...$/({})), /.$/(/../({})), /.$/(/../([][{}])), /./(/..$/(!{})), /../(!(!{})), /./([][{}]), /../(/...$/(key)), /.$/(/../(this)), /.$/(/../(!(!{}))));
key[~this] = ~~this;
key[/.$/(/../(!(!{})))] = key[/.$/(/../(!{}))](~[][{}]/~/./([]), ~this >>> ~key);
key[/./([])] = /./(/....$/(""[key[this]]));
key[key[/./([])]] = key[/.$/(/..../(key[key[this]]))](key[/.$/(/../(!(!{})))], key[/.$/(/....../(key[this]))]);
key[/./(key)] = key[/./(key[this])](key[key[/./([])]], key[key[/./([])]]);
key[/.$/(~this << ~this)] = key[/./(key)] << key[key[/./([])]];
key[/./(/...$/(/./[key[this]]))] = /./(/...$/(this[key[this]]));
key[/./(/..$/([][key[this]]))] = key[/.$/(/...../([][{}]))](/.$/(/.../([][key[this]][key[this]])), /.$/(!(!{})), /./(/./([])), key[/./(/...$/(/./[key[this]]))], /./(!(!{})), /.$/(/........./(this[key[this]])));
key[/./(/....$/([][{}]))] = key[/.$/(/.../({}))](key[/.$/(~this << ~this)] >> (~this >>> ~this), (key[/./(key)] >> (~this >>> ~this)) | (~this >>> ~this));
key[/./(/....$/(""[key[this]]))] = key[/.$/(/........./(this[key[this]]))](key[/./(/....$/(""[key[this]]))]);
key[[][key[this]][key[this]]] = key[/./(!{})](/.$/(/......../((!{})[key[this]])), /./(/..$/([][key[this]])), /./(!(!{})), /.$/(!{}));
key[/..$/(key[/./(!{})])] = key[/./(!{})](key[/./(/....$/([][{}]))], /./(/....$/([][key[this]])), /./(/....$/(""[key[this]])), /./(!(!{})), /./(/....$/(key)), key[[][key[this]][key[this]]]);
key[/.../(key[/./(!{})])] = key[/./(!{})](/.$/(/../(!(!{}))), /.$/(!{}), /./(/....$/([][key[this]][key[this]])), /.$/(/.../([][{}])), key[[][key[this]][key[this]]]);
for( key[~this] = ~~this; key[~this] < input[key[/./(/..$/([][key[this]]))]]; key[~this] = key[/.$/(/../(!{}))](key[~this], key[/.$/(/.../([][{}]))](~null, ~this)) ) {
output[key[/..$/(key[/./(!{})])]](key[key[/./(/...$/(/./[key[this]]))]](input[key[/.../(key[/./(!{})])]](), key[/.$/(!(!{}))](key[~this], key[/.$/(~this << ~this)])));
}
}
}
}
}
解読のヒント:
key
には初期状態で複数の関数が登録されているが、3個以上の引数をとるものは f
のみである。
この関数が用いられる行は長くなりがちであるのでわかりやすい。
前項の値一覧を見ながら置換していけば値がつかめてくるはずである。
また、関数があるからといって必ず利用するわけではないことにも注意する。
詳細な解説は省くが、少し読みやすくするとこうなる。
public function decrypt_obf( input : ByteArray, output : ByteArray ) : void {
var key : Object = {
//省略
};
key["[object challenge]"] = "constructor";
key["-1"] = 0;
key["r"] = 2;
key["null"] = "i";
key["i"] = 4;
key["["] = 16;
key["8"] = 256;
key["x"] = "g";
key["y"] = "length";
key["i"] = 119;
key["i"] = String.fromCharCode( key["i"] ); // = "w";
key["[class Class]"] = "Byte";
key["{}"] = "writeByte";
key["fun"] = "readByte";
for( key["-1"] = 0; key["-1"] < input["length"]; key["-1"] = key["-1"] + 1 ) {
output["writeByte"]( input["readByte"]() ^ ( key["-1"] % 256 ) );
}
}
このコードの原文(decrypt_obf
にあたる部分のみ抜粋)を以下に示す。
実際の処理はたったこれだけである。
public function decrypt( input : ByteArray, output : ByteArray ) : void {
for ( var i : int = 0; i < input.length; i++ ) {
output.writeByte( input.readByte() ^ ( i % 256 ) );
}
}
ここで気を付けてもらいたいのが、難読化コード中で自分のクラス名を利用している点( challenge
の h
など )である。
また、 this.loaderInfo.url
を利用してファイル名 ( *.swf
) を参照するコードも確認されている。
そのため、解析プログラムを作成する際にはクラス名や出力swfのファイル名を一致させる必要がある。
実行の際は以上の点にも注意すること。
前項では手作業による難読化解除の手法を紹介したが、本来の目的が難読化解除自体にない場合、例えば前項のプログラムで言えば decrypt_obf()
を利用したファイルの入出力を行いたい場合などでは、わざわざこんな面倒な手順を踏む必要はない。
難読化されたメソッドを実行し結果を得たいだけなのであれば、今まで書いてきた「注意」に基づいて適宜難読化コードを置換し、開発環境上で再コンパイル・実行して結果を確認するだけである。
もう一度「注意」についてまとめてみる。
- 正規表現置換(
new RegExp("hoge")
→/hoge/
) -
~this
,!{}
を括弧でくくる - ファイル名・クラス名を元データと同じにする
- strict モードを解除する
置換の内容はデコンパイルソフトやソースの内容によって異なる。
基本的にコンパイルエラーにはならないはずなので、エラーが発生した場合はコードをよく読んで対策すること。
例えば、以下のようなコードでファイル入出力が可能である。
前項で示したような「ファイル単位の暗号化/復号化」などでは有効である。
なお、Adobe AIR 環境での実行を想定している。
// 入力ファイル名 bin 内に入れておく
var infile : File = new File( File.applicationDirectory.nativePath + "\\input.dat" );
var instream : FileStream = new FileStream();
// 入力データ
var indata : ByteArray = new ByteArray();
// ファイルのロード
instream.open( infile, FileMode.READ );
instream.readBytes( indata, 0, 0 );
instream.close();
// 出力データ
var outdata : ByteArray = new ByteArray();
// 復号化メソッド呼び出し
decrypt_obf( indata, outdata );
// 出力ファイル名 同じく bin 内に出力される
var outfile : File = new File( File.applicationDirectory.nativePath + "\\output.dat" );
var outstream : FileStream = new FileStream();
// 書き込み
outstream.open( outfile, FileMode.WRITE );
outstream.writeBytes( outdata, 0, outdata.length );
outstream.close();
注: このコードは例外処理を省略していたり、AIR の流儀に則っていなかったり(アプリケーションフォルダへの書き込みはダメらしい)する。 テスト実行にのみ利用可能であることに留意すること。
某ゲームのコア部分の復号化アルゴリズムについてであるが、2015/10/20現在、復号化を実行すると出力ファイル末尾が ファイルサイズ(Byte) % 8
バイトだけ欠損する不具合がある( 同日付時点では、size % 8 == 7
なので7byte欠損する )。
正常に読み込めるので問題はないのかもしれないが、気になる場合は出力データ( 前項のコードで言えば outdata
)にあらかじめ入力データをコピーしておくとよいかもしれない。
不明な点・誤記などあれば @andanteyk まで。