Ayumu's I/O

ねだるな勝ち取れ、さすれば与えられん

API Monitorのフックについて調べてみる

あゆむです。今回はAPI Monitorというツールのフックの動作を簡単に調べてみました。

API Monitorとは

プロセス(32bit/64bit)によるAPIコールを監視・制御できるツールです。監視対象とするAPIは、一覧から選択することで簡単に追加することができます。
配布元のサイトをみると、Version 2.0(Alpha-r13)が最新版で、2013/03/14以降はバージョンアップされていないようです。とはいえ、Version 1.5と2.0(Alpha)まで9年の開きがあることから、今後のサポートもありうると考えられます。
個人的な使用用途はマルウェア解析中のAPIコール状況の監視ですが、動的解析中にクラッシュしてしまうことが何度かあったため、解析の手始めにちょっとだけ使ってみるくらいの利用頻度です。

www.rohitab.com

基本的な使い方

簡単なプログラムを例にAPI Monitorの使い方を説明します。

環境

本記事における動作環境は次の通りです。

OS Windows 7 64bit
コンパイラ gcc version 6.3.0 (MinGW.org GCC-6.3.0-1)
その他 API Monitor v2 Alpha-r13

プログラムの準備

MessageBoxA関数でダイアログを表示するだけのプログラムを準備します。すぐにダイアログが表示されてしまうと扱いづらいので、getchar関数でユーザ入力されるまで待機するようにします。

#include <stdio.h>
#include <windows.h>

int main(void) {
	printf("Press \"Enter\" key to continue: ");
	getchar();
	MessageBoxA(NULL, "Hello world", "Message", MB_OK);
	return 0;
}

監視の手順

1. プログラムを実行します。
f:id:aes256:20180125145425p:plain
2. API Monitorの左上のパネル「API Filter」から監視対象とするAPIを選択します。ここでは、User32.dll!MessageBoxAを監視項目にいれます。
f:id:aes256:20180125145340p:plain
3. API Monitorの左下のパネル「Running Processes」で当該プロセスを選択し、モニタリングを開始します。
f:id:aes256:20180125145541p:plain
4. Enterキーを押下し、ダイアログを表示させます。
f:id:aes256:20180125145648p:plain
5. API Monitorを確認します。右上のパネル「Summary」にて引数の情報がモニタリングできていることが確認できます。
f:id:aes256:20180125145706p:plain

API Monitorのフックを調べてみる

プログラムごとに3パートに分けてAPI Monitorの動作を説明します。

Part1

まず、先ほどの「Hello world」を出力するプログラムを用いて動作を調べてみます。API Monitorによる監視前後におけるプロセスのメモリマップの変化を確認します。監視を開始すると「apimonitor-drv-x86.sys」をはじめ、いくつかのコードが注入されていることがわかります。
f:id:aes256:20180125154744p:plain
特筆すべきは、下図のコードです。命令列らしきコードですね。
f:id:aes256:20180125154645p:plain
ここで、デバッガを使って当該プロセスにアタッチし、このコードを逆アセンブルしてみます(下図)。GetProcAddress関数やMessageBoxA関数などのアドレスを引数として「apimonitor-drv-x86.sys」内をコールしていることがわかります。
これがAPI Monitorによる関数の検知の入り口です。実際にMessageBoxA関数に関してステップインで追っていくと、User32.dllがロードされているメモリではなく下図の箇所が最初に呼び出されます。
f:id:aes256:20180125155212p:plain

さて、当該プログラムのメモリロード後のIAT(Import Address Table)を見てみます。下図のメモリダンプ(Dump1)では、MessageBoxA関数のアドレスが「1C 00 DD 02」となっており、先のコード列を指していることがわかります。このようなAPIのフックは、IATフックと呼ばれます。
f:id:aes256:20180125161117p:plain

Part2

