注意:今回の記事は、原理等未確認な状況です。
「結果的に、こういうことなのだろう」という状況証拠的なものなので、間違っている可能性も多分にあります。
今回の検体のハッシュ値:
MD5:545BFDC9B1976AE0003443FF4F90EB7E
SHA1:92E8CE006BB3C4A1DDB8D8BA8DE3A90C0BBB6326
マルウェア解析中に far call に当たる
とある検体(Emotetらしい?)を分析していたところ、解析回避処理を抜けてsvchost.exeをsuspendで作成した後あたり(この辺が分かる人はwktkすることはお察し)で、メモリ内にデコードしたコードをコールするにあたり、「far call」をしているのを見つけました。
16ビット時代ならともかく、32ビット以降ではえらく珍しいな、と思った次第です。
しかし、その後は解析に問題が発生し、色々調べても答えが出ず、試行錯誤でコードを解析した結果、ある仮説を立てました。
「このfar call の直後、このコードは32ビットから64ビットコード実行に変わる」
な・・・なにを言っているのかわからねーと思うが、俺も何をされたのか分からなかった・・・。
しかし、ソースを素読みしたり、実際にこの先を64ビットのデバッガで調査すると上手くいったりするので、間違いはなさそうです。
そもそも far call ってなんぞ?
というか、最近の人はそもそも far call 知らないかもしれませんね。
call は、C/C++のコードでいうところの「関数」や「API」を呼び出す際に、実際に呼び出す二モニックコードです。
リバースエンジニアリングで intel系 CPUをやっている人は知らないことはないでしょう。
ただ、通常見かける call は、いわゆる 「near call」 です。
これは、「現在のセグメントのメモリ領域内へコールする」というものです(もしかすると厳密に言うと違うかもしれませんが、少なくとも私はそう理解している)。
この場合、call先の指定は相対(現在のアドレスからのオフセット)と絶対(直接アドレス指定、ただし仮想アドレス)の2種類になります。
しかし、上図の赤線部分では、「call far ptr 33h:5930011h」となっています。
これが「far call」です。
far call も厳密に言うとreal address の直接コールとプロテクトモード(protected mode)がありますが、これはアプリケーション内の call のため、今回は後者のプロテクトモードのみ対象として考えます。
この場合の呼び出しは、「セレクタの指定した上位アドレス + コールアドレス」のアドレスを呼び出すことになります(ここも間違ってたらゴメンナサイ)。
なぜこんな仕組みになっているのか?というと、最も大きな要因は16ビット時代の名残で、互換として残っているからだと思っています。
16ビットで表現できるのは、 2の16乗=65,536=64KB です。
流石に、16ビット機が登場した頃でも、メモリのアドレスが64KBでは流石に足りませんでした。
そこで、素直に上下2つ繋げて32ビットにしときゃよかったものを20ビット(1MB)でメモリのアドレスを表現し、上位16バイトと下位16バイトを合算する仕組みになりました。
つまり、真ん中の12ビットは重複する仕組みです。
例えば、20ビットのアドレスとして 「0x10200」を表現する場合、 上位 0x1020、下位 0x0000でも、上位 0x1000、下位0x0200 でも、同じアドレスを示すということです。
こういった仕組みにしたため、この時に「セグメント」という考え方が生まれました。
素直に上下2つ繋げて32ビットにしなかったのは、当時の技術では「4GB?パーソナルコンピュータのメモリでそんなに大量のメモリなんか考えられん!」というのが最大の理由な気はします。当時、補助記憶装置がフロッピーやカセットテープで、それですら数MBがいいところなのに、メモリでそんなサイズになるとは想像できなかった、ってことかなと思います。
これが現代のCPUにも残っています。
現在では、「セレクタ」に上位オフセットアドレスを入れ、far call でセレクタとアドレス(以上の説明でいうところの下位アドレス)を指定することで、同様のことを実現している、ということのようです。
「ということのようです。」と書いたのは、自分が32ビット、64ビットでプログラムを作るときには使うことがなかったため、ドキュメントに書いてある内容を理解してかみ砕くとそういうことのようだ、と知識で得ただけにすぎないためであり、またその理解も、CPUが専門ではないためあまり正確ではないのではないか、という面があるためです。
このコードを実行するとどうなるか?
ちょっと far call の(内容が若干あやしい)うんちくが入りましたが、では上図のコードを実行するとどうなるでしょうか?
少なくとも、IDAでこのまま実行すると、プログラムがクラッシュして終了します。
何故かというと、far call で使用しているセレクタの「0x33」にパラメータが設定されていないからです。
あれ?と思って解析したコードをある程度見直しましたが、セレクタにパラメータを設定している箇所が見つかりませんでした(まあ、見落としの可能性も多分にあるわけですが)。
そのため、IDAでのデバッグでも不正なセレクタ参照でクラッシュしてしまうのでしょう。
対応:セレクタにパラメータを設定する
「セレクタに値が入っていないなら、そこにパラメータをいれてやればいいじゃない。」(アントワ発言)
ということで、セレクタにパラメータをいれてやりましょう。オフセットアドレスがいくつになるのか、という問題がありますが、それまでの解析の結果、0x00・・・000(オール0)のアドレスに、続きの処理があることは分かっています。
なら、near call でいいんじゃない?と言われれば、アドレスの面から言えばそのとおりです。
そのため、それでも far call を使った理由があるはず・・・というのが今回の問題なのですが。
セレクタにパラメータを設定するには、IDAのメニューから「View」→「Open Subviews」→「Selectors」でセレクタの画面を開きます。
そこで右クリックでポップアップメニューを表示し、「Add Selector...」をクリックしてセレクタ入力ダイアログを開きます。
今回は、セレクタの値を「0x33」、アドレス値をオール0にしてOKを押し、登録します。
これで、確かにセレクタを使った far call は実行できるようになります。
・・・しかし、この先のコードが上手く解析できません。
far call 後のコードの謎
far call でコールした後の処理は、確かにIDAでデバッグを継続はできます。
しかし、追っていてもどうも実行している処理が腑に落ちません。
「こんな処理するか?」という処理をしていき、実際に値がおかしくなってクラッシュします。
far call 後の本格的なコードは以下のようになっています。
素で読んでいても目立つ、不自然な「dec eax」。
コードを見ても、これらのタイミングでeaxを減算する意味がわかりません。
そして、実際にデバッガで追って行っても、この「dec eax」で値がおかしくなっているのは明らかです。
これらが「nop」だったほうが、まだちゃんと動くレベルです。
では、これらのコードは一体なぜこんなことになっているのでしょうか?
長くなってきたので、ここで一度切ります。