// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/elevation_service/caller_validation.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/win/scoped_process_information.h"
#include "base/win/startup_information.h"
#include "chrome/elevation_service/elevator.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace elevation_service {
namespace {
// Starts a suspended process that's located at `path`.
base::Process StartSuspendedFakeProcess(const base::FilePath& path) {
PROCESS_INFORMATION temp_process_info = {};
if (!base::PathExists(path)) {
base::CreateDirectory(path.DirName());
// Doesn't matter what the executable is, as long as it's an executable.
base::CopyFile(base::PathService::CheckedGet(base::FILE_EXE), path);
}
base::win::StartupInformation startup_info;
std::wstring writable_cmd_line = path.value();
if (::CreateProcess(nullptr, writable_cmd_line.data(), nullptr, nullptr,
/*bInheritHandles=*/FALSE, CREATE_SUSPENDED, nullptr,
nullptr, startup_info.startup_info(),
&temp_process_info)) {
base::win::ScopedProcessInformation process_info(temp_process_info);
return base::Process(process_info.TakeProcessHandle());
}
return base::Process();
}
void VerifyValidationResult(const base::FilePath& path1,
const base::FilePath& path2,
bool expected_match) {
auto process1 = StartSuspendedFakeProcess(path1);
ASSERT_TRUE(process1.IsRunning());
auto process2 = StartSuspendedFakeProcess(path2);
ASSERT_TRUE(process2.IsRunning());
const auto data = GenerateValidationData(
ProtectionLevel::PROTECTION_PATH_VALIDATION, process1);
ASSERT_TRUE(data.has_value()) << data.error();
EXPECT_EQ(expected_match, SUCCEEDED(ValidateData(process2, *data)))
<< path1 << " vs. " << path2;
process1.Terminate(0, /*wait=*/true);
process2.Terminate(0, /*wait=*/true);
}
} // namespace
class CallerValidationTest : public ::testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::ScopedTempDir temp_dir_;
};
TEST_F(CallerValidationTest, NoneValidationTest) {
const auto my_process = base::Process::Current();
const auto data =
GenerateValidationData(ProtectionLevel::PROTECTION_NONE, my_process);
ASSERT_TRUE(data.has_value()) << data.error();
ASSERT_HRESULT_SUCCEEDED(ValidateData(my_process, *data));
}
TEST_F(CallerValidationTest, PathValidationTest) {
const auto my_process = base::Process::Current();
const auto data = GenerateValidationData(
ProtectionLevel::PROTECTION_PATH_VALIDATION, my_process);
ASSERT_TRUE(data.has_value()) << data.error();
ASSERT_HRESULT_SUCCEEDED(ValidateData(my_process, *data));
}
TEST_F(CallerValidationTest, PathValidationOldDataTest) {
// Test old format validation data.
const std::vector<uint8_t> data = {'P', 'A', 'T', 'H'};
const auto result = ValidateData(base::Process::Current(), data);
ASSERT_HRESULT_FAILED(result);
ASSERT_EQ(result, E_INVALIDARG);
}
TEST_F(CallerValidationTest, DeprecatedPathValidationTest) {
const auto data =
GenerateValidationData(ProtectionLevel::PROTECTION_PATH_VALIDATION_OLD,
base::Process::Current());
ASSERT_FALSE(data.has_value());
EXPECT_EQ(data.error(), Elevator::kErrorUnsupportedProtectionLevel);
}
TEST_F(CallerValidationTest, BackwardsCompatiblePathDataTest) {
auto data = GenerateValidationData(
ProtectionLevel::PROTECTION_PATH_VALIDATION, base::Process::Current());
ASSERT_TRUE(data.has_value());
ASSERT_EQ((*data)[0], ProtectionLevel::PROTECTION_PATH_VALIDATION);
// Simulate a client that has previously generated path validation data but
// with the old validation type (0x01). This is compatible with the new data
// type (0x02).
(*data)[0] = ProtectionLevel::PROTECTION_PATH_VALIDATION_OLD;
const auto result = ValidateData(base::Process::Current(), *data);
ASSERT_HRESULT_SUCCEEDED(result);
}
TEST_F(CallerValidationTest, PathValidationTestFail) {
const auto my_process = base::Process::Current();
const auto data = GenerateValidationData(
ProtectionLevel::PROTECTION_PATH_VALIDATION, my_process);
ASSERT_TRUE(data.has_value()) << data.error();
auto notepad_process =
base::LaunchProcess(L"calc.exe", base::LaunchOptions());
ASSERT_TRUE(notepad_process.IsRunning());
const HRESULT res = ValidateData(notepad_process, *data);
ASSERT_HRESULT_FAILED(res);
ASSERT_EQ(res, Elevator::kValidationDidNotPass);
ASSERT_TRUE(notepad_process.Terminate(0, true));
}
TEST_F(CallerValidationTest, PathValidationTestOtherProcess) {
base::expected<std::vector<uint8_t>, HRESULT> data;
// Start two separate notepad processes to validate that path validation only
// cares about the process path and not the process itself.
{
auto notepad_process =
base::LaunchProcess(L"calc.exe", base::LaunchOptions());
ASSERT_TRUE(notepad_process.IsRunning());
data = GenerateValidationData(ProtectionLevel::PROTECTION_PATH_VALIDATION,
notepad_process);
ASSERT_TRUE(notepad_process.Terminate(0, true));
}
ASSERT_TRUE(data.has_value()) << data.error();
{
auto notepad_process =
base::LaunchProcess(L"calc.exe", base::LaunchOptions());
ASSERT_TRUE(notepad_process.IsRunning());
ASSERT_HRESULT_SUCCEEDED(ValidateData(notepad_process, *data));
ASSERT_TRUE(notepad_process.Terminate(0, true));
}
}
TEST_F(CallerValidationTest, NoneValidationTestOtherProcess) {
const auto my_process = base::Process::Current();
const auto data =
GenerateValidationData(ProtectionLevel::PROTECTION_NONE, my_process);
ASSERT_TRUE(data.has_value()) << data.error();
auto notepad_process =
base::LaunchProcess(L"calc.exe", base::LaunchOptions());
ASSERT_TRUE(notepad_process.IsRunning());
// None validation should not care if the process is different.
ASSERT_HRESULT_SUCCEEDED(ValidateData(notepad_process, *data));
ASSERT_TRUE(notepad_process.Terminate(0, true));
}
// tempdir
// |__ app1.exe
// |
// |__ Application
// | |__ app2.exe
// | |__ app3.exe
// | |__ Temp
// | | |__ app7.exe
// | |
// | |__ 1.2.3.4
// | |__ app10.exe
// |
// |__ Temp
// | |__ app4.exe
// |
// |__ Blah
// | |__ app5.exe
// | |__ app6.exe
// |
// |__ Program Files
// | |__ app8.exe
// |
// |__ Program Files (x86)
// | |__ app9.exe
TEST_F(CallerValidationTest, PathValidationFuzzyPathMatch) {
// Build the paths.
// the temp dir must not end with the 'scoped_dir' dir or else it will be
// removed by the trim path function, so place the test binaries into a subdir
// of the temp dir.
const auto temp_dir = temp_dir_.GetPath().AppendASCII("testdir");
const auto app1_path = temp_dir.AppendASCII("app1.exe");
const auto app2_path =
temp_dir.AppendASCII("Application").AppendASCII("app2.exe");
const auto app3_path =
temp_dir.AppendASCII("Application").AppendASCII("app3.exe");
const auto app4_path = temp_dir.AppendASCII("Temp").AppendASCII("app4.exe");
const auto app5_path = temp_dir.AppendASCII("Blah").AppendASCII("app5.exe");
const auto app6_path = temp_dir.AppendASCII("Blah").AppendASCII("app6.exe");
const auto app7_path = temp_dir.AppendASCII("Application")
.AppendASCII("Temp")
.AppendASCII("app7.exe");
const auto app8_path =
temp_dir.AppendASCII("Program Files").AppendASCII("app8.exe");
const auto app9_path =
temp_dir.AppendASCII("Program Files (x86)").AppendASCII("app9.exe");
const auto app10_path = temp_dir.AppendASCII("Application")
.AppendASCII("1.2.3.4")
.AppendASCII("app10.exe");
// Should ignore 'Temp' and 'Application' for matches.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app1_path, app2_path, /*expected_match=*/true));
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app1_path, app3_path, /*expected_match=*/true));
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app1_path, app4_path, /*expected_match=*/true));
// Invalid subdir 'Blah'.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app1_path, app5_path, /*expected_match=*/false));
// Case for rename of chrome.exe to new_chrome.exe during install.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app2_path, app3_path, /*expected_match=*/true));
// 'Temp' and 'Application' should both normalize to each other.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app2_path, app4_path, /*expected_match=*/true));
// Invalid subdir 'Blah'.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app2_path, app5_path, /*expected_match=*/false));
// Case for temp path of chrome exe during install.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app4_path, app2_path, /*expected_match=*/true));
// Verify app in unusual directory still validates correctly.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app5_path, app6_path, /*expected_match=*/true));
// Verify Temp/Application should only be removed once and not multiple times.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app7_path, app3_path, /*expected_match=*/false));
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app7_path, app1_path, /*expected_match=*/false));
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app8_path, app9_path, /*expected_match=*/true));
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app1_path, app8_path, /*expected_match=*/false));
// Verify app in version dir normalizes to the parent directory.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app2_path, app10_path, /*expected_match=*/true));
// Verify app in version dir does not match an unrelated directory.
ASSERT_NO_FATAL_FAILURE(
VerifyValidationResult(app5_path, app10_path, /*expected_match=*/false));
}
// To run this locally, copy the elevation_service_unittests binary to a
// network drive (e.g. X:) and run it using:
// X:\elevation_service_unittests.exe
// --gtest_filter=CallerValidationTest.PathValidationNetwork
// --gtest_also_run_disabled_tests.
TEST_F(CallerValidationTest, DISABLED_PathValidationNetwork) {
const auto data = GenerateValidationData(
ProtectionLevel::PROTECTION_PATH_VALIDATION, base::Process::Current());
EXPECT_FALSE(data.has_value());
EXPECT_EQ(data.error(), Elevator::kErrorUnsupportedFilePath);
}
TEST_F(CallerValidationTest, TrimProcessPath) {
struct TestData {
base::FilePath::StringPieceType input;
base::FilePath::StringPieceType expected;
} cases[] = {
{L"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome"},
{L"C:\\Program Files\\Google\\Chrome\\Temp\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome"},
{L"C:\\Program Files (x86)\\Google\\Chrome\\Temp\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome"},
{L"C:\\Program Files (x86)\\Google\\Chrome\\Blah\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome\\Blah"},
{L"C:\\Dir\\app.exe", L"C:\\Dir"},
{L"C:\\Dir\\", L"C:\\Dir"},
{L"C:\\Dir", L"C:\\Dir"},
{L"C:\\Program Files "
L"(x86)\\Google\\Chrome\\Temp\\scoped_dir11452_73964817\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome"},
{L"C:\\Program Files "
L"(x86)\\Google\\Chrome\\scoped_dir11452_73964817\\Temp\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome\\scoped_dir11452_73964817"},
{L"C:\\Program Files (x86)\\Google\\scoped_dir1\\Chrome\\chrome.exe",
L"C:\\Program Files\\Google\\scoped_dir1\\Chrome"},
{L"C:\\Temp\\Program Files "
L"(x86)\\Google\\scoped_dir1\\Chrome\\chrome.exe",
L"C:\\Temp\\Program Files\\Google\\scoped_dir1\\Chrome"},
{L"C:\\scoped_dir1234\\Program Files "
L"(x86)\\Google\\scoped_dir1234\\Chrome\\chrome.exe",
L"C:\\scoped_dir1234\\Program Files\\Google\\scoped_dir1234\\Chrome"},
{L"C:\\Program Files\\Google\\Chrome\\Application\\1.2.3.4\\chrome.exe",
L"C:\\Program Files\\Google\\Chrome"},
};
for (size_t i = 0; i < std::size(cases); ++i) {
base::FilePath input(cases[i].input);
auto output = MaybeTrimProcessPathForTesting(input);
EXPECT_EQ(output.value(), cases[i].expected);
}
}
} // namespace elevation_service