Part1で使用したプログラムでは、User32.dllを静的リンクで呼び出していましたが、Part2では動的リンクで呼び出すように書き換えてみます(次のコード参照)。これによって、MessageBoxA関数のアドレスがIATに含まれなくなります。つまり、Part2では、IATに監視対象のAPIがない場合にAPI Monitorで監視対象のAPIコールを検出できるかを確認するということです。マルウェアによっては元のIATが破壊されていることがあるため、これができるか否かで使い物になるかどうかが変わってきます。

#include <stdio.h>
#include <windows.h>

int main(void) {
	printf("Press \"Enter\" key to continue: ");
	getchar();

	FARPROC mba = GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
	mba(NULL, "Hello world", "Message", MB_OK);
	return 0;
}

図は省略しますが、動的リンクの場合でも監視対象を検出することができます。調べたところ、GetProcAddress関数が呼び出された場合にMessageBoxA関数検知のためのルーチン(Part1において”API Monitorによる関数の検知の入り口”と呼んだ箇所)を追加して、そのアドレスを返すようにしているようです。

Part3

最後はGetProcAddress関数を使用せずに、User32.dllのEAT(Export Address Table)からMessageBoxA関数のアドレスを直接取得するプログラムで試してみます。コードは次の通りです。

#include <stdio.h>
#include <windows.h>
#include <string.h>

int main(void) {
	printf("Press \"Enter\" key to continue: ");
	getchar();

	HMODULE hModule = LoadLibraryA("user32.dll");
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER) hModule;
	PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS) ((LPBYTE)hModule + pDosHeader->e_lfanew);
	PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
	PIMAGE_DATA_DIRECTORY pDirectory   = &pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
	PIMAGE_EXPORT_DIRECTORY pImgExport = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hModule + pDirectory->VirtualAddress);

	DWORD* functions = (DWORD*)((void*)hModule + pImgExport->AddressOfFunctions);
	DWORD* names     = (DWORD*)((void*)hModule + pImgExport->AddressOfNames);
	WORD*  ordinals  = (WORD*) ((void*)hModule + pImgExport->AddressOfNameOrdinals);

	for (int i = 0; i < pImgExport->NumberOfNames; ++i) {
		int ord = ordinals[i];
		//printf("%08X (%08x), %s\n", functions[ord], (void*)hModule+functions[ord], (void*)hModule+names[i]);

		if (strcmp((char*)hModule+names[i], "MessageBoxA") == 0) {
			FARPROC mba = ((void*)hModule + functions[ord]);
			mba(NULL, "Hello world", "Message", MB_OK);
			break;
		}
	}
	return 0;
}

結果は下図の通りです。APIMonitorではMessageBoxA関数を検出することはできませんでした。
f:id:aes256:20180126194616p:plain

Trickbot(Ver1000102)を解析してみる

マルウェア解析が趣味のあゆむです。今回はWindows7(64bit)環境下でTrickbotを解析したので、その結果をお伝えします。検体/ログ解析時の種類特定にお役立てください。モジュールの解析までは行っていませんのであしからず。

Trickbotとは

オンラインバンキングマルウェアです。Trickbotの主なターゲットは海外で日本にはあまりばら撒かれていない印象を受けます。
Trickbotは、ウェブインジェクション機能やURLリダイレクション機能などを持っています。たとえば、感染した状態でユーザが銀行のサイトなどにアクセスしようとすると、サイトに不正なコードが挿入されて入力した情報が盗まれるという被害が起こりえます。
主な感染経路はメールによるものです。メールに添付されたファイルを開いて感染するケースや脆弱な環境下で悪意のあるサイトに誘導される形で感染するケースがほとんどのようです。
Trickbotの更新はとても盛んで、バージョンアップして動作が変わったり、新たな機能を有するモジュールをダウンロードするようになったりと日々進化しています。執筆現在(12/23)の最新バージョンは1000108です。

検体情報

解析した検体は、2017年12月13日にばら撒かれたTrickbotです。メールに添付されたDocファイルを開くとTrickbotがダウンロードされて感染するという流れです。

Docfile

