Fridaを用いてマルウェアの動作を解析・変更する

この記事は NTTコミュニケーションズ Advent Calendar 2021 の19日目の記事です。

はじめに

こんにちは。イノベーションセンターテクノロジー部門の田中と申します。インターネットにおける攻撃インフラ撲滅に向けた追跡活動を主に行っています。例えば、追跡中のIPアドレスは真に該当マルウェアのC2であるか、現在も活動中であるか等をOSINTを活用して精度を上げて特定していくのですが、さらに情報が必要になるケースがあります。その際に、有力な技術の1つになるのが、マルウェアやC2に与える情報を制御し、挙動の差異を観測するという手法です。本記事では、Fridaというツールを利用して、解析・変更の初歩の部分について行ってみたいと思います。

概要

前半は、準備としてAPI Monitorというツールを用い、APIコールをトレースし、マルウェアの挙動を簡単に把握します。後半は、動的バイナリ計装(DBI:Dynamic Binary Instrumentation)フレームワークの1つであるFridaを使ってマルウェアの一部の動作をフックし、その動作を変更してみます。DBIは、実行中のプログラムにコードを挿入可能な技術で、関数をフックして入出力の読み取り・書き換えすることが可能となります。API Monitorはマルウェア解析トレーニングコースであるSANS FOR610でも扱われますが概要にとどまるのと、Fridaは、マルウェア解析適用事例が少ないため、今回紹介することにしました。

解析環境の準備

マルウェアを動作させて解析するため、専用の仮想環境を準備します。VirtualBoxやVMware等上にWindows10をセットアップし、解析ツールを準備します。今回は、デバッガx64dbgAPI Monitor、ネットワークツールFakenet-ngを用います。各サイトからダウンロードしてインストールしてください。解析ツールを一括導入してくれるFlareVMを用いるのもお勧めです。仮想環境を構築したらスナップショットを取得し、マルウェア解析後は、元の状態へ戻すようにしてください。(尚、Fridaについては後半で導入方法から紹介します。)

解析検体

今回は、Azorultというマルウェアを対象にします。Anyrunから入手したサンプルを使用します(Azorultサンプル解析ページ)。Azorultサンプルの解析ページを確認して頂くとわかるように、このAzorultサンプルは、コロナ感染状況パネルを模擬したドロッパーから、プロセスID:2636でドロップされたものです。(ドロッパーはAzorultではないのでご注意ください)

API Monitorによる簡易解析

準備した解析環境で、Azorultサンプルを解析していきます。まず、Fakenet-ngを起動します。Fakenet-ngはダミーのDNSサーバやWebサーバ相当の動作をローカル環境で担ってくれ、マルウェアのC2等の通信を仮想環境内に閉じ込めてくれます。

次に、API Monitorを用いてサンプルを起動してAPIコールをトレースします。API Monitorは2013年に開発が止まっていますが、有益なツールなので使い方の一部を解説します。API Monitor(x86)を起動後、File->Monitor New Processで、Azorultのサンプルを選択します。(選択画面で、exeファイルのみしか選択できない仕様のため、Azorultサンプルの拡張子が異なる場合は、exeに変更してください。)サンプルを選択しOK押下すると、Azorultが起動され、以下のように呼び出されたAPIが記録されます。

f:id:NTTCom:20211216113520p:plain

起動後、数秒後にFile->Pause Monitoringでトレースを停止します。その後以下の図のように、API Filter画面のDisplayをクリック、Add Filterをクリックすると、Display Filterダイアログがでるので、「Calling Module Name」を選択肢、Azorultサンプルのファイル名(ここではazorult.exe)を入力しOK押下。

f:id:NTTCom:20211216113527p:plain

すると、azorult.exeから呼び出されたAPIコールのみにフィルタできます。サンプル起動直後のAPIコールは下図のように、LoadLibraryAGetProcAddressによりマルウェアが呼び出したいAPIのアドレス解決をしていることがわかります。これは、Windows仕様における明示的リンク(Explicit Linking)の挙動です。ファイルヘッダのIAT(Import Address Table)に記載され、表層解析で容易にAPIを発見な暗黙的リンク(Implicit Linking)とは対象的です。尚、IATは、Windows実行ファイルであるPE形式で定義されるヘッダ情報です。

f:id:NTTCom:20211216113535p:plain

