前々回記事および前回記事で、マルウェアの筆跡鑑定やマルウェア作成者のプロファイリングができないか研究している、という記事を書きました。
今回は、1つの関数についてそれをやってみた結果の記事となります。
コード全体をやると結構な量になるうえ、それが論文のテーマなので、部分的に「こんな感じで分析している」ということの紹介程度になります。
今回もmemsetの例とします。
特にこだわりが深いわけではないですが、前々回記事でも出したのでそちらとの比較もできること、メモリの0クリアで利用されることが多いため多くのマルウェアで出現が期待できることが挙げられます。
今回は、REvilとみられるマルウェアの検体から抽出したmemset処理です。
対象のハッシュ値は以下のとおりです。
MD5:9CD5E68A49CEEFC94E2E09F9AE9A861A
SHA1:A0FF0573E3167E4E49F91ECFFD6B82D302890E3B
※注意事項:
memsetの例ですが、マルウェアの作成者の直接のコーディングではなく、既存のライブラリから関数単位で挿入された可能性も残されています。
本来、既存のライブラリのコードだった場合は、「そのライブラリのmemsetを利用している」だけの情報にすべきですが、現在のところそこまでできておらず、解析技法を確立する上でその対応が課題となっています。
今回の記事は、「マルウェアの作成者が書いた」体で進めます。 orz
※注意事項その2:
ASLRが有効な場合、検証のためのリトライでアドレスが変わってメンドくさいので、ASLRをOFFにしています。
そのため、以上で提示したマルウェアのヘッダパラメータのうち、「DllCharacteristics」の「IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE」をOFFに変更したものを掲載しています。
(え?誰も検証しない?ソンナサミシイコトイワナイ)
検体の中からmemset処理を発見
当該検体から見つかったmemset処理関数(sub_4046F1)は以下のようになっていました。
ポイントになる部分は以下の点です。
- 第3引数のチェックをしており、0の場合はなにもせず第1引数のアドレスをリターンする。
- 第2引数を指定領域にセットするが、4バイト単位で書き込みを行っている。そのため、書き込むデータを1バイトから4バイトに拡張するために0x01010101と掛け算をするというテクニックをつかっている。
- 実際のデータのセットのために、rep stosd、rep stosbを用いている。rep stosdで4の倍数の領域にeax(4バイト)のデータをセットし、領域が4の倍数のサイズではない場合にrep stosbでal(1バイト)の値を設定するよう細工されている。
- リターン値は第1引数と同じ値が返るため、標準のmemsetに準拠している。
コードはコンパクトに実装しています。
また、可能な限り4バイトで処理することで高速化も図られています。
前々回記事のEmotet2020年春バージョンでは、1バイトずつのセットを繰り返していたので、大きな領域に対しては効率はこちらのほうが良いことが予想されます。
引数もリターン値もmemsetと同じになっており、「memset関数を使用している箇所は単純に置き換え」しても問題がない設計になっています。
処理としてはBlueNoroffと似ていますが、BlueNoroffの方法はオンコードでサイズも固定であることに対し、こちらは汎用性が高い作りとなっています。
感想として、「汎用性が高く、memsetとinput、outputのインターフェスが同じで使い勝手がよく、中身もシンプルかつコンパクトで見やすくまとまっている。」ことから、このコードの作成者のスキルは高いだろう、と考えられます。
それでもツッコミを入れるSachiel
さて、じゃあ私が同じコードを書くか?というと、そうはならないって話です。
(まあ、記事としても、「作成者の差」がないとお話にならないんですけどね)
このコード、確かに良くできているのですが、「自分が書く場合にはここはこうするだろう」、という点がみられました。
まず1つめとして、「私がオンコードで書くなら、第1引数にNULLチェックはいれる」というのがあります。
これは、標準のmemset関数ではやっていないかもしれません(やっちゃいけなくて、なんなら例外発生させないといけないのかもしれません)。
ただ、もし「マルウェアを作る立場」なら、エラーで何かユーザに見えるメッセージやらクラッシュはして欲しくないんですよ。
そうであれば、不用意なクラッシュを回避するために「NULLチェックをマメに入れる」ということは考慮される場合があります。
もちろん呼び出し側がちゃんとチェックする、またはNULL以外を保障してればいいのですが、「関数を作る側」として、「呼び出し側の人」をソコまで信用していないんですよ、私。
これは、昔やっていたプログラム開発現場での苦い経験が蓄積され蓄積され蓄積され蓄積され蓄積された結果、そういう「癖」がついてしまっているのです。
2つめとして、セットされる1バイトの値を4バイトに拡張する処理で、「0x01010101で掛け算する」処理は、私はやらないと思います。
もし私が実装する場合、「シフトとorで4バイトに拡張するように書く」と思います。
これは、私が「古いプログラマだから」というのが大きいです。
少なくとも、私がアセンブラで直接組んでいた頃(あるんかいw)は、imulは結構重かったんですよ。
これは、命令によって1命令あたり必要とする時間が異なる、ということが理由で、命令数が少なければ速いってものじゃなかったということがあります。
その結果、例えば符号なし整数値を2倍するなら、imulではなく1ビット左シフトを使うとか、しょっちゅうやってきました。
コプロセッサを使う場合に至っては、コプロセッサに処理を投げた後の待ちの間に別のコードを数ステップいれるなど、今思い出すと気が狂ってるんじゃないかというくらいコマゴマやっていた「経験」があります。
そのため、「シフトとorで4バイトに拡張するように書いたほうが速い」と考えてコードを書きそうなのです。
その分、命令数が増えるため、プログラムが長くなりますが、そこは処理速度とのバーターとなります。
実際には、現代のCPUではアウトオブオーダー処理などがあって、もっと効率化して処理するとかもありそうなので、実際はさほど差がでないかもしれませんが。(もうそこまでいくと、人間の思考の限界にくるんじゃないかな・・・)
こういった「作成者のポリシー」や「作成者の経験」が、同じようなコードであっても実装の方法を変えてしまうことが考えられます。
そして、それは結果としてコードに表れるのです。
こういった情報を集めることで、「類似性の有無で筆跡鑑定」、「実装の内容から作成者の考え方やポリシーのプロファイリング」、「実装にあたって用いられているテクニックや洗練さで作成者の知識・技能の推察」ができるんじゃないか、ということです。
マルウェア作成者のプロファイルと筆跡鑑定情報の抽出
前回記事では、マルウェア解析で以下の4つの観点の情報が得られるのではないか、と推測しました。
- マルウェア作成の思想・目的(大きな意図)
- 設計・技術的ポリシー(小さな意図)
- マルウェア作成者の知識・技能
- マルウェア作成者のコーデイングの癖(マルウェア作成者の筆跡)
このうち、マルウェアの利用者・攻撃者のプロパティである「マルウェア作成の思想・目的」は、マルウェア全体の目的を調べる必要があるため、一関数のみである今回は省略します。
設計・技術的ポリシー(小さな意図)
- 汎用性の高いプログラムを作成
- memset関数と同じ仕様とすることで、利用や移行が容易
- OSの提供する関数(API)や名前解決を伴う関数の呼び出しを意図的に回避している可能性(トレース回避目的の可能性)
マルウェア作成者の知識・技能
- rep stosd、rep stosbを使った処理の作り方や利点を理解している
- 無駄なくコンパクトに実装できるスキルがある
- 第1引数が不正(NULL)だった場合のメモリアクセス違反までは意識していない
マルウェア作成者のコーデイングの癖(マルウェア作成者の筆跡)
- 引数のチェックは第3引数のみ
- 第2引数の1バイトのデータを4バイトに拡張するために掛け算を利用
- 返り値はmemsetと同様第1引数のポインタ