MD5 5a69998c829e08486b27ad16182f451e
SHA256 ee468a25416ad9f812198dbfb0f389fcdaf6326d1267625e921e7048bc37a9ee

TrickBot(Packed)

FileName nyRhdkwSD.exe / oySielwTD.exe
MD5 8b607501725d998c14f6a34eb4e8dc3e
SHA256 d27ea2a862848c82b7726584c6e66e41cb4988e3e92a42391d85d24fbe4e3d9c
Version(ver) 1000102
GroupTag(gtag) mac1

解析結果

概要

Trickbotは、パックされており本体が動作するまでに4つのステップがあります。下図をご覧ください。最初に実行されるファイルは「nyRhdkwSD.exe」です。これを実行すると、Process Hollowingという手法を用いて、新たに作成した自身のプロセスにコードを注入します(下図1-1)。次に実行元のファイルを別の場所へコピーします(下図2-1)。また、自動実行の設定をタスクスケジューラに登録します(下図2-2)。そして、再度Process Hollowing(下図3-1)とタスクの登録試行(下図4-2)を行い、正規のプログラムsvchost.exeへTrickbotを注入します(下図4-1)。以降では、これらをステップごとに解説します。
f:id:aes256:20171223190750p:plain

内容

1st Step

まず、デバッガで開くと、CreateWindowExA関数がたくさん呼び出されていることがわかります。これらは、解析妨害のために挿入されているようで、ブレイクポイントを設定しても一度も呼び出されません。引数もすべて0で呼び出す気がないです。
f:id:aes256:20171222185237p:plain:w300
f:id:aes256:20171222185251p:plain

実行すると、Process Hollowingを行います。自身をサスペンド状態でプロセス生成し、中身をアンマップ(空洞化)して、新たなコードを注入します。そして、エントリーポイントを書き換えて、スレッドを動作させます。このテクニックはよくある手法でDreamBotでも使われています。
f:id:aes256:20171223221920p:plain
f:id:aes256:20171222190133p:plain

なお、Process Hollowing実行時に呼ばれる関数のアドレスはスタック上に展開されます。
f:id:aes256:20171222190156p:plain

2nd Step

スレッドの動作が始まると注入されたコードが実行され、主にファイルのコピーとタスクスケジューラへの登録を行います。

まず、「%APPDATA%/AppData/Roaming/」に「services」というフォルダを作成します。CopyFileW関数を用いて、実行元のファイル(8b607501725d998c14f6a34eb4e8dc3e)をコピーします。当該解析環境では「oySielwTD.exe」という名前にリネームされて配置されました。
f:id:aes256:20171223162828p:plain

さらに、タスクスケジューラへ次の内容で自動実行の登録を行います。ログオン時または登録時点から3分ごとにプログラムが実行されます。
f:id:aes256:20171222190824p:plain

名前 services update
説明 Look for services monitor.
トリガー 複数のトリガーの定義(任意のユーザのログオン時。毎日hh:mmに起動、トリガーされた後、1日間の間、00:03:00ごとに繰り返す。)
操作 プログラムの開始 C:\Users\■■■\AppData\Roaming\services\oySielwTD.exe
3rd Step

タスクスケジューラによってプログラムが実行されます。このときの動作は1st Stepと同一のもので、再度Process Hollowingが行われます。
f:id:aes256:20171223222441p:plain

4th Step

スレッドの動作が始まると注入されたコードが実行され、2nd Stepと途中まで同一の動作をします。この段階では、主にタスクスケジューラへの登録試行とTrickbotの展開、svchost.exeへの注入が行われます。

詳細を解析していませんが、svchost.exeへの注入もProcess Hollowingによって行われていると思われます。CreateProcessW関数を用いてsvchost.exe(64bit)のプロセスをサスペンド状態で作成します。
f:id:aes256:20171223222504p:plain

