[chrome_elf whitelist] Add whitelist logging support.
- Log all blocked or allowed attempts. - Export API to drain log, and to register for log notification. - New tests. BUG=769590 TEST=chrome_elf_unittests.exe, Whitelist* Change-Id: I4f1ec911efbe3eada231aa36b0fadceb3756521e Reviewed-on: https://chromium-review.googlesource.com/847894 Commit-Queue: Penny MacNeil <pennymac@chromium.org> Reviewed-by: Greg Thompson <grt@chromium.org> Cr-Commit-Position: refs/heads/master@{#532643}
This commit is contained in:

committed by
Commit Bot

parent
17762b3c42
commit
73cde6de3a
chrome_elf
@ -64,6 +64,7 @@ shared_library("chrome_elf") {
|
||||
"//chrome/install_static:primary_module",
|
||||
"//components/crash/content/app:crash_export_thunks",
|
||||
]
|
||||
|
||||
configs += [ "//build/config/win:windowed" ]
|
||||
configs -= [ "//build/config/win:console" ]
|
||||
|
||||
@ -137,6 +138,10 @@ source_set("sha1") {
|
||||
]
|
||||
}
|
||||
|
||||
# This source_set defines whitelist-related structures and APIs used from
|
||||
# outside chrome_elf.dll. The APIs are exported from chrome_elf (add a
|
||||
# data_dep on //chrome_elf:chrome_elf), which will always be loaded before
|
||||
# chrome.dll.
|
||||
source_set("whitelist_packed_format") {
|
||||
sources = [
|
||||
"whitelist/whitelist_packed_format.cc",
|
||||
@ -233,6 +238,7 @@ static_library("nt_registry") {
|
||||
}
|
||||
|
||||
static_library("whitelist") {
|
||||
visibility = [ ":*" ] # Only targets in this file can depend on this.
|
||||
sources = [
|
||||
"whitelist/whitelist.cc",
|
||||
"whitelist/whitelist.h",
|
||||
@ -240,6 +246,8 @@ static_library("whitelist") {
|
||||
"whitelist/whitelist_file.h",
|
||||
"whitelist/whitelist_ime.cc",
|
||||
"whitelist/whitelist_ime.h",
|
||||
"whitelist/whitelist_log.cc",
|
||||
"whitelist/whitelist_log.h",
|
||||
]
|
||||
deps = [
|
||||
":nt_registry",
|
||||
@ -264,6 +272,7 @@ test("chrome_elf_unittests") {
|
||||
"sha1/sha1_unittest.cc",
|
||||
"whitelist/whitelist_file_unittest.cc",
|
||||
"whitelist/whitelist_ime_unittest.cc",
|
||||
"whitelist/whitelist_log_unittest.cc",
|
||||
]
|
||||
include_dirs = [ "$target_gen_dir" ]
|
||||
deps = [
|
||||
|
@ -21,7 +21,7 @@ EXPORTS
|
||||
; From chrome_elf/crash_helper.cc
|
||||
SetMetricsClientId
|
||||
|
||||
; From chrome_elf_main.cc
|
||||
; From chrome_elf/chrome_elf_main.cc
|
||||
DumpProcessWithoutCrash
|
||||
GetUserDataDirectoryThunk
|
||||
SignalChromeElf
|
||||
@ -34,3 +34,7 @@ EXPORTS
|
||||
AddDllToBlacklist
|
||||
IsBlacklistInitialized
|
||||
SuccessfullyBlocked
|
||||
|
||||
; From chrome_elf/whitelist_log
|
||||
DrainLog
|
||||
RegisterLogNotification
|
||||
|
@ -5,7 +5,7 @@ LIBRARY "chrome_elf.dll"
|
||||
|
||||
EXPORTS
|
||||
; When functions are added to this file, they must also be added to
|
||||
; chrome_elf_x86.def
|
||||
; chrome_elf_x64.def
|
||||
|
||||
; From components/crash/content/app/crash_export_stubs.cc
|
||||
CrashForException_ExportThunk
|
||||
@ -17,7 +17,7 @@ EXPORTS
|
||||
; From chrome_elf/crash_helper.cc
|
||||
SetMetricsClientId
|
||||
|
||||
; From chrome_elf_main.cc
|
||||
; From chrome_elf/chrome_elf_main.cc
|
||||
DumpProcessWithoutCrash
|
||||
GetUserDataDirectoryThunk
|
||||
SignalChromeElf
|
||||
@ -30,3 +30,7 @@ EXPORTS
|
||||
AddDllToBlacklist
|
||||
IsBlacklistInitialized
|
||||
SuccessfullyBlocked
|
||||
|
||||
; From chrome_elf/whitelist_log
|
||||
DrainLog
|
||||
RegisterLogNotification
|
||||
|
@ -206,6 +206,19 @@ void SHA1HashBytes(const unsigned char* data, size_t len, unsigned char* hash) {
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
int CompareHashes(const uint8_t* first, const uint8_t* second) {
|
||||
// Compare bytes, high-order to low-order.
|
||||
for (size_t i = 0; i < kSHA1Length; ++i) {
|
||||
if (first[i] < second[i])
|
||||
return -1;
|
||||
if (first[i] > second[i])
|
||||
return 1;
|
||||
// else they are equal, continue;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string SHA1HashString(const std::string& str) {
|
||||
char hash[kSHA1Length] = {};
|
||||
SHA1HashBytes(reinterpret_cast<const unsigned char*>(str.c_str()),
|
||||
|
@ -18,6 +18,13 @@ namespace elf_sha1 {
|
||||
// Length in bytes of a SHA-1 hash.
|
||||
constexpr size_t kSHA1Length = 20;
|
||||
|
||||
// Compare two hashes.
|
||||
// - Returns -1 if |first| < |second|
|
||||
// - Returns 0 if |first| == |second|
|
||||
// - Returns 1 if |first| > |second|
|
||||
// Note: Arguments should be kSHA1Length long.
|
||||
int CompareHashes(const uint8_t* first, const uint8_t* second);
|
||||
|
||||
// Computes the SHA1 hash of the input string |str| and returns the full
|
||||
// hash. The returned SHA1 will be 20 bytes in length.
|
||||
std::string SHA1HashString(const std::string& str);
|
||||
|
@ -7,7 +7,9 @@
|
||||
#include <assert.h>
|
||||
|
||||
#include "chrome_elf/nt_registry/nt_registry.h"
|
||||
#include "chrome_elf/whitelist/whitelist_file.h"
|
||||
#include "chrome_elf/whitelist/whitelist_ime.h"
|
||||
#include "chrome_elf/whitelist/whitelist_log.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@ -27,11 +29,13 @@ bool Init() {
|
||||
// Debug check: Init should not be called more than once.
|
||||
assert(!g_whitelist_initialized);
|
||||
|
||||
// TODO(pennymac): As sources are added, consider multi-threaded init.
|
||||
// Source: Input Method Editors (IMEs)
|
||||
IMEStatus rc = InitIMEs();
|
||||
if (rc != IMEStatus::kSuccess)
|
||||
// TODO(pennymac): As work is added, consider multi-threaded init.
|
||||
// TODO(pennymac): Handle return status codes for UMA.
|
||||
if (InitIMEs() != IMEStatus::kSuccess ||
|
||||
InitFromFile() != FileStatus::kSuccess ||
|
||||
InitLogs() != LogStatus::kSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Record initialization.
|
||||
g_whitelist_initialized = true;
|
||||
|
@ -38,27 +38,11 @@ std::wstring& GetBlFilePath() {
|
||||
// Private functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Returns -1 if |first| < |second|
|
||||
// Returns 0 if |first| == |second|
|
||||
// Returns 1 if |first| > |second|
|
||||
int CompareHashes(const uint8_t* first, const uint8_t* second) {
|
||||
// Compare bytes, high-order to low-order.
|
||||
for (size_t i = 0; i < elf_sha1::kSHA1Length; ++i) {
|
||||
if (first[i] < second[i])
|
||||
return -1;
|
||||
if (first[i] > second[i])
|
||||
return 1;
|
||||
// else they are equal, continue;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Binary predicate compare function for use with
|
||||
// std::equal_range/std::is_sorted. Must return TRUE if lhs < rhs.
|
||||
bool HashBinaryPredicate(const PackedWhitelistModule& lhs,
|
||||
const PackedWhitelistModule& rhs) {
|
||||
return CompareHashes(lhs.basename_hash, rhs.basename_hash) < 0;
|
||||
return elf_sha1::CompareHashes(lhs.basename_hash, rhs.basename_hash) < 0;
|
||||
}
|
||||
|
||||
// Given a file opened for read, pull in the packed list.
|
||||
@ -206,7 +190,7 @@ bool IsModuleListed(const std::string& basename,
|
||||
|
||||
// Search for secondary hash.
|
||||
for (PackedWhitelistModule* i = pair.first; i != pair.second; ++i) {
|
||||
if (!CompareHashes(target.code_id_hash, i->code_id_hash))
|
||||
if (!elf_sha1::CompareHashes(target.code_id_hash, i->code_id_hash))
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -236,4 +220,16 @@ void OverrideFilePathForTesting(const std::wstring& new_bl_path) {
|
||||
GetBlFilePath().assign(new_bl_path);
|
||||
}
|
||||
|
||||
void DeinitFromFileForTesting() {
|
||||
if (!g_initialized)
|
||||
return;
|
||||
|
||||
delete[] g_bl_module_array;
|
||||
g_bl_module_array = nullptr;
|
||||
g_bl_module_array_size = 0;
|
||||
GetBlFilePath().clear();
|
||||
|
||||
g_initialized = false;
|
||||
}
|
||||
|
||||
} // namespace whitelist
|
||||
|
@ -42,6 +42,9 @@ FileStatus InitFromFile();
|
||||
// Overrides the blacklist path for use by tests.
|
||||
void OverrideFilePathForTesting(const std::wstring& new_bl_path);
|
||||
|
||||
// Removes initialization for use by tests.
|
||||
void DeinitFromFileForTesting();
|
||||
|
||||
} // namespace whitelist
|
||||
|
||||
#endif // CHROME_ELF_WHITELIST_WHITELIST_FILE_H_
|
||||
|
@ -109,6 +109,8 @@ class WhitelistFileTest : public testing::Test {
|
||||
OverrideFilePathForTesting(bl_test_file_path_);
|
||||
}
|
||||
|
||||
void TearDown() override { DeinitFromFileForTesting(); }
|
||||
|
||||
void CreateTestFile() {
|
||||
base::File file(base::FilePath(bl_test_file_path_),
|
||||
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE |
|
||||
|
@ -283,4 +283,8 @@ IMEStatus InitIMEs() {
|
||||
return IMEStatus::kSuccess;
|
||||
}
|
||||
|
||||
void DeinitIMEsForTesting() {
|
||||
GetImeVector()->clear();
|
||||
}
|
||||
|
||||
} // namespace whitelist
|
||||
|
@ -34,6 +34,9 @@ extern const wchar_t kImeRegistryKey[];
|
||||
// Initialize internal list of registered IMEs.
|
||||
IMEStatus InitIMEs();
|
||||
|
||||
// Removes initialization for use by tests.
|
||||
void DeinitIMEsForTesting();
|
||||
|
||||
} // namespace whitelist
|
||||
|
||||
#endif // CHROME_ELF_WHITELIST_WHITELIST_IME_H_
|
||||
|
@ -103,6 +103,7 @@ bool AddClassDllPath(const wchar_t* new_guid, const std::wstring& dll_path) {
|
||||
TEST(Whitelist, IMEInitExisting) {
|
||||
// Init IME!
|
||||
EXPECT_EQ(InitIMEs(), IMEStatus::kSuccess);
|
||||
DeinitIMEsForTesting();
|
||||
}
|
||||
|
||||
// Test initialization with a custom IME to exercise all of the code path for
|
||||
@ -134,6 +135,8 @@ TEST(Whitelist, IMEInitNonMs) {
|
||||
|
||||
// 5. Disable reg override.
|
||||
ASSERT_NO_FATAL_FAILURE(CancelRegRedirect(nt::HKLM));
|
||||
|
||||
DeinitIMEsForTesting();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
258
chrome_elf/whitelist/whitelist_log.cc
Normal file
258
chrome_elf/whitelist/whitelist_log.cc
Normal file
@ -0,0 +1,258 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "chrome_elf/whitelist/whitelist_log.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "chrome_elf/sha1/sha1.h"
|
||||
|
||||
namespace whitelist {
|
||||
namespace {
|
||||
|
||||
enum { kMaxLogEntries = 100, kMaxMutexWaitMs = 5000 };
|
||||
|
||||
// Mutex for log access;
|
||||
HANDLE g_log_mutex = nullptr;
|
||||
|
||||
// An event handle that can be registered by outside modules via
|
||||
// RegisterLogNotification().
|
||||
HANDLE g_notification_event = nullptr;
|
||||
|
||||
// This structure will be translated into LogEntry when draining log.
|
||||
struct LogEntryInternal {
|
||||
uint8_t basename_hash[elf_sha1::kSHA1Length];
|
||||
uint8_t code_id_hash[elf_sha1::kSHA1Length];
|
||||
std::string full_path;
|
||||
};
|
||||
|
||||
// Converts a given LogEntryInternal into a LogEntry.
|
||||
void TranslateEntry(LogType log_type,
|
||||
const LogEntryInternal& src,
|
||||
LogEntry* dst) {
|
||||
dst->type = log_type;
|
||||
::memcpy(dst->basename_hash, src.basename_hash, elf_sha1::kSHA1Length);
|
||||
::memcpy(dst->code_id_hash, src.code_id_hash, elf_sha1::kSHA1Length);
|
||||
|
||||
// Sanity check - there should be no LogEntryInternal with a too long path.
|
||||
// LogLoadAttempt() ensures this.
|
||||
assert(src.full_path.size() < std::numeric_limits<uint32_t>::max());
|
||||
dst->path_len = static_cast<uint32_t>(src.full_path.size());
|
||||
::memcpy(dst->path, src.full_path.c_str(), dst->path_len + 1);
|
||||
}
|
||||
|
||||
// Class wrapper for internal logging events of module load attempts.
|
||||
// - A Log instance will either track 'block' events, or 'allow' events.
|
||||
class Log {
|
||||
public:
|
||||
// Move constructor
|
||||
Log(Log&&) noexcept = default;
|
||||
// Move assignment
|
||||
Log& operator=(Log&&) noexcept = default;
|
||||
// Constructor - |log_type| indicates what LogType this instance holds.
|
||||
explicit Log(LogType log_type) : log_type_(log_type) {}
|
||||
|
||||
// Returns the size in bytes of the full log, in terms of LogEntry structs.
|
||||
// I.e. how many bytes would a provided buffer need to be to DrainLog().
|
||||
uint32_t GetFullLogSize() const {
|
||||
uint32_t size = 0;
|
||||
for (auto entry : entries_) {
|
||||
size += GetLogEntrySize(static_cast<uint32_t>(entry.full_path.size()));
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// Add a LogEntryInternal to the log. Take ownership of the argument.
|
||||
void AddEntry(LogEntryInternal&& entry) {
|
||||
// Sanity checks. If load blocked, do not add duplicate logs.
|
||||
if (entries_.size() == kMaxLogEntries ||
|
||||
(log_type_ == LogType::kBlocked &&
|
||||
ContainsEntry(entry.basename_hash, entry.code_id_hash))) {
|
||||
return;
|
||||
}
|
||||
entries_.push_back(std::move(entry));
|
||||
|
||||
// Fire the global notification event - if any is registered.
|
||||
if (g_notification_event)
|
||||
::SetEvent(g_notification_event);
|
||||
}
|
||||
|
||||
// Writes entries from the start of this Log into |buffer| until either all
|
||||
// entries have been written or until no more will fit within |buffer_size|.
|
||||
// - Emitted entries are removed from the log.
|
||||
// - The number of bytes_written is returned.
|
||||
uint32_t Drain(uint8_t* buffer, uint32_t buffer_size) {
|
||||
uint32_t remaining_buffer_size = buffer_size;
|
||||
uint32_t pop_count = 0;
|
||||
uint32_t entry_size = 0;
|
||||
uint32_t bytes_written = 0;
|
||||
// Drain as many entries as possible.
|
||||
for (auto entry : entries_) {
|
||||
entry_size =
|
||||
GetLogEntrySize(static_cast<uint32_t>(entry.full_path.size()));
|
||||
if (remaining_buffer_size < entry_size)
|
||||
break;
|
||||
LogEntry* temp = reinterpret_cast<LogEntry*>(buffer + bytes_written);
|
||||
TranslateEntry(log_type_, entry, temp);
|
||||
// Update counters.
|
||||
remaining_buffer_size -= entry_size;
|
||||
bytes_written += entry_size;
|
||||
++pop_count;
|
||||
}
|
||||
DequeueEntries(pop_count);
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
private:
|
||||
// Logs are currently unordered, so just loop.
|
||||
// - Returns true if the given hashes already exist in the log.
|
||||
bool ContainsEntry(const uint8_t* basename_hash,
|
||||
const uint8_t* code_id_hash) const {
|
||||
for (auto entry : entries_) {
|
||||
if (!elf_sha1::CompareHashes(basename_hash, entry.basename_hash) &&
|
||||
!elf_sha1::CompareHashes(code_id_hash, entry.code_id_hash)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove |count| entries from start of log vector.
|
||||
// - More efficient to take a chunk off the vector once, instead of one entry
|
||||
// at a time.
|
||||
void DequeueEntries(uint32_t count) {
|
||||
assert(count <= entries_.size());
|
||||
entries_.erase(entries_.begin(), entries_.begin() + count);
|
||||
}
|
||||
|
||||
LogType log_type_;
|
||||
std::vector<LogEntryInternal> entries_;
|
||||
|
||||
// DISALLOW_COPY_AND_ASSIGN(Log);
|
||||
Log(const Log&) = delete;
|
||||
Log& operator=(const Log&) = delete;
|
||||
};
|
||||
|
||||
// NOTE: these "globals" are only initialized once during InitLogs().
|
||||
// NOTE: they are wrapped in functions to prevent exit-time dtors.
|
||||
// *These returned Log instances must only be accessed under g_log_mutex.
|
||||
Log& GetBlockedLog() {
|
||||
static Log* const blocked_log = new Log(LogType::kBlocked);
|
||||
return *blocked_log;
|
||||
}
|
||||
|
||||
Log& GetAllowedLog() {
|
||||
static Log* const allowed_log = new Log(LogType::kAllowed);
|
||||
return *allowed_log;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public defines & functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// This is called from inside a hook shim, so don't bother with return status.
|
||||
void LogLoadAttempt(LogType log_type,
|
||||
const uint8_t* basename_hash,
|
||||
const uint8_t* code_id_hash,
|
||||
const char* full_image_path) {
|
||||
if (::WaitForSingleObject(g_log_mutex, kMaxMutexWaitMs) != WAIT_OBJECT_0)
|
||||
return;
|
||||
|
||||
// Build the new log entry.
|
||||
LogEntryInternal entry;
|
||||
::memcpy(&entry.basename_hash[0], basename_hash, elf_sha1::kSHA1Length);
|
||||
::memcpy(&entry.code_id_hash[0], code_id_hash, elf_sha1::kSHA1Length);
|
||||
|
||||
// Only store full path if the module was allowed to load.
|
||||
if (log_type == LogType::kAllowed) {
|
||||
entry.full_path = full_image_path;
|
||||
// Edge condition. Ensure the path length is <= max(uint32_t) - 1.
|
||||
if (entry.full_path.size() > std::numeric_limits<uint32_t>::max() - 1)
|
||||
entry.full_path.resize(std::numeric_limits<uint32_t>::max() - 1);
|
||||
}
|
||||
|
||||
// Add the new entry.
|
||||
Log& log =
|
||||
(log_type == LogType::kBlocked ? GetBlockedLog() : GetAllowedLog());
|
||||
log.AddEntry(std::move(entry));
|
||||
|
||||
::ReleaseMutex(g_log_mutex);
|
||||
}
|
||||
|
||||
LogStatus InitLogs() {
|
||||
// Debug check: InitLogs should not be called more than once.
|
||||
assert(!g_log_mutex);
|
||||
|
||||
// Create unnamed mutex for log access.
|
||||
g_log_mutex = ::CreateMutex(nullptr, false, nullptr);
|
||||
if (!g_log_mutex)
|
||||
return LogStatus::kCreateMutexFailure;
|
||||
|
||||
return LogStatus::kSuccess;
|
||||
}
|
||||
|
||||
void DeinitLogsForTesting() {
|
||||
if (g_log_mutex)
|
||||
::CloseHandle(g_log_mutex);
|
||||
g_log_mutex = nullptr;
|
||||
}
|
||||
|
||||
} // namespace whitelist
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Exports
|
||||
// - Function definition in whitelist_packed_format.h
|
||||
// - Export declared in chrome_elf_[x64|x86].def
|
||||
//------------------------------------------------------------------------------
|
||||
using namespace whitelist;
|
||||
|
||||
uint32_t DrainLog(uint8_t* buffer,
|
||||
uint32_t buffer_size,
|
||||
uint32_t* log_remaining) {
|
||||
if (::WaitForSingleObject(g_log_mutex, kMaxMutexWaitMs) != WAIT_OBJECT_0)
|
||||
return 0;
|
||||
|
||||
Log& blocked = GetBlockedLog();
|
||||
Log& allowed = GetAllowedLog();
|
||||
|
||||
uint32_t bytes_written = blocked.Drain(buffer, buffer_size);
|
||||
bytes_written +=
|
||||
allowed.Drain(buffer + bytes_written, buffer_size - bytes_written);
|
||||
|
||||
// If requested, return the remaining logs size.
|
||||
if (log_remaining) {
|
||||
// Edge case: maximum 32-bit value.
|
||||
uint64_t full_size = blocked.GetFullLogSize() + allowed.GetFullLogSize();
|
||||
if (full_size > std::numeric_limits<uint32_t>::max())
|
||||
*log_remaining = std::numeric_limits<uint32_t>::max();
|
||||
else
|
||||
*log_remaining = static_cast<uint32_t>(full_size);
|
||||
}
|
||||
|
||||
::ReleaseMutex(g_log_mutex);
|
||||
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
bool RegisterLogNotification(HANDLE event_handle) {
|
||||
HANDLE temp = nullptr;
|
||||
if (event_handle && !::DuplicateHandle(::GetCurrentProcess(), event_handle,
|
||||
::GetCurrentProcess(), &temp, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_notification_event)
|
||||
::CloseHandle(g_notification_event);
|
||||
|
||||
g_notification_event = temp;
|
||||
|
||||
return true;
|
||||
}
|
41
chrome_elf/whitelist/whitelist_log.h
Normal file
41
chrome_elf/whitelist/whitelist_log.h
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef CHROME_ELF_WHITELIST_WHITELIST_LOG_H_
|
||||
#define CHROME_ELF_WHITELIST_WHITELIST_LOG_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "chrome_elf/whitelist/whitelist_packed_format.h"
|
||||
|
||||
namespace whitelist {
|
||||
|
||||
// "static_cast<int>(LogStatus::value)" to access underlying value.
|
||||
enum class LogStatus { kSuccess = 0, kCreateMutexFailure = 1, COUNT };
|
||||
|
||||
// Adds a load attempt to the internal load log.
|
||||
// - |log_type| indicates the type of logging.
|
||||
// - |basename_hash| and |code_id_hash| must each point to a 20-byte buffer
|
||||
// holding a SHA-1 digest (of the module's basename and code identifier,
|
||||
// respectively).
|
||||
// - For loads that are allowed, |full_image_path| indicates the full path of
|
||||
// the loaded image.
|
||||
void LogLoadAttempt(LogType log_type,
|
||||
const uint8_t* basename_hash,
|
||||
const uint8_t* code_id_hash,
|
||||
const char* full_image_path);
|
||||
|
||||
// Initialize internal logs.
|
||||
LogStatus InitLogs();
|
||||
|
||||
// Removes initialization for use by tests.
|
||||
void DeinitLogsForTesting();
|
||||
|
||||
} // namespace whitelist
|
||||
|
||||
#endif // CHROME_ELF_WHITELIST_WHITELIST_LOG_H_
|
218
chrome_elf/whitelist/whitelist_log_unittest.cc
Normal file
218
chrome_elf/whitelist/whitelist_log_unittest.cc
Normal file
@ -0,0 +1,218 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "chrome_elf/whitelist/whitelist_log.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "base/synchronization/waitable_event.h"
|
||||
#include "base/time/time.h"
|
||||
#include "chrome_elf/sha1/sha1.h"
|
||||
#include "chrome_elf/whitelist/whitelist_packed_format.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
namespace whitelist {
|
||||
namespace {
|
||||
|
||||
enum { kWaitTimeoutMs = 3000 };
|
||||
|
||||
// Argument for NotificationHandler().
|
||||
struct NotificationHandlerArguments {
|
||||
uint32_t logs_expected;
|
||||
std::unique_ptr<base::WaitableEvent> notification_event;
|
||||
};
|
||||
|
||||
struct TestEntry {
|
||||
uint8_t basename_hash[elf_sha1::kSHA1Length];
|
||||
uint8_t code_id_hash[elf_sha1::kSHA1Length];
|
||||
};
|
||||
|
||||
// Sample TestEntries - hashes are the same, except for first bytes.
|
||||
TestEntry kTestLogs[] = {
|
||||
{{0x11, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71},
|
||||
{0x22, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71}},
|
||||
{{0x33, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71},
|
||||
{0x44, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71}},
|
||||
{{0x55, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71},
|
||||
{0x66, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71}},
|
||||
{{0x77, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71},
|
||||
{0x88, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71}},
|
||||
{{0x99, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71},
|
||||
{0xaa, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71}},
|
||||
{{0xbb, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71},
|
||||
{0xcc, 0xab, 0x9e, 0xa4, 0xbe, 0xf5, 0xf3, 0x6e, 0x7f, 0x20,
|
||||
0xc3, 0xaf, 0x63, 0x9c, 0x6f, 0x0e, 0xfe, 0x5f, 0x27, 0x71}},
|
||||
};
|
||||
|
||||
// Be sure to test the padding/alignment issues well here.
|
||||
const std::string kTestPaths[] = {
|
||||
"1", "123", "1234", "12345", "123456", "1234567",
|
||||
};
|
||||
|
||||
static_assert(
|
||||
arraysize(kTestLogs) == arraysize(kTestPaths),
|
||||
"Some tests currently expect these two arrays to be the same size.");
|
||||
|
||||
// Ensure |buffer_size| passed in is the actual bytes written by DrainLog().
|
||||
void VerifyBuffer(uint8_t* buffer, uint32_t buffer_size) {
|
||||
uint32_t total_logs = 0;
|
||||
size_t index = 0;
|
||||
size_t array_size = arraysize(kTestLogs);
|
||||
|
||||
// Verify against kTestLogs/kTestPaths. Expect 2 * arraysize(kTestLogs)
|
||||
// entries: first half are "blocked", second half are "allowed".
|
||||
LogEntry* entry = nullptr;
|
||||
uint8_t* tracker = buffer;
|
||||
while (tracker < buffer + buffer_size) {
|
||||
entry = reinterpret_cast<LogEntry*>(tracker);
|
||||
|
||||
EXPECT_EQ(elf_sha1::CompareHashes(entry->basename_hash,
|
||||
kTestLogs[index].basename_hash),
|
||||
0);
|
||||
EXPECT_EQ(elf_sha1::CompareHashes(entry->code_id_hash,
|
||||
kTestLogs[index].code_id_hash),
|
||||
0);
|
||||
if (entry->path_len)
|
||||
EXPECT_STREQ(entry->path, kTestPaths[index].c_str());
|
||||
|
||||
++total_logs;
|
||||
tracker += GetLogEntrySize(entry->path_len);
|
||||
|
||||
// Roll index back to 0 for second run through kTestLogs, else increment.
|
||||
index = (index == array_size - 1) ? 0 : index + 1;
|
||||
}
|
||||
EXPECT_EQ(total_logs, array_size * 2);
|
||||
}
|
||||
|
||||
// Helper function to count the number of LogEntries in a buffer returned from
|
||||
// DrainLog().
|
||||
uint32_t GetLogCount(uint8_t* buffer, uint32_t bytes_written) {
|
||||
LogEntry* entry = nullptr;
|
||||
uint8_t* tracker = buffer;
|
||||
uint32_t counter = 0;
|
||||
|
||||
while (tracker < buffer + bytes_written) {
|
||||
entry = reinterpret_cast<LogEntry*>(tracker);
|
||||
++counter;
|
||||
tracker += GetLogEntrySize(entry->path_len);
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
// A thread function used to test log notifications.
|
||||
// - |parameter| should be a NotificationHandlerArguments*.
|
||||
// - Returns 0 for successful retrieval of expected number of LogEntries.
|
||||
DWORD WINAPI NotificationHandler(LPVOID parameter) {
|
||||
NotificationHandlerArguments* args =
|
||||
reinterpret_cast<NotificationHandlerArguments*>(parameter);
|
||||
uint32_t log_counter = 0;
|
||||
|
||||
// Make a buffer big enough for any possible DrainLog call.
|
||||
uint32_t buffer_size = args->logs_expected * GetLogEntrySize(0);
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[buffer_size]);
|
||||
uint32_t bytes_written = 0;
|
||||
|
||||
do {
|
||||
if (!args->notification_event->TimedWait(
|
||||
base::TimeDelta::FromMilliseconds(kWaitTimeoutMs)))
|
||||
break;
|
||||
|
||||
bytes_written = DrainLog(&buffer[0], buffer_size, nullptr);
|
||||
log_counter += GetLogCount(&buffer[0], bytes_written);
|
||||
} while (log_counter < args->logs_expected);
|
||||
|
||||
return (log_counter == args->logs_expected) ? 0 : 1;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Whitelist log tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Test successful initialization and module lookup.
|
||||
TEST(Whitelist, Logs) {
|
||||
// Init.
|
||||
ASSERT_EQ(InitLogs(), LogStatus::kSuccess);
|
||||
|
||||
for (size_t i = 0; i < arraysize(kTestLogs); ++i) {
|
||||
// Add some blocked entries.
|
||||
LogLoadAttempt(LogType::kBlocked, kTestLogs[i].basename_hash,
|
||||
kTestLogs[i].code_id_hash, nullptr);
|
||||
|
||||
// Add some allowed entries.
|
||||
LogLoadAttempt(LogType::kAllowed, kTestLogs[i].basename_hash,
|
||||
kTestLogs[i].code_id_hash, kTestPaths[i].c_str());
|
||||
}
|
||||
|
||||
uint32_t initial_log = 0;
|
||||
DrainLog(nullptr, 0, &initial_log);
|
||||
ASSERT_TRUE(initial_log);
|
||||
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[initial_log]);
|
||||
uint32_t remaining_log = 0;
|
||||
uint32_t bytes_written = DrainLog(&buffer[0], initial_log, &remaining_log);
|
||||
EXPECT_EQ(bytes_written, initial_log);
|
||||
EXPECT_EQ(remaining_log, uint32_t{0});
|
||||
|
||||
VerifyBuffer(&buffer[0], bytes_written);
|
||||
|
||||
DeinitLogsForTesting();
|
||||
}
|
||||
|
||||
// Test notifications.
|
||||
TEST(Whitelist, LogNotifications) {
|
||||
// Init.
|
||||
ASSERT_EQ(InitLogs(), LogStatus::kSuccess);
|
||||
|
||||
uint32_t initial_log = 0;
|
||||
DrainLog(nullptr, 0, &initial_log);
|
||||
EXPECT_EQ(initial_log, uint32_t{0});
|
||||
|
||||
// Set up the required arguments for the test thread.
|
||||
NotificationHandlerArguments handler_data;
|
||||
handler_data.logs_expected = arraysize(kTestLogs);
|
||||
handler_data.notification_event.reset(
|
||||
new base::WaitableEvent(base::WaitableEvent::ResetPolicy::AUTOMATIC,
|
||||
base::WaitableEvent::InitialState::NOT_SIGNALED));
|
||||
|
||||
// Register the event.
|
||||
ASSERT_TRUE(
|
||||
RegisterLogNotification(handler_data.notification_event->handle()));
|
||||
|
||||
// Fire off a thread to handle the notifications.
|
||||
base::win::ScopedHandle thread(::CreateThread(
|
||||
nullptr, 0, &NotificationHandler, &handler_data, 0, nullptr));
|
||||
|
||||
for (size_t i = 0; i < handler_data.logs_expected; ++i) {
|
||||
// Add blocked entries - type doesn't matter in this test.
|
||||
LogLoadAttempt(LogType::kBlocked, kTestLogs[i].basename_hash,
|
||||
kTestLogs[i].code_id_hash, nullptr);
|
||||
}
|
||||
|
||||
EXPECT_EQ(::WaitForSingleObject(thread.Get(), kWaitTimeoutMs * 2),
|
||||
WAIT_OBJECT_0);
|
||||
DWORD exit_code = 1;
|
||||
EXPECT_TRUE(::GetExitCodeThread(thread.Get(), &exit_code));
|
||||
EXPECT_EQ(exit_code, DWORD{0});
|
||||
|
||||
DeinitLogsForTesting();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace whitelist
|
@ -4,6 +4,8 @@
|
||||
|
||||
#include "chrome_elf/whitelist/whitelist_packed_format.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
namespace whitelist {
|
||||
|
||||
// Subdir relative to install_static::GetUserDataDirectory().
|
||||
@ -18,4 +20,11 @@ const wchar_t kFileSubdir[] =
|
||||
// Packed module data cache file.
|
||||
const wchar_t kBlFileName[] = L"\\bldata";
|
||||
|
||||
uint32_t GetLogEntrySize(uint32_t path_len) {
|
||||
// Include padding in the size to fill it to a 32-bit boundary (add one byte
|
||||
// for the string terminator and up to three bytes of padding, which will be
|
||||
// truncated down to the proper amount).
|
||||
return ((offsetof(LogEntry, path) + path_len + 4) & ~3U);
|
||||
}
|
||||
|
||||
} // namespace whitelist
|
||||
|
@ -2,6 +2,21 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This source set defines shared expectations for the module load
|
||||
// whitelist/blacklist (WL/BL).
|
||||
// 1. The BL packed data file format used to pass information from chrome.dll
|
||||
// to chrome_elf.dll across restarts.
|
||||
// 2. The APIs exported by chrome_elf.dll to share logs of module load attempts.
|
||||
|
||||
#ifndef CHROME_ELF_WHITELIST_WHITELIST_PACKED_FORMAT_H_
|
||||
#define CHROME_ELF_WHITELIST_WHITELIST_PACKED_FORMAT_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace whitelist {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// This defines the expected contents of a packed whitelist file.
|
||||
// - At offset 0 of the file: {PackedWhitelistMetadata}
|
||||
@ -14,13 +29,6 @@
|
||||
// second by code_id hash (there can be multiple of the same basename hash).
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#ifndef CHROME_ELF_WHITELIST_WHITELIST_PACKED_FORMAT_H_
|
||||
#define CHROME_ELF_WHITELIST_WHITELIST_PACKED_FORMAT_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace whitelist {
|
||||
|
||||
// Subdir relative to install_static::GetUserDataDirectory().
|
||||
extern const wchar_t kFileSubdir[];
|
||||
|
||||
@ -63,6 +71,70 @@ static_assert(sizeof(PackedWhitelistModule) == 44,
|
||||
"The actual padding of the PackedWhitelistModule struct doesn't "
|
||||
"match the expected padding");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// chrome_elf log API
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Load-attempt log types.
|
||||
enum LogType : uint8_t {
|
||||
kBlocked,
|
||||
kAllowed,
|
||||
};
|
||||
|
||||
// Define a flat log entry for any attempted module load.
|
||||
// The total size in bytes of a log entry is returned by GetLogEntrySize().
|
||||
// - Note: If this is a |blocked| entry, |path_len| will be 0.
|
||||
// (Full path not required for a blacklisted load attempt log.)
|
||||
struct LogEntry {
|
||||
LogType type;
|
||||
uint8_t basename_hash[20];
|
||||
uint8_t code_id_hash[20];
|
||||
// Number of characters in |path| string, not including null terminator.
|
||||
uint32_t path_len;
|
||||
// UTF-8 full module path, null termination guaranteed.
|
||||
char path[1];
|
||||
};
|
||||
|
||||
static_assert(sizeof(LogEntry) == 52,
|
||||
"Ensure expectations for padding and alignment are correct. "
|
||||
"If this changes, double check GetLogEntrySize() calculation.");
|
||||
|
||||
// Returns the full size for a LogEntry, given the LogEntry.path_len.
|
||||
// - Always use this function over manual calculation, as it handles padding
|
||||
// and alignment.
|
||||
// - This function will be built into the caller binary.
|
||||
// - Example of how to use this function to iterate through a buffer returned
|
||||
// from DrainLog():
|
||||
//
|
||||
// uint8_t* tracker = buffer;
|
||||
// while (tracker < buffer + buffer_bytes_written) {
|
||||
// LogEntry* entry = reinterpret_cast<LogEntry*>(tracker);
|
||||
// // Do work.
|
||||
// tracker += GetLogEntrySize(entry->path_len);
|
||||
// }
|
||||
uint32_t GetLogEntrySize(uint32_t path_len);
|
||||
|
||||
} // namespace whitelist
|
||||
|
||||
// Exported API for calling from outside chrome_elf.dll.
|
||||
// Drains the load attempt LogEntries into the provided buffer.
|
||||
// - Returns the number of bytes written. See comments above for LogEntry
|
||||
// details.
|
||||
// - If provided, |log_remaining| receives the number of bytes remaining in the
|
||||
// module log, that didn't fit in |buffer|.
|
||||
// - |buffer_size| can be 0, in which case this simply queries the size of the
|
||||
// module log.
|
||||
extern "C" uint32_t DrainLog(uint8_t* buffer,
|
||||
uint32_t buffer_size,
|
||||
uint32_t* log_remaining);
|
||||
|
||||
// Exported API for calling from outside chrome_elf.dll.
|
||||
// Register an event to be notified when whitelist logs are available via
|
||||
// DrainLog API.
|
||||
// - Pass in a HANDLE to an event created via ::CreateEvent(), or nullptr to
|
||||
// clear.
|
||||
// - This function will duplicate |event_handle|, and call ::SetEvent() when any
|
||||
// new whitelist log is added.
|
||||
extern "C" bool RegisterLogNotification(HANDLE event_handle);
|
||||
|
||||
#endif // CHROME_ELF_WHITELIST_WHITELIST_PACKED_FORMAT_H_
|
||||
|
Reference in New Issue
Block a user