もう少し下にスクロールしてみると、下図のように、なにやらWindowsバージョン情報と、CreateMutexAでMutexが作られていることがわかります。Mutexは2重起動等を防ぐため等に正規アプリケーションで用いられますが、マルウェアでも散見される挙動です。

f:id:NTTCom:20211216113542p:plain

さらにスクロールしてみると、下図のように、wininet.dllがロードされ、インターネット接続に関するAPI群をアドレス解決している様子を見ることができます。

f:id:NTTCom:20211216113548p:plain

Fakenet-ngのログを見ると、ドメインcoronavirusstatus[.]spaceに対し101バイトの長さの通信を行っていることがわかります。(実際にはFakenet-ngが該当ドメインの権威サーバに代わり、DNS/HTTP疑似応答していることがログでわかります。尚、執筆時点で、該当ドメインのAレコード応答はありませんでした。)

f:id:NTTCom:20211216134250p:plain

Step.1 FridaによるCreateMutexAの追跡

前節のAPI Monitorによる解析で、Azorultサンプルは、動的に用いるAPI関数のアドレス解決していました。さらに、ハイフンにより繋がれた文字列でMutexを作り、あるドメインに接続し101バイトの通信することがわかりました。Fridaを使いCreateMutexAにフックをかけてみます。FlareVMではfridaは導入されないので以下の様にインストールします。

pip install frida-tools

ここでは以下のコードを準備しました。Fridaでは、解析プロセスの立ち上げや解析スクリプトの適用はpythonにより行いますが、フック等を明示する解析スクリプト自体はJavaScriptにより記述します。ModuleのgetExportByNameにより、DLL名とAPI名を指定しアドレスを得た(1)あとで、Interceptorのattachを用い、指定アドレスをフックし、関数が呼ばれる前の処理(onEnter)、呼ばれたあとの処理(onLeave)を記載できます。このように、フックしたあとで、ユーザが任意のコードを実行させることがFridaの特徴になります。ここでは、CreateMutexAをフックし(2)、引数の2番目のlpNameを表示(3)するようにします。(リンクからCreateMutexAのプロトタイプを確認できます)

import frida
import sys

file = 'C:\\work\\frida\\azorult.exe'

pid = frida.spawn(file)
session = frida.attach(pid)

script = session.create_script("""
console.log("Starting ....");
// CreateMutexAのアドレスを取得 (1)        
var CreateMutexA = Module.getExportByName("kernel32.DLL", "CreateMutexA");
// 該当アドレスでフック (2)
Interceptor.attach(CreateMutexA, {
    // 関数が呼ばれる際に引数を取得し表示 (3)
    onEnter: function (args) {
        console.log("Entering .... CreateMutexA");
        console.log("[*] CreateMutexA args: " + args[2].readAnsiString());
    },
         });
         
         """)

script.load()
frida.resume(pid)
sys.stdin.read()

動作させると以下のように、API Monitorで見たMutexが作られていることがわかります。 f:id:NTTCom:20211216113601p:plain

Step2. FridaによるMutex文字列生成ロジック追跡

では、このハイフン区切りのMutex文字列はどのように生成されるのでしょうか?Azorultを解析したblackberry社のブログ記事によると、下図のようなロジックで生成されるとあります。感染Windows端末におけるGUID、プロダクト名、ユーザ名、コンピュータ名を入力として、ある関数(図ではID generate func())で4Byteの固定長に変換され、それがハイフンで繋がれMutex文字列になっていることがわかります。

f:id:NTTCom:20211216113608p:plain

引用元:https://blogs.blackberry.com/en/2019/06/threat-spotlight-analyzing-azorult-infostealer-malware

この関数(以下、符号化関数と呼びます)をデバッガを用い確認してみましょう。Azorultサンプルは32bitバイナリですので、x32dbgを起動し、Azorultサンプルを開き、0x00406204にブレークポイントを設定します。(x32dbg左下のコマンド窓から、bp 406204と入力しエンター押下。)ブレークポイント設定後、実行(F9キー押下)を数回起動すると、該当アドレスで停止します。(数回が何回なのかは、x32dbgの「設定->Events」の設定によります。また、ブレークポイントを過ぎてしまった場合、Ctrl+F2キー押下で開始時点からやり直せます。)0x00406204のブレークポイントに到達したら、gキーを押下します。すると以下のグラフ画面が表示されます。

f:id:NTTCom:20211216113617p:plain

