blob: c5e45d3c8323fa7611c7f1997dc83808f3ce1e9f [file]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/chrome_elf/third_party_dlls/hook.h"
#include <atomic>
#include <limits>
#include <string>
#include <windows.h>
#include <assert.h>
#include <ntstatus.h>
#include <psapi.h>
#include <winternl.h>
#include "base/compiler_specific.h"
#include "chrome/chrome_elf/crash/crash_helper.h"
#include "chrome/chrome_elf/hook_util/hook_util.h"
#include "chrome/chrome_elf/pe_image_safe/pe_image_safe.h"
#include "chrome/chrome_elf/sha1/sha1.h"
#include "chrome/chrome_elf/third_party_dlls/hardcoded_blocklist.h"
#include "chrome/chrome_elf/third_party_dlls/logs.h"
#include "chrome/chrome_elf/third_party_dlls/main.h"
#include "chrome/chrome_elf/third_party_dlls/packed_list_file.h"
#include "chrome/chrome_elf/third_party_dlls/packed_list_format.h"
#include "chrome/chrome_elf/third_party_dlls/public_api.h"
#include "sandbox/win/src/service_resolver.h"
// http://blogs.msdn.com/oldnewthing/archive/2004/10/25/247180.aspx
extern "C" IMAGE_DOS_HEADER __ImageBase;
namespace third_party_dlls {
namespace {
typedef ULONG SECTION_INHERIT;
typedef NTSTATUS(WINAPI* NtMapViewOfSectionFunction)(
IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN ULONG_PTR ZeroBits,
IN SIZE_T CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PSIZE_T ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType,
IN ULONG Win32Protect);
// Allocate storage for the hook thunk in a page of this module to save on doing
// an extra allocation at run time. We chose a reasonable size value (128) and
// check that it's sufficient later when performing the hooks.
#pragma section(".crthunk", read, execute)
static __declspec(allocate(".crthunk")) BYTE g_thunk_storage[128];
// Set if and when ApplyHook() has been successfully executed.
bool g_hook_active = false;
// Indicates if the hook was disabled after the hook was activated.
std::atomic<bool> g_hook_disabled(false);
// Set to a different NTSTATUS if an error occured while applying the hook.
std::atomic<int32_t> g_apply_hook_result(STATUS_SUCCESS);
// Check if a given |process| handle references THIS process.
bool IsTargetCurrentProcess(HANDLE process) {
return process == ::GetCurrentProcess() ||
::GetProcessId(process) == ::GetCurrentProcessId();
}
// Check if a given memory |section_base| is marked as an image.
bool IsSectionImage(PVOID section_base) {
assert(section_base);
MEMORY_BASIC_INFORMATION info = {};
if (!::VirtualQuery(section_base, &info, sizeof(info))) {
return false;
}
return (info.Type & MEM_IMAGE) == MEM_IMAGE;
}
// Query the full path backing the section.
// - Returned system wstring (UTF-16) will be empty() if anything fails.
std::wstring GetSectionName(PVOID section_base) {
assert(section_base);
constexpr DWORD kMaxNameSize = MAX_PATH + 1;
WCHAR name[kMaxNameSize];
DWORD size = ::GetMappedFileName(::GetCurrentProcess(), section_base, name,
kMaxNameSize);
if (size == 0 || size >= kMaxNameSize) {
return std::wstring();
}
return name;
}
// Utility function for converting UTF-16 to UTF-8.
bool UTF16ToUTF8(const std::wstring& utf16, std::string* utf8) {
assert(utf8);
if (utf16.empty()) {
utf8->clear();
return true;
}
int size_needed_bytes = ::WideCharToMultiByte(CP_UTF8, 0, &utf16[0],
static_cast<int>(utf16.size()),
nullptr, 0, nullptr, nullptr);
if (!size_needed_bytes)
return false;
utf8->resize(size_needed_bytes);
return ::WideCharToMultiByte(CP_UTF8, 0, &utf16[0],
static_cast<int>(utf16.size()), &(*utf8)[0],
size_needed_bytes, nullptr, nullptr);
}
// Helper function to contain the data mining for the values needed.
// - |image_name| and |section_basename| will be lowercased. |section_path|
// will be left untouched, preserved for case-sensitive operations with it.
// - All strings returned are UTF-8. Treat accordingly.
// - This function succeeds if image_name || section_* is found.
// Note: |section_path| contains |section_basename|, if the section name is
// successfully mined.
bool GetDataFromImage(PVOID buffer,
DWORD buffer_size,
DWORD* time_date_stamp,
DWORD* image_size,
std::string* image_name,
std::string* section_path,
std::string* section_basename) {
assert(buffer && buffer_size && time_date_stamp && image_size && image_name &&
section_path && section_basename);
image_name->clear();
section_path->clear();
section_basename->clear();
pe_image_safe::PEImageSafe image(reinterpret_cast<HMODULE>(buffer),
buffer_size);
PIMAGE_FILE_HEADER file_header = image.GetFileHeader();
if (!file_header ||
image.GetImageBitness() == pe_image_safe::ImageBitness::kUnknown) {
return false;
}
*time_date_stamp = file_header->TimeDateStamp;
if (image.GetImageBitness() == pe_image_safe::ImageBitness::k64) {
PIMAGE_OPTIONAL_HEADER64 opt_header =
reinterpret_cast<PIMAGE_OPTIONAL_HEADER64>(image.GetOptionalHeader());
*image_size = opt_header->SizeOfImage;
} else {
PIMAGE_OPTIONAL_HEADER32 opt_header =
reinterpret_cast<PIMAGE_OPTIONAL_HEADER32>(image.GetOptionalHeader());
*image_size = opt_header->SizeOfImage;
}
//----------------------------------------------------------------------------
// Get the module name 1) out of PE Export Dir, and 2) from the memory section
// name.
PIMAGE_EXPORT_DIRECTORY exports = image.GetExportDirectory();
// IMAGE_EXPORT_DIRECTORY::Name is a RVA to an asciiz string with the name of
// this DLL. Ascii is UTF-8 compliant.
if (exports && exports->Name + MAX_PATH <= buffer_size) {
const char* name =
reinterpret_cast<const char*>(image.RVAToAddr(exports->Name));
*image_name = std::string(name, ::strnlen(name, MAX_PATH));
}
// Lowercase |image_name|.
const auto tolower = [](auto c) {
return (c >= 'A' && c <= 'Z') ? (c + ('a' - 'A')) : c;
};
for (size_t i = 0; i < image_name->size(); i++)
(*image_name)[i] = tolower((*image_name)[i]);
std::wstring temp_section_path = GetSectionName(buffer);
// For now, consider it a success if at least one source results in a name.
// Allow for the rare case of one or the other not being there.
// (E.g.: a module could have no export directory.)
if (image_name->empty() && temp_section_path.empty())
return false;
// There is further processing to do on the section path now.
if (temp_section_path.empty())
return true;
// Extract the section basename from the section path.
std::wstring temp_section_basename;
size_t sep = temp_section_path.find_last_of('\\');
if (sep == std::string::npos && !temp_section_path.empty()) {
temp_section_basename = temp_section_path;
} else if (sep != std::string::npos && temp_section_path.back() != '\\') {
temp_section_basename = temp_section_path.substr(sep + 1);
}
// Lowercase |section_basename|.
for (size_t i = 0; i < temp_section_basename.size(); i++)
temp_section_basename[i] = tolower(temp_section_basename[i]);
// Convert section strings from UTF-16 to UTF-8.
return UTF16ToUTF8(temp_section_path, section_path) &&
UTF16ToUTF8(temp_section_basename, section_basename);
}
//------------------------------------------------------------------------------
// Local interceptor function implementation
//
// 1) Allow mapping.
// 2) Return if OS failure or not interested in section.
// 3) Mine the data needed out of the PE headers.
// 4) Lookup module in local cache (blocking).
// 5) Temporarily check old (deprecated) blocklist.
// 6) Unmap view if blocking required.
// 7) Log the result either way.
//------------------------------------------------------------------------------
// CFG must be disabled for the call to orig_MapViewOfSection below.
DISABLE_CFI_ICALL
NTSTATUS NewNtMapViewOfSectionImpl(
NtMapViewOfSectionFunction orig_MapViewOfSection,
HANDLE section,
HANDLE process,
PVOID* base,
ULONG_PTR zero_bits,
SIZE_T commit_size,
PLARGE_INTEGER offset,
PSIZE_T view_size,
SECTION_INHERIT inherit,
ULONG allocation_type,
ULONG protect) {
assert(IsThirdPartyInitialized());
// Need to initially allow the mapping to go through, to access the module
// info we really need to make any decisions. It will be unmapped if
// necessary later in this function.
NTSTATUS ret = orig_MapViewOfSection(section, process, base, zero_bits,
commit_size, offset, view_size, inherit,
allocation_type, protect);
if (g_hook_disabled.load(std::memory_order_relaxed))
return ret;
// If there was an OS-level failure, if the mapping target is NOT this
// process, or if the section is not a (valid) Portable Executable,
// we're not interested. Return the OS-level result code.
if (!NT_SUCCESS(ret) || !IsTargetCurrentProcess(process) ||
!IsSectionImage(*base)) {
return ret;
}
// Mine the data needed from the PE headers.
DWORD time_date_stamp = 0;
DWORD image_size = 0;
std::string image_name;
std::string section_path;
std::string section_basename;
assert(*view_size < std::numeric_limits<DWORD>::max());
// A memory section can be > 32-bits, but an image/PE in memory can only be <=
// 32-bits in size. That's a limitation of Windows and its interactions with
// processors. No section that appears to be an image (checked above) should
// have such a large size.
if (!GetDataFromImage(*base, static_cast<DWORD>(*view_size), &time_date_stamp,
&image_size, &image_name, &section_path,
&section_basename)) {
return ret;
}
// Note that one of either image_name or section_basename can be empty.
elf_sha1::Digest image_name_hash;
if (!image_name.empty())
image_name_hash = elf_sha1::SHA1HashString(image_name);
elf_sha1::Digest section_basename_hash;
if (!section_basename.empty())
section_basename_hash = elf_sha1::SHA1HashString(section_basename);
elf_sha1::Digest fingerprint_hash = elf_sha1::SHA1HashString(
GetFingerprintString(time_date_stamp, image_size));
// Check sources for blocklist decision.
bool block = false;
if (!image_name.empty() &&
IsModuleListed(image_name_hash, fingerprint_hash)) {
// 1) Third-party DLL blocklist, check for image name from PE header.
block = true;
} else if (!section_basename.empty() &&
section_basename_hash != image_name_hash &&
IsModuleListed(section_basename_hash, fingerprint_hash)) {
// 2) Third-party DLL blocklist, check for image name from the section.
block = true;
} else if (!image_name.empty() && DllMatch(image_name)) {
// 3) Hard-coded blocklist with name from PE header (deprecated).
block = true;
} else if (!section_basename.empty() &&
section_basename.compare(image_name) != 0 &&
DllMatch(section_basename)) {
// 4) Hard-coded blocklist with name from the section (deprecated).
block = true;
}
// Else, no block.
// UNMAP the view. This image is being blocked.
if (block) {
::UnmapViewOfFile(*base);
*base = nullptr;
ret = STATUS_UNSUCCESSFUL;
}
// LOG!
// - If there was a failure getting |section_path|, at least pass image_name.
LogLoadAttempt((block ? third_party_dlls::LogType::kBlocked
: third_party_dlls::LogType::kAllowed),
image_size, time_date_stamp,
section_path.empty() ? image_name : section_path);
return ret;
}
} // namespace
//------------------------------------------------------------------------------
// Public defines & functions
//------------------------------------------------------------------------------
// Interception of NtMapViewOfSection within the current process.
// - This/these replacement functions should never be called directly. They are
// called from the hook patch.
NTSTATUS WINAPI
NewNtMapViewOfSection(NtMapViewOfSectionFunction orig_MapViewOfSection,
HANDLE section,
HANDLE process,
PVOID* base,
ULONG_PTR zero_bits,
SIZE_T commit_size,
PLARGE_INTEGER offset,
PSIZE_T view_size,
SECTION_INHERIT inherit,
ULONG allocation_type,
ULONG protect) {
NTSTATUS ret = STATUS_UNSUCCESSFUL;
__try {
ret = NewNtMapViewOfSectionImpl(
orig_MapViewOfSection, section, process, base, zero_bits, commit_size,
offset, view_size, inherit, allocation_type, protect);
} __except (elf_crash::GenerateCrashDump(GetExceptionInformation())) {
}
return ret;
}
#if defined(_WIN64)
// x64 has an extra layer of indirection. This just wraps the above
// interceptor function that x86 hits directly.
NTSTATUS WINAPI NewNtMapViewOfSection64(HANDLE section,
HANDLE process,
PVOID* base,
ULONG_PTR zero_bits,
SIZE_T commit_size,
PLARGE_INTEGER offset,
PSIZE_T view_size,
SECTION_INHERIT inherit,
ULONG allocation_type,
ULONG protect) {
return NewNtMapViewOfSection(
reinterpret_cast<NtMapViewOfSectionFunction>(g_thunk_storage), section,
process, base, zero_bits, commit_size, offset, view_size, inherit,
allocation_type, protect);
}
#endif
ThirdPartyStatus ApplyHook() {
constexpr wchar_t kNtdllName[] = L"ntdll.dll";
// Debug check: ApplyHook() should not be called more than once.
assert(!g_hook_active);
// Prep system-service thunk via the appropriate ServiceResolver instance.
sandbox::ServiceResolverThunk thunk(::GetCurrentProcess(), /*relaxed=*/false);
assert(sizeof(g_thunk_storage) >= thunk.GetThunkSize());
// Set target process to self.
thunk.AllowLocalPatches();
// Mark the thunk storage as readable and writeable, since we
// are ready to write to it now.
DWORD old_protect = 0;
if (!::VirtualProtect(g_thunk_storage, sizeof(g_thunk_storage),
PAGE_EXECUTE_READWRITE, &old_protect)) {
return ThirdPartyStatus::kHookVirtualProtectFailure;
}
// Replace the default NtMapViewOfSection system service with our patched
// version.
#if defined(_WIN64)
void* entry_point = reinterpret_cast<void*>(&NewNtMapViewOfSection64);
#else
void* entry_point = reinterpret_cast<void*>(&NewNtMapViewOfSection);
#endif // defined(_WIN64)
// Setup() applies the system-service patch, and stores a copy of the original
// system service coded in |g_thunk_storage|.
NTSTATUS ntstatus = thunk.Setup(
::GetModuleHandle(kNtdllName), reinterpret_cast<void*>(&__ImageBase),
"NtMapViewOfSection", nullptr, entry_point, g_thunk_storage,
sizeof(g_thunk_storage), nullptr);
if (!NT_SUCCESS(ntstatus)) {
// Remember the status code.
g_apply_hook_result.store(ntstatus, std::memory_order_relaxed);
return ThirdPartyStatus::kHookApplyFailure;
}
// Mark the thunk storage (original system service) as executable and prevent
// any future writes to it.
// - Do not treat this as a fatal error on failure.
if (!::VirtualProtect(g_thunk_storage, sizeof(g_thunk_storage),
PAGE_EXECUTE_READ, &old_protect)) {
assert(false);
}
g_hook_active = true;
return ThirdPartyStatus::kSuccess;
}
bool GetDataFromImageForTesting(PVOID mapped_image,
DWORD* time_date_stamp,
DWORD* image_size,
std::string* image_name,
std::string* section_path,
std::string* section_basename) {
return GetDataFromImage(mapped_image, pe_image_safe::kImageSizeNotSet,
time_date_stamp, image_size, image_name, section_path,
section_basename);
}
} // namespace third_party_dlls
void DisableHook() {
third_party_dlls::g_hook_disabled.store(true, std::memory_order_relaxed);
}
int32_t GetApplyHookResult() {
return third_party_dlls::g_apply_hook_result.load(std::memory_order_relaxed);
}