From 3a029763047b5144e9fdca7f4c0d70553ddbf2e8 Mon Sep 17 00:00:00 2001 From: Elinor Fung <47805090+elinor-fung@users.noreply.github.com> Date: Fri, 15 Mar 2019 12:33:43 -0700 Subject: Port AMSI scanning for assembly loads (#23231) * Port AMSI scanning for assembly loads * Define PLATFORM_WINDOWS for Windows build * Remove check for LOAD_LIBRARY_SEARCH_SYSTEM32 support --- configurecompiler.cmake | 30 ++++++------ src/vm/CMakeLists.txt | 2 + src/vm/amsi.cpp | 120 +++++++++++++++++++++++++++++++++++++++++++++++ src/vm/amsi.h | 16 +++++++ src/vm/peimagelayout.cpp | 82 +++++++++++++++++++------------- 5 files changed, 204 insertions(+), 46 deletions(-) create mode 100644 src/vm/amsi.cpp create mode 100644 src/vm/amsi.h diff --git a/configurecompiler.cmake b/configurecompiler.cmake index d9b2a66e63..39c2e7e410 100644 --- a/configurecompiler.cmake +++ b/configurecompiler.cmake @@ -221,11 +221,11 @@ if (WIN32) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DEBUG /PDBCOMPRESS") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:1572864") - # Temporarily disable incremental link due to incremental linking CFG bug crashing crossgen. + # Temporarily disable incremental link due to incremental linking CFG bug crashing crossgen. # See https://github.com/dotnet/coreclr/issues/12592 # This has been fixed in VS 2017 Update 5 but we're keeping this around until everyone is off # the versions that have the bug. The bug manifests itself as a bad crash. - set(NO_INCREMENTAL_LINKER_FLAGS "/INCREMENTAL:NO") + set(NO_INCREMENTAL_LINKER_FLAGS "/INCREMENTAL:NO") # Debug build specific flags set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "/NOVCFEATURE ${NO_INCREMENTAL_LINKER_FLAGS}") @@ -396,6 +396,8 @@ if (CLR_CMAKE_PLATFORM_UNIX) endif(CLR_CMAKE_PLATFORM_UNIX) if (WIN32) + add_definitions(-DPLATFORM_WINDOWS=1) + # Define the CRT lib references that link into Desktop imports set(STATIC_MT_CRT_LIB "libcmt$<$,$>:d>.lib") set(STATIC_MT_VCRT_LIB "libvcruntime$<$,$>:d>.lib") @@ -469,7 +471,7 @@ if (CLR_CMAKE_PLATFORM_UNIX) # and so the compiler thinks that there is a mistake. add_compile_options(-Wno-constant-logical-operand) # We use pshpack1/2/4/8.h and poppack.h headers to set and restore packing. However - # clang 6.0 complains when the packing change lifetime is not contained within + # clang 6.0 complains when the packing change lifetime is not contained within # a header file. add_compile_options(-Wno-pragma-pack) @@ -559,22 +561,22 @@ if (WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /guard:cf") set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} /guard:cf") - # Statically linked CRT (libcmt[d].lib, libvcruntime[d].lib and libucrt[d].lib) by default. This is done to avoid - # linking in VCRUNTIME140.DLL for a simplified xcopy experience by reducing the dependency on VC REDIST. - # - # For Release builds, we shall dynamically link into uCRT [ucrtbase.dll] (which is pushed down as a Windows Update on downlevel OS) but - # wont do the same for debug/checked builds since ucrtbased.dll is not redistributable and Debug/Checked builds are not - # production-time scenarios. - add_compile_options($<$,$>:/MT>) - add_compile_options($<$,$>:/MTd>) + # Statically linked CRT (libcmt[d].lib, libvcruntime[d].lib and libucrt[d].lib) by default. This is done to avoid + # linking in VCRUNTIME140.DLL for a simplified xcopy experience by reducing the dependency on VC REDIST. + # + # For Release builds, we shall dynamically link into uCRT [ucrtbase.dll] (which is pushed down as a Windows Update on downlevel OS) but + # wont do the same for debug/checked builds since ucrtbased.dll is not redistributable and Debug/Checked builds are not + # production-time scenarios. + add_compile_options($<$,$>:/MT>) + add_compile_options($<$,$>:/MTd>) set(CMAKE_ASM_MASM_FLAGS "${CMAKE_ASM_MASM_FLAGS} /ZH:SHA_256") - + if (CLR_CMAKE_TARGET_ARCH_ARM OR CLR_CMAKE_TARGET_ARCH_ARM64) # Contracts work too slow on ARM/ARM64 DEBUG/CHECKED. add_definitions(-DDISABLE_CONTRACTS) - endif (CLR_CMAKE_TARGET_ARCH_ARM OR CLR_CMAKE_TARGET_ARCH_ARM64) - + endif (CLR_CMAKE_TARGET_ARCH_ARM OR CLR_CMAKE_TARGET_ARCH_ARM64) + endif (WIN32) if(CLR_CMAKE_ENABLE_CODE_COVERAGE) diff --git a/src/vm/CMakeLists.txt b/src/vm/CMakeLists.txt index 7d7ecd9ca5..397f4e2ea5 100644 --- a/src/vm/CMakeLists.txt +++ b/src/vm/CMakeLists.txt @@ -553,12 +553,14 @@ endif(FEATURE_STANDALONE_GC) if(WIN32) set(VM_SOURCES_DAC_AND_WKS_WIN32 + amsi.cpp clrtocomcall.cpp rcwwalker.cpp winrttypenameconverter.cpp ) set(VM_HEADERS_DAC_AND_WKS_WIN32 + amsi.h clrtocomcall.h rcwwalker.h winrttypenameconverter.h diff --git a/src/vm/amsi.cpp b/src/vm/amsi.cpp new file mode 100644 index 0000000000..47da6708e5 --- /dev/null +++ b/src/vm/amsi.cpp @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// +// File: amsi.cpp +// + +#include "common.h" +#include "amsi.h" + +namespace +{ + // https://docs.microsoft.com/en-us/windows/desktop/api/amsi/ + DECLARE_HANDLE(HAMSICONTEXT); + DECLARE_HANDLE(HAMSISESSION); + + enum AMSI_RESULT + { + AMSI_RESULT_CLEAN = 0, + AMSI_RESULT_NOT_DETECTED = 1, + AMSI_RESULT_BLOCKED_BY_ADMIN_START = 0x4000, + AMSI_RESULT_BLOCKED_BY_ADMIN_END = 0x4fff, + AMSI_RESULT_DETECTED = 0x8000 + } AMSI_RESULT; + + bool AmsiResultIsMalware(DWORD result) + { + return result >= AMSI_RESULT_DETECTED; + } + + bool AmsiResultIsBlockedByAdmin(DWORD result) + { + return result >= AMSI_RESULT_BLOCKED_BY_ADMIN_START + && result <= AMSI_RESULT_BLOCKED_BY_ADMIN_END; + } + + using PAMSI_AMSISCANBUFFER_API = HRESULT(WINAPI *)( + _In_ HAMSICONTEXT amsiContext, + _In_ PVOID buffer, + _In_ ULONG length, + _In_ LPCWSTR contentName, + _In_opt_ HAMSISESSION session, + _Out_ DWORD *result); + + using PAMSI_AMSIINITIALIZE_API = HRESULT(WINAPI *)( + _In_ LPCWSTR appName, + _Out_ HAMSICONTEXT *amsiContext); + + PAMSI_AMSISCANBUFFER_API AmsiScanBuffer; + HAMSICONTEXT s_amsiContext; + CRITSEC_COOKIE s_csAmsi; + + bool InitializeLock() + { + if (s_csAmsi != nullptr) + return true; + + CRITSEC_COOKIE lock = ClrCreateCriticalSection(CrstLeafLock, CRST_REENTRANCY); + if (lock == nullptr) + return false; + + if (InterlockedCompareExchangeT(&s_csAmsi, lock, nullptr) != nullptr) + ClrDeleteCriticalSection(lock); + + return true; + } +} + +// Here we will invoke into AmsiScanBuffer, a centralized area for non-OS +// programs to report into Defender (and potentially other anti-malware tools). +// This should only run on in memory loads, Assembly.Load(byte[]) for example. +// Loads from disk are already instrumented by Defender, so calling AmsiScanBuffer +// wouldn't do anything. +bool Amsi::IsBlockedByAmsiScan(PVOID flatImageBytes, COUNT_T size) +{ + STANDARD_VM_CONTRACT; + + if (!InitializeLock()) + return false; + + // Lazily initialize AMSI because it is very expensive + { + CRITSEC_Holder csh(s_csAmsi); + + // Cache that we failed if this didn't work so we don't keep trying to reinitialize + static bool amsiInitializationAttempted = false; + if (s_amsiContext == nullptr && !amsiInitializationAttempted) + { + HMODULE amsi = CLRLoadLibraryEx(W("amsi.dll"), nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (amsi != nullptr) + { + PAMSI_AMSIINITIALIZE_API AmsiInitialize = (PAMSI_AMSIINITIALIZE_API)GetProcAddress(amsi, "AmsiInitialize"); + if (AmsiInitialize != nullptr) + { + HAMSICONTEXT amsiContext = nullptr; + if (AmsiInitialize(W("coreclr"), &amsiContext) == S_OK) + { + AmsiScanBuffer = (PAMSI_AMSISCANBUFFER_API)GetProcAddress(amsi, "AmsiScanBuffer"); + if (AmsiScanBuffer != nullptr) + { + s_amsiContext = amsiContext; + } + } + } + } + + amsiInitializationAttempted = true; + } + } + + if (s_amsiContext == nullptr || AmsiScanBuffer == nullptr) + return false; + + DWORD result; + HRESULT hr = AmsiScanBuffer(s_amsiContext, flatImageBytes, size, nullptr, nullptr, &result); + if (hr == S_OK && (AmsiResultIsMalware(result) || AmsiResultIsBlockedByAdmin(result))) + return true; + + return false; +} \ No newline at end of file diff --git a/src/vm/amsi.h b/src/vm/amsi.h new file mode 100644 index 0000000000..1583c4f18e --- /dev/null +++ b/src/vm/amsi.h @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// +// File: amsi.h +// + +#ifndef __AMSI_H__ +#define __AMSI_H__ + +namespace Amsi +{ + bool IsBlockedByAmsiScan(void *flatImageBytes, COUNT_T size); +}; + +#endif // __AMSI_H__ diff --git a/src/vm/peimagelayout.cpp b/src/vm/peimagelayout.cpp index 5bc4e361c5..e839e8ffb0 100644 --- a/src/vm/peimagelayout.cpp +++ b/src/vm/peimagelayout.cpp @@ -3,12 +3,16 @@ // See the LICENSE file in the project root for more information. -// +// #include "common.h" #include "peimagelayout.h" #include "peimagelayout.inl" +#if defined(PLATFORM_WINDOWS) && !defined(CROSSGEN_COMPILE) +#include "amsi.h" +#endif + #ifndef DACCESS_COMPILE PEImageLayout* PEImageLayout::CreateFlat(const void *flat, COUNT_T size,PEImage* pOwner) { @@ -67,7 +71,7 @@ PEImageLayout* PEImageLayout::Map(HANDLE hFile, PEImage* pOwner) POSTCONDITION(RETVAL->CheckFormat()); } CONTRACT_END; - + PEImageLayoutHolder pAlloc(new MappedImageLayout(hFile,pOwner)); if (pAlloc->GetBase()==NULL) { @@ -81,7 +85,7 @@ PEImageLayout* PEImageLayout::Map(HANDLE hFile, PEImage* pOwner) else if(!pAlloc->CheckFormat()) ThrowHR(COR_E_BADIMAGEFORMAT); - RETURN pAlloc.Extract(); + RETURN pAlloc.Extract(); } #ifdef FEATURE_PREJIT @@ -292,7 +296,7 @@ void PEImageLayout::ApplyBaseRelocations() #endif // FEATURE_PREJIT -RawImageLayout::RawImageLayout(const void *flat, COUNT_T size,PEImage* pOwner) +RawImageLayout::RawImageLayout(const void *flat, COUNT_T size, PEImage* pOwner) { CONTRACTL { @@ -308,14 +312,28 @@ RawImageLayout::RawImageLayout(const void *flat, COUNT_T size,PEImage* pOwner) if (size) { - HandleHolder mapping(WszCreateFileMapping(INVALID_HANDLE_VALUE, NULL, - PAGE_READWRITE, 0, - size, NULL)); +#if defined(PLATFORM_WINDOWS) && !defined(CROSSGEN_COMPILE) + if (Amsi::IsBlockedByAmsiScan((void*)flat, size)) + { + // This is required to throw a BadImageFormatException for compatibility, but + // use the message from ERROR_VIRUS_INFECTED to give better insight on what's wrong + SString virusHrString; + GetHRMsg(HRESULT_FROM_WIN32(ERROR_VIRUS_INFECTED), virusHrString); + ThrowHR(COR_E_BADIMAGEFORMAT, virusHrString); + } +#endif // defined(PLATFORM_WINDOWS) && !defined(CROSSGEN_COMPILE) + + HandleHolder mapping(WszCreateFileMapping(INVALID_HANDLE_VALUE, + NULL, + PAGE_READWRITE, + 0, + size, + NULL)); if (mapping==NULL) ThrowLastError(); m_DataCopy.Assign(CLRMapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0)); if(m_DataCopy==NULL) - ThrowLastError(); + ThrowLastError(); memcpy(m_DataCopy,flat,size); flat=m_DataCopy; } @@ -341,14 +359,14 @@ RawImageLayout::RawImageLayout(const void *mapped, PEImage* pOwner, BOOL bTakeOw #ifndef FEATURE_PAL PathString wszDllName; WszGetModuleFileName((HMODULE)mapped, wszDllName); - + m_LibraryHolder=CLRLoadLibraryEx(wszDllName,NULL,GetLoadWithAlteredSearchPathFlag()); #else // !FEATURE_PAL _ASSERTE(!"bTakeOwnership Should not be used on FEATURE_PAL"); #endif // !FEATURE_PAL } - TESTHOOKCALL(ImageMapped(GetPath(),mapped,bFixedUp?IM_IMAGEMAP|IM_FIXEDUP:IM_IMAGEMAP)); + TESTHOOKCALL(ImageMapped(GetPath(),mapped,bFixedUp?IM_IMAGEMAP|IM_FIXEDUP:IM_IMAGEMAP)); IfFailThrow(Init((void*)mapped,(bool)(bFixedUp!=FALSE))); } @@ -360,32 +378,32 @@ ConvertedImageLayout::ConvertedImageLayout(PEImageLayout* source) STANDARD_VM_CHECK; } CONTRACTL_END; - m_Layout=LAYOUT_LOADED; + m_Layout=LAYOUT_LOADED; m_pOwner=source->m_pOwner; _ASSERTE(!source->IsMapped()); - + if (!source->HasNTHeaders()) EEFileLoadException::Throw(GetPath(), COR_E_BADIMAGEFORMAT); LOG((LF_LOADER, LL_INFO100, "PEImage: Opening manually mapped stream\n")); - m_FileMap.Assign(WszCreateFileMapping(INVALID_HANDLE_VALUE, NULL, - PAGE_READWRITE, 0, + m_FileMap.Assign(WszCreateFileMapping(INVALID_HANDLE_VALUE, NULL, + PAGE_READWRITE, 0, source->GetVirtualSize(), NULL)); if (m_FileMap == NULL) ThrowLastError(); - - m_FileView.Assign(CLRMapViewOfFileEx(m_FileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0, + + m_FileView.Assign(CLRMapViewOfFileEx(m_FileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0, (void *) source->GetPreferredBase())); if (m_FileView == NULL) m_FileView.Assign(CLRMapViewOfFile(m_FileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)); - + if (m_FileView == NULL) ThrowLastError(); - + source->LayoutILOnly(m_FileView, TRUE); //@TODO should be false for streams - TESTHOOKCALL(ImageMapped(GetPath(),m_FileView,IM_IMAGEMAP)); + TESTHOOKCALL(ImageMapped(GetPath(),m_FileView,IM_IMAGEMAP)); IfFailThrow(Init(m_FileView)); #ifdef CROSSGEN_COMPILE @@ -420,7 +438,7 @@ MappedImageLayout::MappedImageLayout(HANDLE hFile, PEImage* pOwner) #ifndef CROSSGEN_COMPILE // Capture last error as it may get reset below. - + DWORD dwLastError = GetLastError(); // There is no reflection-only load on CoreCLR and so we can always throw an error here. // It is important on Windows Phone. All assemblies that we load must have SEC_IMAGE set @@ -452,14 +470,14 @@ MappedImageLayout::MappedImageLayout(HANDLE hFile, PEImage* pOwner) m_FileView.Assign(CLRMapViewOfFile(m_FileMap, 0, 0, 0, 0)); if (m_FileView == NULL) ThrowLastError(); - TESTHOOKCALL(ImageMapped(GetPath(),m_FileView,IM_IMAGEMAP)); + TESTHOOKCALL(ImageMapped(GetPath(),m_FileView,IM_IMAGEMAP)); IfFailThrow(Init((void *) m_FileView)); #ifdef CROSSGEN_COMPILE //Do base relocation for PE. Unlike LoadLibrary, MapViewOfFile will not do that for us even with SEC_IMAGE if (pOwner->IsTrustedNativeImage()) { - // This should never happen in correctly setup system, but do a quick check right anyway to + // This should never happen in correctly setup system, but do a quick check right anyway to // avoid running too far with bogus data if (!HasCorHeader()) @@ -510,7 +528,7 @@ MappedImageLayout::MappedImageLayout(HANDLE hFile, PEImage* pOwner) if (m_LoadedFile == NULL) { - // For CoreCLR, try to load all files via LoadLibrary first. If LoadLibrary did not work, retry using + // For CoreCLR, try to load all files via LoadLibrary first. If LoadLibrary did not work, retry using // regular mapping - but not for native images. if (pOwner->IsTrustedNativeImage()) ThrowHR(E_FAIL); // we don't have any indication of what kind of failure. Possibly a corrupt image. @@ -553,14 +571,14 @@ LoadedImageLayout::LoadedImageLayout(PEImage* pOwner, BOOL bNTSafeLoad, BOOL bTh PRECONDITION(CheckPointer(pOwner)); } CONTRACTL_END; - - m_Layout=LAYOUT_LOADED; + + m_Layout=LAYOUT_LOADED; m_pOwner=pOwner; DWORD dwFlags = GetLoadWithAlteredSearchPathFlag(); if (bNTSafeLoad) dwFlags|=DONT_RESOLVE_DLL_REFERENCES; - + m_Module = CLRLoadLibraryEx(pOwner->GetPath(), NULL, dwFlags); if (m_Module == NULL) { @@ -584,11 +602,11 @@ FlatImageLayout::FlatImageLayout(HANDLE hFile, PEImage* pOwner) { CONSTRUCTOR_CHECK; STANDARD_VM_CHECK; - PRECONDITION(CheckPointer(pOwner)); + PRECONDITION(CheckPointer(pOwner)); } CONTRACTL_END; - m_Layout=LAYOUT_FLAT; - m_pOwner=pOwner; + m_Layout=LAYOUT_FLAT; + m_pOwner=pOwner; LOG((LF_LOADER, LL_INFO100, "PEImage: Opening flat %S\n", (LPCWSTR) GetPath())); COUNT_T size = SafeGetFileSize(hFile, NULL); @@ -596,9 +614,9 @@ FlatImageLayout::FlatImageLayout(HANDLE hFile, PEImage* pOwner) { ThrowLastError(); } - + // It's okay if resource files are length zero - if (size > 0) + if (size > 0) { m_FileMap.Assign(WszCreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL)); if (m_FileMap == NULL) @@ -608,7 +626,7 @@ FlatImageLayout::FlatImageLayout(HANDLE hFile, PEImage* pOwner) if (m_FileView == NULL) ThrowLastError(); } - TESTHOOKCALL(ImageMapped(GetPath(),m_FileView,IM_FLAT)); + TESTHOOKCALL(ImageMapped(GetPath(),m_FileView,IM_FLAT)); Init(m_FileView, size); } -- cgit v1.2.3