0x00406204が符号化関数の先頭になります。この符号化関数は何度か呼ばれており、画面は、1回目の呼び出し時になります。1回目の符号化対象はGUIDで、関数内のローカル変数を意味する[ebp-4]にこのGUID文字列のポインタが格納されているのがわかります。真ん中のブロックで、xor演算やシフト演算(shl,shr)が含まれ、ループにより繰り返し回実行されることがわかります。繰り返しは対象文字列の先頭から末尾まで続きます。特に興味深いのが、xorで固定の値(ここでは0x6521458a)が使われていることです。このサンプルはこの値をキーとして、符号化処理をしていることがわかります。こういった文字列はIOCとして活用できる可能性があります。

符号化関数のアドレスが確認できたので、Fridaで関数をフックしてみます。先程のコードに以下を追加します。先程のようにAPIではなく任意のアドレスでフックさせたい場合は、NativePointerを用いポインタを作っておきます(4),(6)。符号化関数の入力値については、eaxレジスタに文字列へのポインタが入っているので、this.context.[レジスタ名]で読み取り、Memory.readAnsiStringでポインタの示す文字列を取得します(5)。符号化関数の出力値はebxレジスタに格納されるので、同様にthis.context.ebxで値そのものを取得します(7)。

// 復号化関数開始アドレスのフック用ポインタ作成 (4)
var enc_func_start = new NativePointer("0x00406204")
Interceptor.attach(enc_func_start, {
    onEnter: function (args) {
        console.log("entering .... Encode_function");
        // eaxレジスタの値の示す文字列取得 (5)
        console.log("[*] Input_value : " + Memory.readAnsiString(this.context.eax));
    },
         });
// 復号化関数終了アドレスのフック用ポインタ作成 (6)
var enc_func_end = new NativePointer("0x00406265")
Interceptor.attach(enc_func_end, {
    onEnter: function (args) {
        //console.log("exiting .... Encode_function");
        // ebxレジスタの値取得 (7)
        console.log("[*] Output_value : " + this.context.ebx);
    },
         });

上記コードの実行結果は以下のようになります。GUID、プロダクト名、ユーザ名、コンピュータ名、及び前述4つをつなげた文字列、の計5個が順番に、Input_valueとして符号化がされて、4Byteの文字列がOutput_valueとして生成されているのがわかります。最終的にハイフンを挿入されてMutexが作られています。

f:id:NTTCom:20211216113624p:plain

Step.3 FridaによるGetComputerNameW結果の書き換え

最後に、Fridaを用い、マルウェアに誤った情報を伝える一例として、GetComputerNameWの結果を書き換えてみます。前節までの分析でみたように、本Azorultサンプルは、コンピュータ名等を用いMutexを作り、それをHTTP通信によりビーコンとしてC2に送ります。一般に、C2側ではこの情報をデコードして感染端末情報を入手することで、ターゲットか否かを判断し、情報搾取等次の行動に移ります。

ここでは以下のコードを準備しました。Step.1と同様に、ModuleのgetExportByNameで、DLL名とAPI名を指定しアドレスを得た(8)あと、Interceptorのattachで、指定アドレスをフックします。関数の呼ばれたあとの処理(onLeave)を記載していきます。まず、書き換え対象の文字列をMemory.allocでUTF16としてメモリを確保し変数stに割り当てます(10)。次に、GetComputerNameWにより得られたコンピュータ名の格納アドレスを確認していくのですが、ここで問題発生しました。GetComputerNameWのLeave時に、コンピュータ名を格納したメモリアドレスは、[ESP-8]と想定したのですが、そこにはありませんでした。そこで、Memory.readByteArrayを用いESPから100Byteダンプします(11)。すると[ESP+8]にコンピュータ名の存在が確認できるので、このアドレスを書き換え対象としてフック用のポインタを作成します(12)。Memory.copyで、先程用意した、変数stに書き換えます(13)。書き換わった後のメモリを表示すると確かに「NTTcom8213」にコンピュータ名が変わっていることを確認できます。

import frida
import sys

file = 'C:\\work\\frida\\azorult.exe'

pid = frida.spawn(file)
session = frida.attach(pid)

