0

[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:
Penny MacNeil
2018-01-29 22:56:53 +00:00
committed by Commit Bot
parent 17762b3c42
commit 73cde6de3a
17 changed files with 682 additions and 32 deletions

@ -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

@ -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;
}

@ -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_

@ -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_