なお、一連の動作終了後の「oySielwTD.exe」のヒープ領域には、Trickbot(64bit)が展開されています。マジックナンバー0x4D 0x5A(MZ)が確認できます。
f:id:aes256:20171222191340p:plain:w300

Trickbot

Trickbotが注入されたsvchost.exeが動作すると、まず、「%APPDATA%/AppData/Roaming/」に新たにファイルとフォルダを作成します。この動作は繰り返されるようで、削除しても当該svchost.exeが動作している間は再度生成されます。
f:id:aes256:20171222192407p:plain
Modulesフォルダには、モジュールをダウンロードして配置します。client_id、group_tagの中身は下図をご覧ください。黒塗りにしていますが、client_idは感染端末のユーザ名から始まり、Windowsバージョンと乱数で構成されています。
f:id:aes256:20171222193107p:plain

外部のサービスと通信して感染端末のグローバルIPアドレスを確認します。User-AgentがChromeとなっていますが、確認したところ感染端末にインストールされているChromeのバージョンと異なっていました。
f:id:aes256:20171223095034p:plain
f:id:aes256:20171223165157p:plain

C2サーバとのC2通信が発生します。
f:id:aes256:20171222192651p:plain
残念ながら解析時点では接続できませんでした。Trickbotは基本的にHTTPS通信を行うようです。
f:id:aes256:20171223145144p:plain

なお、おとりC2サーバを用意して通信させてみた結果、次のリクエストを投げていることがわかりました。「/5/spk/」の5はコマンドIDで、C2サーバから何かをダウンロードする際に使われるとのことです。*1
f:id:aes256:20171223163622p:plain

さて、C2サーバのIPアドレスを含むconfigは、Trickbotが注入されたsvchost.exeのメモリのヒープ領域に保持されています。
f:id:aes256:20171222193418p:plain:w300

取り出したデータは次の通りです。ここにTrickbotのバージョンやC2通信先、取得するモジュールのリストが記載されています。

<mcconf>
<ver>1000102</ver>
<gtag>mac1</gtag>
<servs>
<srv>79.106.41.9:449</srv>
<srv>185.21.149.41:449</srv>
<srv>200.111.97.235:449</srv>
<srv>67.209.219.92:449</srv>
<srv>209.205.188.238:449</srv>
<srv>73.252.252.62:449</srv>
<srv>76.16.105.16:449</srv>
<srv>82.202.236.84:443</srv>
<srv>78.155.199.124:443</srv>
<srv>179.43.160.45:443</srv>
<srv>94.250.253.142:443</srv>
<srv>5.200.55.47:443</srv>
<srv>37.60.177.19:443</srv>
<srv>94.250.255.50:443</srv>
<srv>82.146.48.44:443</srv>
<srv>194.87.93.30:443</srv>
<srv>194.87.94.225:443</srv>
<srv>195.62.53.88:443</srv>
<srv>82.146.48.241:443</srv>
<srv>195.88.209.128:443</srv>
<srv>80.87.198.204:443</srv>
<srv>194.87.146.14:443</srv>
<srv>195.133.147.140:443</srv>
<srv>92.53.66.60:443</srv>
<srv>194.87.93.84:443</srv>
<srv>82.202.226.189:443</srv>
<srv>95.154.199.136:443</srv>
</servs>
<autorun>
<module name="systeminfo" ctl="GetSystemInfo"/>
<module name="injectDll"/>
</autorun>
</mcconf>

解析結果は以上です。
今回の解析ではC2サーバと通信ができなかったため、これ以降の分析はしていません。通常は、この後に各種情報を窃取する機能を持ったモジュールがダウンロードするようです。そのうちチャレンジします。

名前付きパイプによるプロセス間通信をやってみる

Windowsにおいてプロセス間通信(IPC)に使われる名前付きパイプについて調べました。

名前付きパイプ(Named Pipe)とは

