注意

このページに含まれる情報は直ちに悪用されうるものではありませんが
使い方によっては所謂チート行為等迷惑行為に繋がる恐れがあります。
そういった使い方をして発生した問題に対して当方では責任を負いかねます。

このページで目指すのは通常個別に探すしかないデータのアドレスを
効率よく発見するためのもの、または個人的に知的好奇心を満たすための
ものです。

知り得た情報を元に艦これの運営やサーバー管理、そして
ユーザーに迷惑がかかるような行為に発展させることのないよう
最新の注意を払ってくださいますようお願い申し上げます。


艦これのゲームシステムにおける、クライアント側の中核を担っているのが 
Core.swf です。Core.swf から各パーツが読み込まれて戦闘や補給といった
処理が読み込まれていく仕組みになっています。

音声や画像の読み込み処理などもこのCore.swfが行っているため
Core.swfを解析することで効率よくデータの収集等が行えます。

起動時の処理概要

ゲーム起動直後、ブラウザに読み込まれるのは
MainD2 というファイルです。所謂プカプカ丸が表示されているのが
この段階です。この時MainD2は艦これのサーバーからCore.swfを
ダウンロードし、ロードします。

ロードされたCore.swf は 「か・ん・こ・れ 始まります」 という
ボイスやその他起動に必要なデータを取得し、最後に
api_start2 という艦これのゲームAPIを呼び出し、
その結果を取得して、晴れて起動準備完了 ということになっています。
この api_start2 に艦これの各種画像、BGMやボイス、装備のパラメータなどなど
重要なデータが詰まっています。

よって、Core.swf と api_start2 を理解することで
艦これが提供する各種画像や音声へのアクセス方法を理解することができる
ようになります。
この内、api_start2はJSON形式のテキストデータであるため、容易に
内容を確認することができますが、Core.swfは暗号化され、そのまま入手しても
内容を閲覧することはできません。

Coreのデコード

暗号化されたCoreの内容を見るための作戦は大きく分けて2つあります。

  • 先にMainD2を調べてCore.swfの復号の仕方を解明する
  • MainD2にCore.swfを読み込ませ復号された段階でメモリをダンプし、復号済みCore.swfを発見する

この内、単純なのはメモリのダンプを行う方法で、新仕様ボイス解析 において
扱っています。ただ、この方法は、一回の復号に時間と手間がかかるため常に最新のデータを
展開したい場合少々手間となります。

よって、まずは内容を見ることができる MainD2を開腹し、暗号化されたCore.swfを取得し、
それを復号し、さらに読み込む までの一連の流れを理解する必要があります。

MainD2

MainD2の開腹自体は大したことではありません。
Widnwos環境ではhugflashのHugDimentionモード
Linux環境ではJPEXS
などを用いることで MainD2.swf内部でロジックを記述している
ActionScriptのソースをある程度復元することができます。

ActionScriptの難読化

MainD2の処理のほとんどは普通に読むことができますが、
最も重要なCoreの復号部分など、要所要所は読むことができない
記号の羅列のようになっています。
これは難読化処理といいます。

JavaScriptやActionScriptなど スクリプト言語と呼ばれるタイプの
言語は柔軟手軽で便利ではありますが、一方で、開発者以外にも
ソースコードが丸見えになってしまうという欠点があります。
よって、こういったスクリプト言語では 言語の柔軟さを活かした
難読化 という処理を行い、元のコードと全く同じ意味になるが
人間には非常に読みづらいコードを生成してそれを使う場合があります。

難読化の解除

難読化の解除にはActionScriptの仕様を十分理解する必要があるため
ここにはすべて書くことはできませんが概要を紹介します。

javascriptやActionScriptのスクリプト言語特有の機能に
文字列指定によるメンバー/メソッドへのアクセス 機能があります。

参考

例えば

hoge.func();
hoge["func"]();

はどちらも hogeのメソッド func() へのアクセスを行えます。
とすると例えば

hoge[(/../"funny") + (/.$/(/.../"bunny")) + /./"curry"]();

こうなふうに書くとどうでしょうか?
途端に意味がわからなくなります。
/../ の部分は 正規表現で文字列を処理しています。

/../ …文字列の先頭の2文字
/.$/ …文字列の末尾の1文字

などを意味しています。
正規表現は非常に複雑なのでここでは詳しく書きませんが
様々な分野で登場するので知っておいて損はなかもしれません。
さてこの一見意味不明な文字列ですが
これを解釈すれば

hoge[(/../"funny") + (/.$/(/.../"bunny")) + /./"curry"]();

→hoge["fu" + (/.$/"bun") + "c"]();
→hoge["fu" + "n" + "c"]();
→hoge["func"]();
→hoge.func();  

このようになりやはり同じ意味になります。

また、スクリプト言語は柔軟やキャスト機能を備えており
例えば
{} は空objectを意味しますが

/./(!{})

と書くとどうでしょうか?
まず ! 否定記号があるので {} (空Object) が bool型にキャストされ
ActionScriptの仕様では true と理解されます。
C言語からプログラムを始めた身には気持ち悪くて仕方ありませんが
これは正しく処理されます。
よって !{} は false を意味してしまいます。
次に /./ は先程の正規表現で直後の文字列の 先頭1文字を取得するので
falseがさらに String型にキャストされ "false" と理解されます。

よって

/./(!{})
→/./( false )
→/./"false"
→"f"

となります。
先ほどの例を改造すれば

hoge[(/./(!{})) + (/..$/(/.../"bunny")) + /./"curry"]();
→hoge["func"]()

こんな書き方が成立します。

他にも

