Development/Windows

#3 AMSI - AMSI Provider 작성하기

geunyeong 2021. 11. 8. 21:59

Table of Contents

    Abstract

    AMSI Provider는 Windows에 AMSI Provider로 등록되어 AMSI Scan 기능을 제공하는 서드 파티 멀웨어 방지 공급자를 말한다. 개요에서 보았듯이 AMSI Scan 기능을 이용하는 AMSI Client들은 AMSI API 함수를 호출하면 자동으로 레지스트리에서 AMSI Provider 관련 키와 값을 찾아 AMSI Provider의 DLL을 로드하고, AMSI DLL의 Scan 함수를 호출한다. 그러므로 AMSI Provider를 만든다는 것은 AMSI Client들이 로드할 DLL을 작성하고 이를 Windows 레지스트리에 등록하는 것을 의미한다.

    How to develop AMSI Provider

    Sample Code

    우선 Visual Studio로 DLL 프로젝트를 하나 생성하고 DllMain 함수를 작성한다.

    ////////////////////////////////////////////////////
    // 
    // dllmain.cpp of amsiprovider
    // 
    ////////////////////////////////////////////////////
    
    #include <Windows.h>
    
    #include "log.h"
    
    BOOL 
    APIENTRY 
    DllMain(
        HMODULE module,
        DWORD  reason_for_call,
        LPVOID reserved
    )
    {
        WCHAR current_module_name[MAX_PATH]{ 0, };
    
        switch (reason_for_call) {
        case DLL_PROCESS_ATTACH:
            OutputDebugStringA("[amsiprovider] AMSIProvider DLL was loaded");
    
            DisableThreadLibraryCalls(module);
    
            GetModuleFileName(NULL, current_module_name, ARRAYSIZE(current_module_name));
            OutputDebugFormatStringA("[amsiprovider] Current module is %ws", current_module_name);
            
            break;
    
        case DLL_PROCESS_DETACH:
            OutputDebugStringA("[amsiprovider] AMSIProvider DLL will be unloaded");
    
            break;
        }
    
        return TRUE;
    }

    AMSI DLL에서 DllMain의 역할은 크지 않다. 각자가 AMSI를 이용해 하고 싶은 일에 따라 초기화 작업이 필요하다면 DllMain에서 하면 된다. 예를 들어 중앙에서 AMSI 정책을 관리하는 환경이라면 DllMain은 AMSI 정책을 가져오는 코드를 작성하면 된다. 본 예제에서는 AMSI DLL을 로드한 프로세스 이름을 OutputDebugString으로 출력하도록 했다.

    다음은 IAntimalwareProvider 인터페이스를 상속한 AMSI Provider 클래스를 작성한다.

    • CAmsiProvider.h
    ////////////////////////////////////////////////////
    // 
    // CAmsiProvider.h of amsiprovider
    // 
    ////////////////////////////////////////////////////
    
    #pragma once
    
    #include <Windows.h>
    #include <amsi.h>
    #include <wrl/module.h>
    
    class
    	DECLSPEC_UUID("00000000-0000-0000-0000-000000000000") // UUID는 원하는 것으로 바꾸어도 무방
    	CAmsiProvider : public Microsoft::WRL::RuntimeClass<
    			Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
    			IAntimalwareProvider,
    			Microsoft::WRL::FtmBase
    	>
    {
    public:
    	IFACEMETHOD(Scan)(_In_ IAmsiStream* stream, _Out_ AMSI_RESULT* amsi_result) override;
    	IFACEMETHOD_(VOID, CloseSession)(_In_ ULONGLONG session) override;
    	IFACEMETHOD(DisplayName)(_Outptr_ LPWSTR* display_name) override;
    };
    • CAmsiProvider.cpp
    ////////////////////////////////////////////////////
    // 
    // CAmsiProvider.cpp of amsiprovider
    // 
    ////////////////////////////////////////////////////
    
    #include "CAmsiProvider.h"
    #include "log.h"
    
    // AMSI Stream에서 문자열 데이터를 가져오는 함수
    wchar_t* 
    GetAttributeStringValue(
    	_In_ IAmsiStream* stream,
    	_In_ AMSI_ATTRIBUTE attr
    )
    {
    	ULONG alloc_size = 0;
    	
    	HRESULT result = stream->GetAttribute(attr, 0, nullptr, &alloc_size);
    	if (result != E_NOT_SUFFICIENT_BUFFER) {
    		return nullptr;
    	}
    
    	unsigned char* value = new unsigned char[alloc_size];
    	result = stream->GetAttribute(attr, alloc_size, value, &alloc_size);
    	if (!SUCCEEDED(result)) {
    		delete[] value;
    		return nullptr;
    	}
    
    	return (wchar_t*)value;
    }
    
    // AMSI Stream에서 T 타입에 대한 데이터를 가져오는 함수
    template<typename T>
    T
    GetAttributeValue(
    	_In_ IAmsiStream* stream,
    	_In_ AMSI_ATTRIBUTE attr
    )
    {
    	T ret;
    
    	ULONG actual_size;
    	HRESULT result = stream->GetAttribute(attr, 
                                                  sizeof(T), 
                                                  reinterpret_cast<PBYTE>(&ret), 
                                                  &actual_size);
    	if (SUCCEEDED(result) && actual_size == sizeof(T)) {
    		return ret;
    	}
    
    	return T();
    }
    
    // AmsiScan* API 호출 시 불리는 스캔 함수
    HRESULT
    CAmsiProvider::Scan(
    	_In_ IAmsiStream* stream, 
    	_Out_ AMSI_RESULT* amsi_result
    )
    {
    	LPCWSTR app_name = 
    		GetAttributeStringValue(stream, AMSI_ATTRIBUTE_APP_NAME);
    	LPCWSTR content_name = 
    		GetAttributeStringValue(stream, AMSI_ATTRIBUTE_CONTENT_NAME);
    	ULONGLONG content_size = 
    		GetAttributeValue<ULONGLONG>(stream, AMSI_ATTRIBUTE_CONTENT_SIZE);
    	PBYTE content_addr = 
    		GetAttributeValue<PBYTE>(stream, AMSI_ATTRIBUTE_CONTENT_ADDRESS);
    
    	// content의 첫줄만 가져옴
    	LPWSTR line_end_addr = wcschr((wchar_t*)content_addr, L'\n');
    	if (!line_end_addr) {
    		line_end_addr = wcschr((wchar_t*)content_addr, L'\0');
    	}
    	size_t line_len = line_end_addr - (LPWSTR)content_addr;
    	LPWSTR contents = new WCHAR[line_len + 1];
    	ZeroMemory(contents, sizeof(WCHAR) * (line_len + 1));
    
    	wcsncpy_s(contents, line_len + 1, (const wchar_t*)content_addr, line_len);
    
    	// 출력
    	OutputDebugFormatStringA(
    		"[amsiprovider] AMSI data\n"
    		"App Name: %ws\n"
    		"Content Name: %ws\n"
    		"Content Size: %llu\n"
    		"Content: %ws",
    		(app_name && wcslen(app_name)) ? app_name : L"<empty>", 
    		(content_name && wcslen(content_name)) ? content_name : L"<empty>",
    		content_size, contents
    	);
    
    	delete[] app_name, content_name, contents;
    	
    	* amsi_result = AMSI_RESULT_CLEAN;
    
    	return S_OK;
    }
    
    VOID
    CAmsiProvider::CloseSession(
    	_In_ ULONGLONG session
    )
    {
    	OutputDebugStringA("[amsiprovider] Session was closed");
    }
    
    HRESULT 
    CAmsiProvider::DisplayName(
    	_Outptr_ LPWSTR* display_name
    )
    {
    	*display_name = const_cast<LPWSTR>(L"AMSI Provider Example");
    	return S_OK;
    }

    Description

    AMSI Provider class

    IAntimalwareProvider를 상속한 CAmsiProvider라는 클래스를 생성한 후, IAntimalwareProvider가 정의하고 있는 가상 메소드 Scan, CloseSession, DisplayName을 구현한다. Scan은 AmsiScanBuffer 나 AmsiScanString이 호출될 때 넘어오는 파라메터를 받는 콜백 함수다. 즉 Scan 함수에서 내가 원하는 스캔 기능을 작성하고, 허용/차단도 할 수 있다. 본 예제의 Scan 함수는 AMSI Scan 기능을 이용하는 응용 프로그램의 이름과 Content 이름, Content 크기, 실제 데이터에서 첫 줄(line)만 OutputDebugString으로 출력하도록 작성했다.

    CloseSession은 AmsiScanBuffer 나 AmsiScanString이 호출될 때 마다 생성되는 AMSI 세션이 종료될 때 호출되는 콜백함수다. CloseSession은 별다른 동작을 할 필요가 없다면 내용을 채울 필요는 없고, DisplayName은 위 코드를 그대로 사용하면 된다.

    Result

    AMSI Provider 클래스에 정의한 GUID는 후에 AMSI DLL을 AMSI Provider로 등록할 때 필요하다. 다른 GUID들과 중복되지 않도록 잘 정해야 한다. 위 코드를 빌드하면 AMSI DLL이 만들어진다. 이를 레지스트리에 등록하면 AMSI 등록이 완료된다. 아래 그림은 위 코드로 만든 AMSI DLL을 AMSI Provider로 등록한 후 PowerShell에서 echo asdf를 실행했을 때 나오는 Debug String을 DebugView로 확인한 모습이다.

     

    PowerShell.exe의 AMSI 로그

     

    PowerShell과 wscript, cscript 등 AMSI를 이용하는 응용 프로그램들은 프로그램이 처음 로드될 때 AMSI 초기화를 진행하므로 AMSI Provider를 등록하더라도 이미 실행되고 있던 응용 프로그램은 AMSI를 이용할 수 없다. 이럴 땐 응용 프로그램을 다시 시작하면 된다.

     

    Github

    AMSI Example 코드 깃허브(main 브랜치에 안보인다면 dev 브랜치에서 찾으면 된다)

    https://github.com/geun-yeong/amsi-example/tree/main

    'Development > Windows' 카테고리의 다른 글

    Windows I/O Manager  (0) 2021.11.19
    #5 AMSI - Regsvr32로 AMSI Provider 등록하기  (0) 2021.11.11
    #4 AMSI - AMSI Provider 등록하기  (0) 2021.11.09
    #2 AMSI - AMSI 스캔 기능 사용하기  (0) 2021.11.06
    #1 AMSI - 개요  (0) 2021.11.05