名前付きパイプとは、プロセス間のデータ転送のためのプログラミングAPI。メールスロットと異なり、信頼される双方向通信を実現できる。ファイルのようにアクセスでき、Windows I/Oの標準関数であるCreateFile関数、ReadFile関数、WriteFile関数、CloseHandle関数を使って扱うことができる。生成時に作成するインスタンスの個数を指定できるため、1つのサーバが複数のクライアントと通信することもできる。また、サーバが自身のアカウント権限ではなくクライアント側のアカウント権限でスレッドの実行する、偽装(Impersonation)と呼ばれる機能も利用できる。名前付きパイプの関数は、kernel32.dllに実装されている。

f:id:aes256:20171204190757p:plain
図1.名前付きパイプ(PIPE_ACCESS_INBOUND)による通信のイメージ

CreateNamedPipe関数

名前付きパイプは、CreateNamedPipe関数を使ってインスタンスを作成できる。この関数の第一引数lpNameは、名前付きパイプ名へのポインタで、次の形式で指定する。

\\<サーバ>\Pipe\<パイプ名>
  • <サーバ>の部分には、名前付きパイプサーバを実行するコンピュータを指定する。Windows定義のエイリアス「\\.\」を指定した場合は、そのシステムのみで有効となる。 本記事では未検証だが、名前付きパイプはネットワークにまたがるプロセス間通信にも利用可能で、<サーバ>の部分にDNS名、IPアドレス、NetBIOS名なども指定できる。
  • <パイプ名>の部分には一意な名前を指定する。サブディレクトリを含むことができる。
例:\\.\Pipe\MyServerApp\ConnectionPipe

第二引数dwOpenModeでパイプの通信方向を指定できる。

説明
PIPE_ACCESS_DUPLEX 双方向。サーバとクライアントの両方が名前付きパイプの読み書きを行える。
PIPE_ACCESS_INBOUND 一方向。データの流れをクライアントからサーバへの方向に限定する。
PIPE_ACCESS_OUTBOUND 一方向。データの流れをサーバからクライアントへの方向に限定する。

パイプに関連する他の関数

関数名 説明
ConnectNamedPipe クライアントが名前付きパイプに接続してくるのを待機する。
CallNamedPipe 名前付きパイプがメッセージタイプの場合は当該関数で接続する。
PeekNamedPipe 名前付きパイプの状態を調べる。
WaitNamedPipe 接続可能になるかタイムアウト時間が経過まで待機する。

詳細は、下記Webページにまとめられている。
https://msdn.microsoft.com/ja-jp/library/cc429276.aspx

名前付きパイプのアクティビティの確認方法

Windows Sysinternals の PipeListツールを使う。
docs.microsoft.com

名前付きパイプの名前、それに対して作成されているインスタンス数、最大インスタンス数を表示できる。

C:\Users\Ayum>C:\Users\Ayum\Downloads\PipeList\pipelist.exe

PipeList v1.02 - Lists open named pipes
Copyright (C) 2005-2016 Mark Russinovich
Sysinternals - www.sysinternals.com

Pipe Name                                    Instances       Max Instances
---------                                    ---------       -------------
InitShutdown                                      3               -1
lsass                                             4               -1
protected_storage                                 3               -1
ntsvcs                                            3               -1
scerpc                                            3               -1
plugplay                                          3               -1
Winsock2\CatalogChangeListener-484-0              1                1
epmapper                                          3               -1
(以下略)

プログラム

クライアント側で文字列を入力すると名前付きパイプを利用してプロセス間通信(一方向)を行い、サーバ側で文字列が表示されるというプログラム。実行する際は、名前付きパイプを作成してから読み込む必要があるのでサーバ側から先に実行する。終了する場合は、”EXIT”という文字列を入力する。

サーバ側のソースコード

サーバ側では、CreateNamedPipe関数を呼び出して名前付きパイプのインスタンスを作成し、ConnectNamedPipe関数でクライアントが接続するまで待機する。その後、名前付きパイプにクライアントからのメッセージがある場合は表示する。

#include <stdio.h>
#include <windows.h>