foo[null] = "bunny";
foo[-10]  = "curry";

こんな書き方もC出身者には吐き気を催すほかないわけですが
スクリプト言語たるActionScriptは余裕で処理できます。

_[null] = "bunny";
_[-10]  = "curry";
hoge[(/./(!{})) + (/..$/(/.../_[null])) + /./_[-10]]();

こうなってくると読もうとすると混迷を極めます。
が、MainD2に使われている難読化の仕組みはまさにこれです。

結局

というわけで難読化解除の苦労がわかってもらえたかと思いますが
これを手分けして頑張って解読したわけです。( お疲れ様です)

そこから判明した MainD2による Core.swf の復号の手順はこうです
まず、 先頭の 128byteは Core.swf のヘッダなどがありこれには
手をつけません。 128byte 以降を いくつかのパートにわけて

暗号化データ[…128…| A | B | C | D | E | G | H ]

複合後データ[…128…| A | H |---------------| G ]

といったようにブロックごとにガバッと並べ替えて
並べ替えると通常の swf データになる という仕組みでした。

具体的には

 output.writeBytes(input,0,128) … inputの先頭0byte目から128byteをoutputに書き込み
 output.writeBytes(input, D, S) … inputの先頭Dbyte目からSbyteをoutputに書き込み
以下ブロックの数だけ同様の処理

このようにして input の一部を切り取って outputに連結していくようになっています。
また ブロックの分割数などはいつも一定なので ファイルサイズが変わった場合でも
D,Sがよろしく算出できるようになっています。

デコーダの製作

ここまで来たら

package {
    import flash.display.*;
    import flash.events.*;
    import flash.net.*;
    import flash.utils.*;
    import flash.system.*;
    import flash.text.*;
    import flash.events.KeyboardEvent;
    import flash.net.FileReference;

    public class mainD2 extends Sprite {

        private var _core:Loader;
        private var _core_encoded:URLLoader;
        private var Core_encoded_data:ByteArray;
        private var Core_decoded_data:ByteArray;

        public function mainD2(){

            addEventListener(KeyboardEvent.KEY_DOWN , KeyDownFunc);

            _core = new Loader();
            var val:* = new URLVariables();
            val.version = "khzhuwdqwlfd";
            var req:* = new URLRequest("http://203.104.248.135/kcs/Core.swf");
            req.method = "GET";
            req.data = val;
            var urlLoader:* = new URLLoader();
            urlLoader.dataFormat = "binary";
            urlLoader.addEventListener("complete", _ninja1);
            urlLoader.load(req);
         }

        private function _ninja1(event:Event):void{

            _core_encoded = URLLoader(event.target);
            _core_encoded.removeEventListener("complete", _ninja1);

            Core_encoded_data  = _core_encoded.data;
            Core_decoded_data = new ByteArray();
            var corekey:Object  = _createKey();
            _core.contentLoaderInfo.addEventListener("complete", _ninja2);

            var domain:LoaderContext = new LoaderContext();
            domain.applicationDomain = ApplicationDomain.currentDomain;

            var es:TextField = new TextField();
            es.text = "EncodedSize:"+Core_encoded_data.length;
            es.y=0;
            addChild(es);

            //Ninnin!!
            //___(Core_encoded_data, Core_decoded_data, corekey);
            hack(Core_encoded_data, Core_decoded_data);

            var ds:TextField = new TextField();
            ds.text = "DecodedSize:"+Core_decoded_data.length;
            ds.y = 15;
            addChild(ds);

        }

        private function _ninja2(event:Event):void{
            _core.contentLoaderInfo.removeEventListener("complete", _ninja2);

        }

        private function KeyDownFunc(e:KeyboardEvent):void{

          var fileRef:FileReference = new FileReference()
          //fileRef.save( Core_encoded_data,"Core_encoded.swf");
          fileRef.save( Core_decoded_data,"Core_decoded.swf");
        }

        private function hack(input:ByteArray, output:ByteArray):void
        {
            (DxやSの算出)

            out.writeBytes(infile, 0, 128);
            out.writeBytes(infile, D1, S);
            out.writeBytes(infile, D2, S);
            …  省略
        }

    }
}//package

このようなコードを試作しました。
これは非常に行儀が悪いのですぐに公開を取りやめましたが
一応使うことができます。
mainD2を模したコードで
まず艦これのサーバーから新鮮なCore.swfを取得し
取得が終わり次第 ninja1 が実行されます。

ninja1 はデバッグ用のメッセージの準備をしたあと
難読化解除の成果で手に入れた hack() を実行し 暗号化されたCore.swfを
復号し、待機します。

ユーザーが何らかのキー操作をすると KeyDownFunc が呼ばれ
復号済みのデータを Core_decoded.swf として保存する

という流れです。
Flex開発環境を用意して

mxmlc -strict=false -static-link-runtime-shared-libraries mainD2.as

↑こんな感じでコンパイルすると mainD2.swf が生成されるのでこれをブラウザに投げて
上記コードを走らせ、デコード済み Core.swf を取得できるわけです。

ただ、swfからローカルにデータを保存するFileReferenceは
微妙で、メインの処理からは呼び出し禁止、ユーザー操作に応じた部分でのみ
動くようで かなーり怪しい実装です。
実際OSやブラウザによって動かないパターンがあるようです。

それから

Core_decoded.swf は正しく並び替えがなされているので
普通のswfデータとして hugflashや JPEXSで展開
中を確認できます。

こうして艦これの新仕様ボイスの算出方法などが解明されました。
そちらのながれは
 こちら で紹介しています。

コメントを追加する