script = session.create_script("""
console.log("Starting ....");
// GetComputerNameWのアドレスを得る (8)        
var gcExportAddress = Module.getExportByName("kernel32.DLL", "GetComputerNameW");
// フック設定 (9)
Interceptor.attach(gcExportAddress, {
    onEnter: function (args) {
        console.log("Entering .... GetComputerNameW");
    },
    onLeave: function (retval) {
        console.log("Leaving .... GetComputerNameW");
        // 書き換え文字列のメモリ確保 (10)
        var st=Memory.allocUtf16String("NTTcom8213");
        // espを起点にメモリダンプ (11)
        var esp = this.context.esp;
        var pointer = new NativePointer(esp);        
        var mem = Memory.readByteArray(pointer, 100);
        console.log("Stack memory dump ... ESP: " + esp);
        console.log(mem);
        
        console.log("[*] Return Value exists ESP + 8");                
        // esp+8のアドレスを取得しポインタ作成 (12)
        esp = parseInt(esp) + 8;
        var pointer = new NativePointer(esp);

        console.log("[*] Original Return Value: " + Memory.readUtf16String(pointer));
        // 書き換え実施 (13)
        Memory.copy(pointer, st, 22);
        
        console.log("[*] Altered Return Value: " + Memory.readUtf16String(pointer));
            }
         });
var CreateMutexA = Module.getExportByName("kernel32.DLL", "CreateMutexA");
Interceptor.attach(CreateMutexA, {
    onEnter: function (args) {
        console.log("entering .... CreateMutexA");
        console.log("[*] CreateMutexA arg: " + args[2].readAnsiString());
    },
         });
         
         """)

script.load()
frida.resume(pid)
sys.stdin.read()

本スクリプトの動作結果は以下になります。GetComputerNameWの結果をフックにより書き換え、Mutex文字列における、コンピュータ名から生成される、後半2つのブロックの文字列の変更を確認できました。AzorultのC2側ではこの文字列を逆のロジックでデコードし、コンピュータ名を得るので、NTTcom8213という誤った情報を与えることができました。

f:id:NTTCom:20211216113631p:plain

補足

今回Fridaを検証してみて、気づいた点を書いておきます。

アドレスによりフックが失敗するケースあり

  • Step.2の検証では、WindowsAPIではなく、任意のアドレスをNativePointerにしてフックを仕掛けました。アドレスによってはクラッシュやエラーメッセージが出て、追跡対象のプログラムが正しく動作しなくなるケースがありました。
  • 異なる構文や工夫をしてフックを試みましたが、断念しました。(おそらくデバッガのブレークポイントと同じ様に、停止させたいメモリアドレスに対しInt3命令(0xCC)に置き換えることで、実現していると思ったので何かしらやりようがあると思いましたが)

Interceptor.attachのonLeave動作仕様の正しい理解

  • Step.3検証で、想定したメモリアドレスとずれていたことを述べましたが、OnLeaveの仕様を私がちゃんと理解できていないと思われます。私の想定では、onEnterで関数の頭、onLeaveで関数のRETもしくはRET後の想定だったのですが、それぞれEIPを表示させると、OnLeaveに関してもOnEnterと同一の関数冒頭のアドレスでした。(つまりRet時にthis.context.[レジスタ]で想定した値が取れない)
  • NativeFunctionを用い関数を定義することでうまく行かないかなと試しましたが、現状で解決策が見つかっていません。

サンプルAzorult検体は、API MonitorのFakenet-ngログで見たように、HTTP POST通信により、Mutexに登録したユーザ情報をエンコードしてC2サーバへの送信を試みることがわかりました。この際、先程の符号化関数の処理に加えて、さらに3Byteの別のxorキーで、あるロジックにより難読化を行い最終的なPOSTデータを生成します。また、感染後はビーコン以外のC2通信も観測できます。興味のある方は、該当キーやロジック等さらなる解明に挑戦してみてください。

今回用いたサンプルAzorultのsha256

  • fda64c0ac9be3d10c28035d12ac0f63d85bb0733e78fe634a51474c83d0a0df8

終わりに

いかがだったでしょうか。今回はFridaを使いマルウェアの動作を解析し、誤った情報を与えることを行いました。ちょっとした動作の変更であればデバッガ上で可能ですが、任意のコード実行を含む大きな変更を伴う場合などは、動的バイナリ計装の恩恵は大きくなります。実は当初、最近勢いがあるエミュレーターQilingで同様の解析を行うつもりで検証をしていたのですが、上記補足に書いた以上にインパクトのある問題が出て、今回の目標だと適さないことがわかったので急遽本検証に変更しました。マルウェア解析では常に2つ以上のツールを用意するのが王道と言われますが、その必要性を実感しました。Qilingについても機会がありましたら紹介したいと思います。

それでは、明日の記事もお楽しみに!

© NTT Communications Corporation All Rights Reserved.