| // Copyright 2015 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/common/safe_browsing/mach_o_image_reader_mac.h" |
| |
| #include <arpa/inet.h> |
| #include <libkern/OSByteOrder.h> |
| #include <mach-o/fat.h> |
| #include <mach-o/loader.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <string.h> |
| #include <uuid/uuid.h> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/memory_mapped_file.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace safe_browsing { |
| namespace { |
| |
| // Definitions from |
| // <http://opensource.apple.com/source/xnu/xnu-2782.1.97/bsd/sys/codesign.h>. |
| |
| enum { |
| CSMAGIC_CODEDIRECTORY = 0xfade0c02, |
| CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, |
| |
| CSSLOT_CODEDIRECTORY = 0, |
| }; |
| |
| struct CodeSigningBlob { |
| uint32_t type; |
| uint32_t offset; |
| }; |
| |
| struct CodeSigningSuperBlob { |
| uint32_t magic; |
| uint32_t length; |
| uint32_t count; |
| CodeSigningBlob index[]; |
| }; |
| |
| struct CodeSigningDirectory { |
| uint32_t magic; |
| uint32_t length; |
| uint32_t version; |
| uint32_t flags; |
| uint32_t hashOffset; |
| uint32_t identOffset; |
| uint32_t nSpecialSlots; |
| uint32_t nCodeSlots; |
| uint32_t codeLimit; |
| uint8_t hashSize; |
| uint8_t hashType; |
| uint8_t spare1; |
| uint8_t pageSize; |
| uint32_t spare2; |
| // Version 0x20100. |
| uint32_t scatterOffset; |
| // Version 0x20200. |
| uint32_t teamOffset; |
| }; |
| |
| class MachOImageReaderTest : public testing::Test { |
| protected: |
| void OpenTestFile(const char* file_name, base::MemoryMappedFile* file) { |
| base::FilePath test_data; |
| ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data)); |
| |
| base::FilePath path = test_data.AppendASCII("safe_browsing") |
| .AppendASCII("mach_o") |
| .AppendASCII(file_name); |
| |
| ASSERT_TRUE(file->Initialize(path)); |
| } |
| |
| // Returns the identity of the signed code data. |
| void GetSigningIdentity(const std::vector<uint8_t>& signature, |
| std::string* identity) { |
| auto* super_blob = |
| reinterpret_cast<const CodeSigningSuperBlob*>(&signature[0]); |
| EXPECT_EQ(CSMAGIC_EMBEDDED_SIGNATURE, ntohl(super_blob->magic)); |
| ASSERT_EQ(CSSLOT_CODEDIRECTORY, ntohl(super_blob->index[0].type)); |
| size_t dir_offset = ntohl(super_blob->index[0].offset); |
| auto* directory = |
| reinterpret_cast<const CodeSigningDirectory*>(&signature[dir_offset]); |
| ASSERT_EQ(CSMAGIC_CODEDIRECTORY, ntohl(directory->magic)); |
| size_t ident_offset = ntohl(directory->identOffset) + dir_offset; |
| *identity = |
| std::string(reinterpret_cast<const char*>(&signature[ident_offset])); |
| } |
| |
| // Returns the hash of the code data itself. Note that this is not the |
| // CDHash, but is instead the hash in the CodeDirectory blob, which is |
| // over the contents of the signed data. This is visible as hash #0 |
| // when using `codesign -d -vvvvvv`. |
| void GetCodeSignatureHash(const std::vector<uint8_t>& signature, |
| std::vector<uint8_t>* hash) { |
| auto* super_blob = |
| reinterpret_cast<const CodeSigningSuperBlob*>(&signature[0]); |
| EXPECT_EQ(CSMAGIC_EMBEDDED_SIGNATURE, ntohl(super_blob->magic)); |
| ASSERT_EQ(CSSLOT_CODEDIRECTORY, ntohl(super_blob->index[0].type)); |
| size_t dir_offset = ntohl(super_blob->index[0].offset); |
| auto* directory = |
| reinterpret_cast<const CodeSigningDirectory*>(&signature[dir_offset]); |
| ASSERT_EQ(CSMAGIC_CODEDIRECTORY, ntohl(directory->magic)); |
| size_t hash_offset = ntohl(directory->hashOffset) + dir_offset; |
| std::vector<uint8_t> actual_hash(&signature[hash_offset], |
| &signature[hash_offset + directory->hashSize]); |
| EXPECT_EQ(20u, actual_hash.size()); |
| *hash = actual_hash; |
| } |
| |
| void ExpectCodeSignatureHash(const std::vector<uint8_t>& signature, |
| const char* expected) { |
| std::vector<uint8_t> actual_hash; |
| GetCodeSignatureHash(signature, &actual_hash); |
| |
| std::vector<uint8_t> expected_hash; |
| ASSERT_TRUE(base::HexStringToBytes(expected, &expected_hash)); |
| EXPECT_EQ(expected_hash, actual_hash); |
| } |
| }; |
| |
| TEST_F(MachOImageReaderTest, Executable32) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("executable32", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_FALSE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), reader.GetFileType()); |
| EXPECT_EQ(15u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| |
| // Test an arbitrary load command. |
| auto commands = reader.GetLoadCommands(); |
| ASSERT_EQ(15u, commands.size()); |
| auto command = commands[11]; |
| ASSERT_EQ(static_cast<uint32_t>(LC_LOAD_DYLIB), command.cmd()); |
| auto* actual = command.as_command<dylib_command>(); |
| EXPECT_EQ(2u, actual->dylib.timestamp); |
| EXPECT_EQ(0x4ad0101u, actual->dylib.current_version); |
| EXPECT_EQ(0x10000u, actual->dylib.compatibility_version); |
| } |
| |
| TEST_F(MachOImageReaderTest, Executable64) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("executable64", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_TRUE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_TRUE(reader.GetMachHeader64()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), reader.GetFileType()); |
| EXPECT_EQ(15u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| } |
| |
| TEST_F(MachOImageReaderTest, ExecutableFat) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("executablefat", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_TRUE(reader.IsFat()); |
| auto images = reader.GetFatImages(); |
| ASSERT_EQ(2u, images.size()); |
| |
| // Note: this image is crafted to have 32-bit first. |
| { |
| EXPECT_FALSE(images[0]->IsFat()); |
| EXPECT_FALSE(images[0]->Is64Bit()); |
| EXPECT_TRUE(images[0]->GetMachHeader()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), images[0]->GetFileType()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(images[0]->GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| } |
| |
| { |
| EXPECT_FALSE(images[1]->IsFat()); |
| EXPECT_TRUE(images[1]->Is64Bit()); |
| EXPECT_TRUE(images[1]->GetMachHeader()); |
| EXPECT_TRUE(images[1]->GetMachHeader64()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), images[1]->GetFileType()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(images[1]->GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| |
| // Test an arbitrary load command. |
| auto commands = images[1]->GetLoadCommands(); |
| ASSERT_EQ(15u, commands.size()); |
| auto command = commands[1]; |
| ASSERT_EQ(static_cast<uint32_t>(LC_SEGMENT_64), command.cmd()); |
| auto* actual = command.as_command<segment_command_64>(); |
| EXPECT_EQ("__TEXT", std::string(actual->segname)); |
| EXPECT_EQ(0u, actual->fileoff); |
| EXPECT_EQ(4096u, actual->filesize); |
| EXPECT_EQ(0x7, actual->maxprot); |
| EXPECT_EQ(0x5, actual->initprot); |
| EXPECT_EQ(3u, actual->nsects); |
| EXPECT_EQ(0u, actual->flags); |
| } |
| } |
| |
| TEST_F(MachOImageReaderTest, ExecutablePPC) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("executableppc", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_FALSE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_EQ(OSSwapInt32(MH_EXECUTE), reader.GetFileType()); |
| EXPECT_EQ(10u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| } |
| |
| TEST_F(MachOImageReaderTest, Dylib32) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("lib32.dylib", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_FALSE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_DYLIB), reader.GetFileType()); |
| EXPECT_EQ(13u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| } |
| |
| TEST_F(MachOImageReaderTest, Dylib64) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("lib64.dylib", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_TRUE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_TRUE(reader.GetMachHeader64()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_DYLIB), reader.GetFileType()); |
| EXPECT_EQ(13u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| |
| // Test an arbitrary load command. |
| auto commands = reader.GetLoadCommands(); |
| ASSERT_EQ(13u, commands.size()); |
| auto command = commands[6]; |
| ASSERT_EQ(static_cast<uint32_t>(LC_UUID), command.cmd()); |
| uuid_t expected = {0xB6, 0xB5, 0x12, 0xD7, |
| 0x64, 0xE9, |
| 0x3F, 0x7A, |
| 0xAB, 0x4A, |
| 0x87, 0x46, 0x36, 0x76, 0x87, 0x47}; |
| EXPECT_EQ(0, uuid_compare(expected, |
| command.as_command<uuid_command>()->uuid)); |
| } |
| |
| TEST_F(MachOImageReaderTest, DylibFat) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("libfat.dylib", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_TRUE(reader.IsFat()); |
| auto images = reader.GetFatImages(); |
| ASSERT_EQ(2u, images.size()); |
| |
| // Note: this image is crafted to have 64-bit first. |
| { |
| EXPECT_FALSE(images[0]->IsFat()); |
| EXPECT_TRUE(images[0]->Is64Bit()); |
| EXPECT_TRUE(images[0]->GetMachHeader()); |
| EXPECT_TRUE(images[0]->GetMachHeader64()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_DYLIB), images[0]->GetFileType()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(images[0]->GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| } |
| |
| { |
| EXPECT_FALSE(images[1]->IsFat()); |
| EXPECT_FALSE(images[1]->Is64Bit()); |
| EXPECT_TRUE(images[1]->GetMachHeader()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_DYLIB), images[1]->GetFileType()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_FALSE(images[1]->GetCodeSignatureInfo(&signature)); |
| EXPECT_TRUE(signature.empty()); |
| } |
| } |
| |
| TEST_F(MachOImageReaderTest, SignedExecutable32) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("signedexecutable32", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_FALSE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), reader.GetFileType()); |
| EXPECT_EQ(16u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_TRUE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_EQ(9344u, signature.size()); |
| |
| std::string identity; |
| GetSigningIdentity(signature, &identity); |
| EXPECT_EQ("signedexecutable32", identity); |
| |
| ExpectCodeSignatureHash(signature, |
| "11fb88eb63c10dfc3d24a2545ea2a9c50c2921b5"); |
| } |
| |
| TEST_F(MachOImageReaderTest, SignedExecutableFat) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("signedexecutablefat", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_TRUE(reader.IsFat()); |
| auto images = reader.GetFatImages(); |
| ASSERT_EQ(2u, images.size()); |
| |
| // Note: this image is crafted to have 32-bit first. |
| { |
| EXPECT_FALSE(images[0]->IsFat()); |
| EXPECT_FALSE(images[0]->Is64Bit()); |
| EXPECT_TRUE(images[0]->GetMachHeader()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), images[0]->GetFileType()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_TRUE(images[0]->GetCodeSignatureInfo(&signature)); |
| EXPECT_EQ(9344u, signature.size()); |
| |
| std::string identity; |
| GetSigningIdentity(signature, &identity); |
| EXPECT_EQ("signedexecutablefat", identity); |
| |
| ExpectCodeSignatureHash(signature, |
| "11fb88eb63c10dfc3d24a2545ea2a9c50c2921b5"); |
| } |
| |
| { |
| EXPECT_FALSE(images[1]->IsFat()); |
| EXPECT_TRUE(images[1]->Is64Bit()); |
| EXPECT_TRUE(images[1]->GetMachHeader()); |
| EXPECT_TRUE(images[1]->GetMachHeader64()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_EXECUTE), images[1]->GetFileType()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_TRUE(images[1]->GetCodeSignatureInfo(&signature)); |
| EXPECT_EQ(9344u, signature.size()); |
| |
| std::string identity; |
| GetSigningIdentity(signature, &identity); |
| EXPECT_EQ("signedexecutablefat", identity); |
| |
| ExpectCodeSignatureHash(signature, |
| "750a57326ba85857371094900475defd837f5e14"); |
| } |
| } |
| |
| TEST_F(MachOImageReaderTest, SignedDylib64) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("libsigned64.dylib", &file)); |
| MachOImageReader reader; |
| ASSERT_TRUE(reader.Initialize(file.data(), file.length())); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_TRUE(reader.Is64Bit()); |
| EXPECT_TRUE(reader.GetMachHeader()); |
| EXPECT_TRUE(reader.GetMachHeader64()); |
| EXPECT_EQ(static_cast<uint32_t>(MH_DYLIB), reader.GetFileType()); |
| EXPECT_EQ(14u, reader.GetLoadCommands().size()); |
| |
| std::vector<uint8_t> signature; |
| EXPECT_TRUE(reader.GetCodeSignatureInfo(&signature)); |
| EXPECT_EQ(9328u, signature.size()); |
| |
| std::string identity; |
| GetSigningIdentity(signature, &identity); |
| EXPECT_EQ("libsigned64", identity); |
| |
| ExpectCodeSignatureHash(signature, |
| "8b1c79b60bb53a7f17b5618d5feb10dc8b88d806"); |
| } |
| |
| TEST_F(MachOImageReaderTest, NotMachO) { |
| base::MemoryMappedFile file; |
| ASSERT_NO_FATAL_FAILURE(OpenTestFile("src.c", &file)); |
| MachOImageReader reader; |
| EXPECT_FALSE(reader.Initialize(file.data(), file.length())); |
| } |
| |
| TEST_F(MachOImageReaderTest, IsMachOMagicValue) { |
| static const uint32_t kMagics[] = { MH_MAGIC, MH_MAGIC, FAT_MAGIC }; |
| for (uint32_t magic : kMagics) { |
| SCOPED_TRACE(base::StringPrintf("0x%x", magic)); |
| EXPECT_TRUE(MachOImageReader::IsMachOMagicValue(magic)); |
| EXPECT_TRUE(MachOImageReader::IsMachOMagicValue(OSSwapInt32(magic))); |
| } |
| } |
| |
| // https://crbug.com/524044 |
| TEST_F(MachOImageReaderTest, CmdsizeSmallerThanLoadCommand) { |
| #pragma pack(push, 1) |
| struct TestImage { |
| mach_header_64 header; |
| segment_command_64 page_zero; |
| load_command small_sized; |
| segment_command_64 fake_code; |
| }; |
| #pragma pack(pop) |
| |
| TestImage test_image = {}; |
| |
| test_image.header.magic = MH_MAGIC_64; |
| test_image.header.cputype = CPU_TYPE_X86_64; |
| test_image.header.filetype = MH_EXECUTE; |
| test_image.header.ncmds = 3; |
| test_image.header.sizeofcmds = sizeof(test_image) - sizeof(test_image.header); |
| |
| test_image.page_zero.cmd = LC_SEGMENT; |
| test_image.page_zero.cmdsize = sizeof(test_image.page_zero); |
| strcpy(test_image.page_zero.segname, SEG_PAGEZERO); |
| test_image.page_zero.vmsize = PAGE_SIZE; |
| |
| test_image.small_sized.cmd = LC_SYMSEG; |
| test_image.small_sized.cmdsize = sizeof(test_image.small_sized) - 3; |
| |
| test_image.fake_code.cmd = LC_SEGMENT; |
| test_image.fake_code.cmdsize = sizeof(test_image.fake_code); |
| strcpy(test_image.fake_code.segname, SEG_TEXT); |
| |
| MachOImageReader reader; |
| EXPECT_TRUE(reader.Initialize(reinterpret_cast<const uint8_t*>(&test_image), |
| sizeof(test_image))); |
| |
| EXPECT_FALSE(reader.IsFat()); |
| EXPECT_TRUE(reader.Is64Bit()); |
| |
| const auto& load_commands = reader.GetLoadCommands(); |
| EXPECT_EQ(3u, load_commands.size()); |
| |
| EXPECT_EQ(static_cast<uint32_t>(LC_SEGMENT), load_commands[0].cmd()); |
| EXPECT_EQ(static_cast<uint32_t>(LC_SYMSEG), load_commands[1].cmd()); |
| EXPECT_EQ(sizeof(load_command) - 3, load_commands[1].cmdsize()); |
| EXPECT_EQ(static_cast<uint32_t>(LC_SEGMENT), load_commands[2].cmd()); |
| } |
| |
| // https://crbug.com/591194 |
| TEST_F(MachOImageReaderTest, RecurseFatHeader) { |
| #pragma pack(push, 1) |
| struct TestImage { |
| fat_header header; |
| fat_arch arch1; |
| fat_arch arch2; |
| mach_header_64 macho64; |
| mach_header macho; |
| }; |
| #pragma pack(pop) |
| |
| TestImage test_image = {}; |
| test_image.header.magic = FAT_MAGIC; |
| test_image.header.nfat_arch = 2; |
| test_image.arch1.offset = offsetof(TestImage, macho64); |
| test_image.arch1.size = sizeof(mach_header_64); |
| test_image.arch2.offset = 0; // Cannot point back at the fat_header. |
| test_image.arch2.size = sizeof(test_image); |
| |
| test_image.macho64.magic = MH_MAGIC_64; |
| test_image.macho.magic = MH_MAGIC; |
| |
| MachOImageReader reader; |
| EXPECT_FALSE(reader.Initialize(reinterpret_cast<const uint8_t*>(&test_image), |
| sizeof(test_image))); |
| } |
| |
| } // namespace |
| } // namespace safe_browsing |