// 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 "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 <mach/vm_param.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