| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "sandbox/win/src/sandbox_nt_util.h" |
| |
| #include <windows.h> |
| |
| #include <ntstatus.h> |
| #include <winternl.h> |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "base/files/file.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_util.h" |
| #include "base/win/scoped_handle.h" |
| #include "base/win/scoped_process_information.h" |
| #include "sandbox/win/src/nt_internals.h" |
| #include "sandbox/win/src/policy_broker.h" |
| #include "sandbox/win/src/win_utils.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace sandbox { |
| namespace { |
| |
| using ScopedUnicodeString = std::unique_ptr<UNICODE_STRING, NtAllocDeleter>; |
| |
| TEST(SandboxNtUtil, IsSameProcessPseudoHandle) { |
| HANDLE current_process_pseudo = GetCurrentProcess(); |
| EXPECT_TRUE(IsSameProcess(current_process_pseudo)); |
| } |
| |
| TEST(SandboxNtUtil, IsSameProcessNonPseudoHandle) { |
| base::win::ScopedHandle current_process( |
| OpenProcess(PROCESS_QUERY_INFORMATION, false, GetCurrentProcessId())); |
| ASSERT_TRUE(current_process.is_valid()); |
| EXPECT_TRUE(IsSameProcess(current_process.get())); |
| } |
| |
| TEST(SandboxNtUtil, IsSameProcessDifferentProcess) { |
| STARTUPINFO si = {sizeof(si)}; |
| PROCESS_INFORMATION pi = {}; |
| // Calc is preferred over notepad because notepad will fail to launch on |
| // Windows if the store version is not installed. |
| wchar_t command_line[] = L"calc"; |
| ASSERT_TRUE(CreateProcessW(nullptr, command_line, nullptr, nullptr, false, 0, |
| nullptr, nullptr, &si, &pi)); |
| base::win::ScopedProcessInformation process_info(pi); |
| |
| EXPECT_FALSE(IsSameProcess(process_info.process_handle())); |
| EXPECT_TRUE(TerminateProcess(process_info.process_handle(), 0)); |
| } |
| |
| struct VirtualMemDeleter { |
| void operator()(char* p) { ::VirtualFree(p, 0, MEM_RELEASE); } |
| }; |
| |
| typedef std::unique_ptr<char, VirtualMemDeleter> unique_ptr_vmem; |
| |
| #if defined(_WIN64) |
| |
| void AllocateBlock(SIZE_T size, |
| SIZE_T free_size, |
| char** base_address, |
| std::vector<unique_ptr_vmem>* mem_range) { |
| unique_ptr_vmem ptr(static_cast<char*>(::VirtualAlloc( |
| *base_address, size - free_size, MEM_RESERVE, PAGE_READWRITE))); |
| ASSERT_NE(nullptr, ptr.get()); |
| mem_range->push_back(std::move(ptr)); |
| *base_address += size; |
| } |
| |
| #define KIB(x) ((x)*1024ULL) |
| #define MIB(x) (KIB(x) * 1024ULL) |
| #define GIB(x) (MIB(x) * 1024ULL) |
| // Construct a basic memory layout to do the test. We reserve first to get a |
| // base address then reallocate with the following pattern. |
| // |512MiB-64KiB Free|512MiB-128Kib Free|512MiB-256Kib Free|512MiB+512KiB Free| |
| // The purpose of this is leave a couple of free memory regions within a 2GiB |
| // block of reserved memory that we can test the searching allocator. |
| void AllocateTestRange(std::vector<unique_ptr_vmem>* mem_range) { |
| // Ensure we preallocate enough space in the vector to prevent unexpected |
| // allocations. |
| mem_range->reserve(5); |
| SIZE_T total_size = |
| MIB(512) + MIB(512) + MIB(512) + MIB(512) + KIB(512) + KIB(64); |
| unique_ptr_vmem ptr(static_cast<char*>( |
| ::VirtualAlloc(nullptr, total_size, MEM_RESERVE, PAGE_READWRITE))); |
| ASSERT_NE(nullptr, ptr.get()); |
| char* base_address = ptr.get(); |
| char* orig_base = base_address; |
| ptr.reset(); |
| AllocateBlock(MIB(512), KIB(64), &base_address, mem_range); |
| AllocateBlock(MIB(512), KIB(128), &base_address, mem_range); |
| AllocateBlock(MIB(512), KIB(256), &base_address, mem_range); |
| AllocateBlock(MIB(512) + KIB(512), KIB(512), &base_address, mem_range); |
| // Allocate a memory block at end to act as an upper bound. |
| AllocateBlock(KIB(64), 0, &base_address, mem_range); |
| ASSERT_EQ(total_size, static_cast<SIZE_T>(base_address - orig_base)); |
| } |
| |
| // Test we can allocate appropriate blocks. |
| void TestAlignedRange(char* base_address) { |
| unique_ptr_vmem ptr_256k(new (sandbox::NT_PAGE, base_address) char[KIB(256)]); |
| EXPECT_EQ(base_address + GIB(1) + MIB(512) - KIB(256), ptr_256k.get()); |
| unique_ptr_vmem ptr_64k(new (sandbox::NT_PAGE, base_address) char[KIB(64)]); |
| EXPECT_EQ(base_address + MIB(512) - KIB(64), ptr_64k.get()); |
| unique_ptr_vmem ptr_128k(new (sandbox::NT_PAGE, base_address) char[KIB(128)]); |
| EXPECT_EQ(base_address + GIB(1) - KIB(128), ptr_128k.get()); |
| // We will have run out of space here so should also fail. |
| unique_ptr_vmem ptr_64k_noalloc( |
| new (sandbox::NT_PAGE, base_address) char[KIB(64)]); |
| EXPECT_EQ(nullptr, ptr_64k_noalloc.get()); |
| } |
| |
| // Test the 512k block which exists at the end of the maximum allocation |
| // boundary. |
| void Test512kBlock(char* base_address) { |
| // This should fail as it'll just be out of range. |
| unique_ptr_vmem ptr_512k_noalloc( |
| new (sandbox::NT_PAGE, base_address) char[KIB(512)]); |
| EXPECT_EQ(nullptr, ptr_512k_noalloc.get()); |
| // Check that moving base address we can allocate the 512k block. |
| unique_ptr_vmem ptr_512k( |
| new (sandbox::NT_PAGE, base_address + GIB(1)) char[KIB(512)]); |
| EXPECT_EQ(base_address + GIB(2), ptr_512k.get()); |
| // Free pointer first. |
| ptr_512k.reset(); |
| ptr_512k.reset(new (sandbox::NT_PAGE, base_address + GIB(2)) char[KIB(512)]); |
| EXPECT_EQ(base_address + GIB(2), ptr_512k.get()); |
| } |
| |
| // Test we can allocate appropriate blocks even when starting at an unaligned |
| // address. |
| void TestUnalignedRange(char* base_address) { |
| char* unaligned_base = base_address + 123456; |
| unique_ptr_vmem ptr_256k( |
| new (sandbox::NT_PAGE, unaligned_base) char[KIB(256)]); |
| EXPECT_EQ(base_address + GIB(1) + MIB(512) - KIB(256), ptr_256k.get()); |
| unique_ptr_vmem ptr_64k(new (sandbox::NT_PAGE, unaligned_base) char[KIB(64)]); |
| EXPECT_EQ(base_address + MIB(512) - KIB(64), ptr_64k.get()); |
| unique_ptr_vmem ptr_128k( |
| new (sandbox::NT_PAGE, unaligned_base) char[KIB(128)]); |
| EXPECT_EQ(base_address + GIB(1) - KIB(128), ptr_128k.get()); |
| } |
| |
| // Test maximum number of available allocations within the predefined pattern. |
| void TestMaxAllocations(char* base_address) { |
| // There's only 7 64k blocks in the first 2g which we can fill. |
| unique_ptr_vmem ptr_1(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_1.get()); |
| unique_ptr_vmem ptr_2(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_2.get()); |
| unique_ptr_vmem ptr_3(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_3.get()); |
| unique_ptr_vmem ptr_4(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_4.get()); |
| unique_ptr_vmem ptr_5(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_5.get()); |
| unique_ptr_vmem ptr_6(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_6.get()); |
| unique_ptr_vmem ptr_7(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_NE(nullptr, ptr_7.get()); |
| unique_ptr_vmem ptr_8(new (sandbox::NT_PAGE, base_address) char[1]); |
| EXPECT_EQ(nullptr, ptr_8.get()); |
| } |
| |
| // Test extreme allocations we know should fail. |
| void TestExtremes() { |
| unique_ptr_vmem ptr_null(new (sandbox::NT_PAGE, nullptr) char[1]); |
| EXPECT_EQ(nullptr, ptr_null.get()); |
| unique_ptr_vmem ptr_too_large( |
| new (sandbox::NT_PAGE, reinterpret_cast<void*>(0x1000000)) char[GIB(4)]); |
| EXPECT_EQ(nullptr, ptr_too_large.get()); |
| unique_ptr_vmem ptr_overflow( |
| new (sandbox::NT_PAGE, reinterpret_cast<void*>(SIZE_MAX)) char[1]); |
| EXPECT_EQ(nullptr, ptr_overflow.get()); |
| unique_ptr_vmem ptr_invalid(new ( |
| sandbox::NT_PAGE, reinterpret_cast<void*>(SIZE_MAX - 0x1000000)) char[1]); |
| EXPECT_EQ(nullptr, ptr_invalid.get()); |
| } |
| |
| // Test nearest allocator, only do this for 64 bit. We test through the exposed |
| // new operator as we can't call the AllocateNearTo function directly. |
| TEST(SandboxNtUtil, NearestAllocator) { |
| std::vector<unique_ptr_vmem> mem_range; |
| AllocateTestRange(&mem_range); |
| ASSERT_LT(0U, mem_range.size()); |
| char* base_address = static_cast<char*>(mem_range[0].get()); |
| |
| TestAlignedRange(base_address); |
| Test512kBlock(base_address); |
| TestUnalignedRange(base_address); |
| TestMaxAllocations(base_address); |
| TestExtremes(); |
| } |
| |
| #endif // defined(_WIN64) |
| |
| // Test whether function ValidParameter works as expected, that is properly |
| // checks access to the buffer and doesn't modify it in any way. |
| TEST(SandboxNtUtil, ValidParameter) { |
| static constexpr unsigned int buffer_size = 4096; |
| unique_ptr_vmem buffer_guard(static_cast<char*>( |
| ::VirtualAlloc(nullptr, buffer_size, MEM_COMMIT, PAGE_READWRITE))); |
| ASSERT_NE(nullptr, buffer_guard.get()); |
| |
| unsigned char* ptr = reinterpret_cast<unsigned char*>(buffer_guard.get()); |
| |
| // Fill the buffer with some data. |
| for (unsigned int i = 0; i < buffer_size; i++) |
| ptr[i] = (i % 256); |
| |
| // Setup verify function. |
| auto verify_buffer = [&]() { |
| for (unsigned int i = 0; i < buffer_size; i++) { |
| if (ptr[i] != (i % 256)) |
| return false; |
| } |
| |
| return true; |
| }; |
| |
| // Verify that the buffer can be written to and doesn't change. |
| EXPECT_TRUE(ValidParameter(ptr, buffer_size, RequiredAccess::WRITE)); |
| EXPECT_TRUE(verify_buffer()); |
| |
| DWORD old_protection; |
| // Change the protection of buffer to READONLY. |
| EXPECT_TRUE( |
| ::VirtualProtect(ptr, buffer_size, PAGE_READONLY, &old_protection)); |
| |
| // Writting to buffer should fail now. |
| EXPECT_FALSE(ValidParameter(ptr, buffer_size, RequiredAccess::WRITE)); |
| |
| // But reading should be ok. |
| EXPECT_TRUE(ValidParameter(ptr, buffer_size, RequiredAccess::READ)); |
| |
| // One final check that the buffer hasn't been modified. |
| EXPECT_TRUE(verify_buffer()); |
| } |
| |
| TEST(SandboxNtUtil, CopyNameAndAttributes) { |
| OBJECT_ATTRIBUTES object_attributes; |
| InitializeObjectAttributes(&object_attributes, nullptr, 0, nullptr, nullptr); |
| std::unique_ptr<wchar_t, NtAllocDeleter> name; |
| size_t name_len; |
| uint32_t attributes; |
| EXPECT_EQ(STATUS_UNSUCCESSFUL, |
| sandbox::CopyNameAndAttributes(&object_attributes, &name, &name_len, |
| &attributes)); |
| UNICODE_STRING object_name = {}; |
| InitializeObjectAttributes(&object_attributes, &object_name, 0, |
| reinterpret_cast<HANDLE>(0x88), nullptr); |
| EXPECT_EQ(STATUS_UNSUCCESSFUL, |
| sandbox::CopyNameAndAttributes(&object_attributes, &name, &name_len, |
| &attributes)); |
| wchar_t name_buffer[] = {L'A', L'B', L'C', L'D'}; |
| object_name.Length = static_cast<USHORT>(sizeof(name_buffer)); |
| object_name.MaximumLength = object_name.Length; |
| object_name.Buffer = name_buffer; |
| |
| InitializeObjectAttributes(&object_attributes, &object_name, 0, |
| reinterpret_cast<HANDLE>(0x88), nullptr); |
| EXPECT_EQ(STATUS_UNSUCCESSFUL, |
| sandbox::CopyNameAndAttributes(&object_attributes, &name, &name_len, |
| &attributes)); |
| InitializeObjectAttributes(&object_attributes, &object_name, 0x12345678, |
| nullptr, nullptr); |
| ASSERT_EQ(STATUS_SUCCESS, |
| sandbox::CopyNameAndAttributes(&object_attributes, &name, &name_len, |
| &attributes)); |
| EXPECT_EQ(object_attributes.Attributes, attributes); |
| EXPECT_EQ(std::size(name_buffer), name_len); |
| EXPECT_EQ(0, wcsncmp(name.get(), name_buffer, std::size(name_buffer))); |
| EXPECT_EQ(L'\0', name.get()[name_len]); |
| } |
| |
| TEST(SandboxNtUtil, GetNtExports) { |
| const NtExports* exports = GetNtExports(); |
| ASSERT_TRUE(exports); |
| static_assert((sizeof(NtExports) % sizeof(void*)) == 0); |
| // Verify that the structure is fully initialized. |
| for (size_t i = 0; i < sizeof(NtExports) / sizeof(void*); i++) |
| EXPECT_TRUE(reinterpret_cast<void* const*>(exports)[i]); |
| } |
| |
| TEST(SandboxNtUtil, ExtractModuleName) { |
| { |
| UNICODE_STRING module_path = {}; |
| ::RtlInitUnicodeString(&module_path, L"no-path-sep"); |
| ScopedUnicodeString result(ExtractModuleName(&module_path)); |
| EXPECT_TRUE(result); |
| EXPECT_EQ(result->Length, module_path.Length); |
| EXPECT_EQ(std::wstring(module_path.Buffer), std::wstring(result->Buffer)); |
| } |
| { |
| UNICODE_STRING module_path = {}; |
| ::RtlInitUnicodeString(&module_path, L"c:\\has a\\path\\module.dll"); |
| ScopedUnicodeString result(ExtractModuleName(&module_path)); |
| |
| EXPECT_TRUE(result); |
| EXPECT_EQ(result->Length, 10 * sizeof(wchar_t)); |
| EXPECT_EQ(std::wstring(L"module.dll"), std::wstring(result->Buffer)); |
| } |
| { |
| UNICODE_STRING module_path = {}; |
| ::RtlInitUnicodeString(&module_path, L"c:\\only a\\path\\"); |
| ScopedUnicodeString result(ExtractModuleName(&module_path)); |
| |
| EXPECT_FALSE(result); |
| } |
| { |
| UNICODE_STRING module_path = {}; |
| ::RtlInitUnicodeString(&module_path, L"A"); |
| ScopedUnicodeString result(ExtractModuleName(&module_path)); |
| |
| EXPECT_TRUE(result); |
| EXPECT_EQ(result->Length, module_path.Length); |
| EXPECT_EQ(std::wstring(module_path.Buffer), std::wstring(result->Buffer)); |
| } |
| { |
| UNICODE_STRING module_path = {}; |
| ::RtlInitUnicodeString(&module_path, L""); |
| ScopedUnicodeString result(ExtractModuleName(&module_path)); |
| |
| EXPECT_TRUE(result); |
| EXPECT_EQ(result->Length, 0); |
| } |
| } |
| |
| TEST(SandboxNtUtil, GetCurrentClientId) { |
| CLIENT_ID client_id = GetCurrentClientId(); |
| EXPECT_EQ(client_id.UniqueProcess, |
| reinterpret_cast<LPVOID>(::GetCurrentProcessId())); |
| EXPECT_EQ(client_id.UniqueThread, |
| reinterpret_cast<LPVOID>(::GetCurrentThreadId())); |
| } |
| |
| } // namespace |
| } // namespace sandbox |