int main() {
    HANDLE hPipe = INVALID_HANDLE_VALUE;
    hPipe = CreateNamedPipe("\\\\.\\pipe\\mypipe", //lpName
                           PIPE_ACCESS_INBOUND,            // dwOpenMode
                           PIPE_TYPE_BYTE | PIPE_WAIT,     // dwPipeMode
                           1,                              // nMaxInstances
                           0,                              // nOutBufferSize
                           0,                              // nInBufferSize
                           100,                            // nDefaultTimeOut
                           NULL);                          // lpSecurityAttributes

    if (hPipe == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "Couldn't create NamedPipe.");
        return 1;
    }
    if (!ConnectNamedPipe(hPipe, NULL)) {
        fprintf(stderr, "Couldn't connect to NamedPipe.");
        CloseHandle(hPipe);
        return 1;
    }

    while (1) {
        char szBuff[256];
        DWORD dwBytesRead;
        if (!ReadFile(hPipe, szBuff, sizeof(szBuff), &dwBytesRead, NULL)) {
                fprintf(stderr, "Couldn't read NamedPipe.");
                break;
        }
        if (!strncmp("EXIT", szBuff, 4)) { // Exit Check
                break;
        }
        szBuff[dwBytesRead] = '\0';
        printf("Server : %s", szBuff);
        Sleep(1);
    }
    FlushFileBuffers(hPipe);
    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe);
    return 0;
}

クライアント側のソースコード

クライアント側では、サーバ側で作成した名前付きパイプのハンドルを取得し、ユーザによって入力された文字列をWriteFile関数で書き込む。

#include <stdio.h>
#include <windows.h>

int main() {
	HANDLE hPipe = INVALID_HANDLE_VALUE;
	hPipe = CreateFile("\\\\.\\pipe\\mypipe",
                          GENERIC_WRITE,
                          0,
                          NULL,
                          OPEN_EXISTING, 
                          FILE_ATTRIBUTE_NORMAL,
                          NULL);
	
	if (hPipe == INVALID_HANDLE_VALUE) {
		fprintf(stderr, "Couldn't create NamedPipe.");
		return 1;
	}

	while (1) {
		char szBuff[255];
		DWORD dwBytesWritten;
		fgets(szBuff, sizeof(szBuff), stdin);
		if (!WriteFile(hPipe, szBuff, strlen(szBuff), &dwBytesWritten, NULL)) {
			fprintf(stderr, "Couldn't write NamedPipe.");
			break;
		}
		if (!strncmp("EXIT", szBuff, 4)) { // Exit Check
			break;
		}
	}
	CloseHandle(hPipe);
	return 0;
}

実行結果

クライアントに文字列を入力してEnterを押下すると、サーバにも同一の文字列が表示される。

f:id:aes256:20171204175406p:plain
図2.プログラムの実行イメージ
このときの名前付きパイプのアクティビティは次の通りとなった。「mypipe」が表示されていることがわかる。

C:\Users\Ayum>C:\Users\Ayum\Downloads\PipeList\pipelist.exe

PipeList v1.02 - Lists open named pipes
Copyright (C) 2005-2016 Mark Russinovich
Sysinternals - www.sysinternals.com

Pipe Name                                    Instances       Max Instances
---------                                    ---------       -------------
InitShutdown                                      3               -1
lsass                                             4               -1
protected_storage                                 3               -1
ntsvcs                                            3               -1
scerpc                                            3               -1
plugplay                                          3               -1
(中略)
mypipe                                            1                1

Process Hackerなどのツールを使ってクライアントのハンドル一覧を見れば、パイプのハンドルを確認できる。

f:id:aes256:20171205174120p:plain
図3.クライアントのハンドル一覧


上記コードを32bit/64bitプログラムにそれぞれコンパイルし、各プロセス間で名前付きパイプによる通信が可能か検証した。いずれも成功した。

サーバ    クライアント プロセス間通信
32bit 32bit 成功
32bit 64bit 成功
64bit 32bit 成功
64bit 64bit 成功