それでは、実際にAPIの実行状況を大雑把にトレースしてみましょう。[00A06]の処理終了時にブレークポイントを設定し、GetProcAddressが求めているAPIをチェックしていきます。また、APIをコールするときのスタックの値を確認することで、パラメータも確認できます。確認結果を【表1】にまとめてみました。
(※冗長になるパラメータは一部省いてあります。)
【表1】メモリ上のプログラムのAPIの呼び出し状況
アドレス | Windows API | 処理内容・パラメータ等 |
[0025B] | OpenSMManagerA | サービスマネージャ情報を取得。 マシン名 = null (ローカルコンピュータ) データベース名 = null (SERVICE_ACTIVE_DATABASE) 希望するアクセス = 0x00000004 (SC_MANAGER_SERVICE) |
[0037A] | EnumServicesStatusExA | 指定されたサービスマネージャデータベースのサービス情報の列挙。 SCMデータベースのハンドル = 直前に取得したもの サービスのタイプ = 0x00000030 (SERVICE_WIN32) サービスの状態 = 0x00000003 (SERVICE_STATE_ALL) ステータスバッファ = 0x00000000 ステータスバッファのサイズ = 0x00000000 |
[00430] | GlobalAlloc | 指定されたバイト数をヒープから割り当て。 割り当ての属性 = 0x00000040 割り当てたいバイト数 = 0x00004E20 |
[00548] | EnumServicesStatusExA | 指定されたサービスマネージャデータベースのサービス情報の列挙。 SCMデータベースのハンドル = 直前に取得したもの サービスのタイプ = 0x00000030 (SERVICE_WIN32) サービスの状態 = 0x00000003 (SERVICE_STATE_ALL) ステータスバッファ = (バッファのポインタ) ステータスバッファのサイズ = 0x00004E20 |
[005F0] | GlobalFree | ヒープ領域を解放。 グローバルメモリオブジェクトのハンドル = (バッファのポインタ) |
[007E7] | EnumWindows | トップレベルウィンドウの列挙 コールバック関数 = [00789] アプリケーション定義の値 = 0x0012EC98 (スタック上のアドレス) |
[01732] | LoadLibraryA | 指定された実行可能モジュールをロード。 モジュールのファイル名 = ADVAPI32.DLL |
[01745] | RegOpneKeyExA | レジストリキーのオープン。 ハンドル = 0x80000002 (HKEY_LOCAL_MACHINE) サブキー = SOFTWARE\ESET セキュリティアクセスマスク = 0x00020019 (KEY_READ) |
[01513] | LoadLibraryA | 指定された実行可能モジュールをロード。 モジュールのファイル名 = ADVAPI32.DLL |
[0152C] | RegOpneKeyExA | レジストリキーのオープン。 ハンドル = 0x80000002 (HKEY_LOCAL_MACHINE) サブキー = SOFTWARE\AVAST Software セキュリティアクセスマスク = 0x00020019 (KEY_READ) |
[01F3B] | GetTickCount | システム起動後の時間をミリ秒で取得。 |
[01FE7] | Sleep | スリープ。 中断の時間 = 0x00003E80 (16000) ミリ秒 |
[02087] | GetTickCount | システム起動後の時間をミリ秒で取得。 |
[021D8] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00400000 (exe本体) 領域のサイズ = 0x0034DD68 (3,464,552) bytes 希望アクセス保護 = 0x00000040 (PAGE_EXECUTE_READWRITE) 従来のアクセス保護を取得するアドレス = 0x0012ECE8 (スタック上のアドレス) |
[024D8] | VirtualAlloc | メモリ領域の取得 予約またはコミットしたい領域 = null (システム依存) 領域のサイズ = 0x0002E400 (189,440) bytes 割り当てのタイプ = 0x00001000 (MEM_COMMIT) アクセス保護のタイプ = 0x00000004 (PAGE_READWRITE) |
[025BD] | RtlDecompressBuffer | 圧縮バッファの解凍 フォーマット = 0x00000002 (COMPRESSION_FORMAT_LZNT1) アンコンプレスバッファ = 直前のVirtualAllocで得られたアドレス アンコンプレスバッファサイズ = 0x0002E400 (189,440) bytes コンプレスバッファ = 0x007216C0 (exeの領域内) コンプレスバッファサイズ = 0x00024696 (149,142) bytes 最終アンコンプレスサイズ = 0x0012FD78 (スタック上のアドレス) |
[01042] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00400000 (exe本体) 領域のサイズ = 0x0034DD68 (3,464,552) bytes 希望アクセス保護 = 0x00000004 (PAGE_READWRITE) 従来のアクセス保護を取得するアドレス = 0x0012EC7C (スタック上のアドレス) |
[02775] | RtlZeroMemory | メモリの0埋め ディスティネーション = RtlDecompressBufferの展開先 領域のサイズ= 0x0002E400 (189,440) bytes |
[0282D] | VirtualFree | メモリの解放 解放される領域 = [024D8]で確保した領域 |
[01228] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00400000 (exe本体) 領域のサイズ = 0x00000400 (1,024) bytes 希望アクセス保護 = 0x00000002 (PAGE_READONLY) 従来のアクセス保護を取得するアドレス = 0x0012ECA0 (スタック上のアドレス) |
[0134C] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00401000 (exe本体) 領域のサイズ = 0x00012D21 (77,089) bytes 希望アクセス保護 = 0x00000020 (PAGE_EXECUTE_READ) 従来のアクセス保護を取得するアドレス = 0x0012EC9C (スタック上のアドレス) |
[0134C] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00414000 (exe本体) 領域のサイズ = 0x0000009A (154) bytes 希望アクセス保護 = 0x00000002 (PAGE_READONLY) 従来のアクセス保護を取得するアドレス = 0x0012EC9C (スタック上のアドレス) |
[0134C] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00415000 (exe本体) 領域のサイズ = 0x0001B3D8 (111,576) bytes 希望アクセス保護 = 0x00000004 (PAGE_READWRITE) 従来のアクセス保護を取得するアドレス = 0x0012EC9C (スタック上のアドレス) |
[0134C] | VirtualProtect | アクセス保護の変更 コミット済みページ領域アドレス = 0x00431000 (exe本体) 領域のサイズ = 0x00003DC (988) bytes 希望アクセス保護 = 0x00000002 (PAGE_READONLY) 従来のアクセス保護を取得するアドレス = 0x0012EC9C (スタック上のアドレス) |
以下、API呼び出し時の返り先のアドレスを次のWindows APIのアドレスにすることにより、連続的にAPIを呼び出している。また、スタックには、連続で処理を呼び出しても動くように引数が設定されている。 | ||
[02B52] | RtlZeroMemory | メモリの0埋め ディスティネーション = 仮想メモリで動作している自身の領域 (「仮想メモリ取得した方法の正体」で取得した領域) 領域のサイズ= 0x00002B59 (11,097) bytes |
| VirtualFree | メモリの解放 解放される領域 =仮想メモリで動作している自身の領域 (「仮想メモリ取得した方法の正体」で取得した領域) |
| CreateThread | 呼び出し側プロセスの仮想アドレス空間で実行するべき1個のスレッドを生成 セキュリティ記述子 = 0x00000000 (既定のセキュリティ識別子) 初期のスタックサイズ = 0x00000000 (既定サイズ) スレッドの機能 = 0x00413C10 (プログラム本体内のアドレス) スレッドの引数 = 0x00000000 (引数はNULL) 作成オプション = 0x00000000 (作成と同時に実行) スレッド識別子 = 0x00000000 (スレッド識別子は格納しない) |
| RtlExitUserThread | 自身のスレッドを終了 |
この処理で、Windows APIの呼び出しで今までとは別の手法があったので、併せて解説しておきます。
通常、APIを含め、callによる呼び出しの後は、必ずretnでスタックに設定されているアドレスへ戻る挙動をします。返していえば、そのスタックのアドレスに任意のアドレスをあらかじめ設定しておくことができれば、callの戻り先を任意に操作することができる、ともいえます。
その手法を用いて、APIを呼び出した後、その戻り先のアドレスを次のAPIのアドレスにすることで、連続的にAPIを呼び出すこともできます。当然、その場合は適切にAPIの引数をスタックに格納しておくことが必要となります。今回のランサムウェアでは、そのような技法が見られました(【図47】参照)。
RtlZeroMemoryのAPI開始時のスタックの先頭位置にはリターンアドレスが入っていますが、そのリターン先が0x77496B15となっています。これは、VirtualFreeのAPIのアドレスです。つまり、RtlZeroMemoryが終了し、リターンする先は、VirtualFreeの開始位置なのです。
RtlZeroMemoryのリターン先のアドレスの後には、RtlZeroMemoryの引数が続きます。そして、その後ろはVirtualFree終了時のリターンアドレスになりますが、今度はそこにCreateThreadのアドレスが格納されているのです。さらにその先は、RtlExitUserThreadが呼ばれるよう、同様にパラメータが続きます。これは、メモリに展開し、動いていたプログラムをクリアし、領域を開放するため、通常の処理のようにリターンして次のコードを実行させることができないことから編み出された方法と思われます。
【表1】の処理の流れと内容を見渡して見ましょう。
サービスマネージャから情報を引き出しているほか、レジストリを参照してセキュリティ製品の情報を参照している形跡があります。そして、その後に特に注目すべき挙動があります。VirtualAllocを行い、そこにRtlDecompressBufferで情報を展開しているのです。その後に、VirtualProtectで実行プログラム上のアクセス保護を変更して書き込めるようにしており、その後に展開した領域をクリアした上で解放しています。展開されたデータの内容と、その行先が気になりますが、行先の候補としてはアクセス保護を変更されたプログラム本体である可能性が考えられます。その後も、アクセス保護をこまめに変更しています。最後に、現在動作している自身のプログラムの領域をゼロクリアした上で開放し、新たなスレッドを起動して、自身のスレッドを終了しています。
この流れから、大きな2つの着眼点があると思います。一つは、VirtualAllocで確保された領域に展開された情報がどのようなもので、その情報を何に使用しているか、という点です。もう一つは、自身のスレッドを終了する前に起動しているスレッドは、どのような処理を行うのか、という点です。
このように、Windows APIの呼び出しをトレースするだけでも、ある程度マルウェアの動きを推定できます。また、精査するポイントを探すのに役立ちます。このことから、何かあったときに調査できる仕組みとして、日ごろからある程度のイベントを取得して、振る舞いを記録し、チェックできるようになっていれば、有用だと考えられます。Counter TackのSentinelなどは、組織内のPCの挙動イベントを記録し、サーバで保存、解析を行っており、有事の際の調査では初動の調査で威力を発揮するのではないかと思います。
では、先に述べた2つの着眼点について、できるだけ追いかけてみたいと思います。まず、この展開された領域にはどのような情報が格納されているのでしょうか。展開結果を探ってみたいと思います。
< 仮想メモリ内のWindows APIの使用 | 目 次 | 新たな仮想メモリに展開された情報の内容 > |