#include #include #include #include #include #include #include #include #include #include #include #include #include namespace Nid { namespace { using Internal::Read; using Internal::Write; constexpr std::uint32_t kLcSegment64 = 0x19; constexpr std::uint32_t kLcCodeSignature = 0x1d; constexpr std::uint32_t kLcDyldInfo = 0x22; constexpr std::uint32_t kLcDyldInfoOnly = 0x80000022u; constexpr std::uint32_t kLcDyldExportsTrie = 0x80000033u; constexpr std::uint32_t kLcDyldChainedFixups = 0x80000034u; constexpr std::uint32_t kLcLoadDylib = 0x0c; constexpr std::uint32_t kLcLoadWeakDylib = 0x80000018u; constexpr std::uint32_t kLcReexportDylib = 0x8000001fu; constexpr std::uint32_t kLcLazyLoadDylib = 0x20; constexpr std::uint32_t kLcLoadUpwardDylib = 0x80000023u; constexpr std::uint64_t kExportWeakDefinition = 0x04; constexpr std::uint64_t kExportReexport = 0x08; constexpr std::uint64_t kExportStubAndResolver = 0x10; constexpr std::uint32_t kImportFormatPlain = 1; constexpr std::uint32_t kImportFormatAddend = 2; constexpr std::uint32_t kImportFormatAddend64 = 3; constexpr std::uint64_t kPageSize = 0x1000; struct MachHeader64 { std::uint32_t Magic; std::uint32_t CpuType; std::uint32_t CpuSubtype; std::uint32_t FileType; std::uint32_t CommandCount; std::uint32_t CommandBytes; std::uint32_t Flags; std::uint32_t Reserved; }; struct LoadCommand { std::uint32_t Cmd; std::uint32_t Size; }; struct SegmentCommand64 { std::uint32_t Cmd; std::uint32_t Size; char Name[16]; std::uint64_t VmAddress; std::uint64_t VmSize; std::uint64_t FileOffset; std::uint64_t FileSize; std::uint32_t MaxProtection; std::uint32_t InitProtection; std::uint32_t SectionCount; std::uint32_t Flags; }; struct LinkeditDataCommand { std::uint32_t Cmd; std::uint32_t Size; std::uint32_t DataOffset; std::uint32_t DataSize; }; struct DyldInfoCommand { std::uint32_t Cmd; std::uint32_t Size; std::uint32_t RebaseOffset; std::uint32_t RebaseSize; std::uint32_t BindOffset; std::uint32_t BindSize; std::uint32_t WeakBindOffset; std::uint32_t WeakBindSize; std::uint32_t LazyBindOffset; std::uint32_t LazyBindSize; std::uint32_t ExportOffset; std::uint32_t ExportSize; }; struct ChainedFixupsHeader { std::uint32_t Version; std::uint32_t StartsOffset; std::uint32_t ImportsOffset; std::uint32_t SymbolsOffset; std::uint32_t ImportsCount; std::uint32_t ImportsFormat; std::uint32_t SymbolsFormat; }; void RequireRange(const std::vector& binary, std::uint64_t offset, std::uint64_t size) { if (offset > binary.size() || size > binary.size() - offset) throw std::runtime_error("Mach-O range out of bounds"); } std::uint64_t ReadUleb(const std::vector& data, std::size_t& cursor, std::size_t end) { std::uint64_t value = 0; unsigned shift = 0; for (;;) { if (cursor >= end) throw std::runtime_error("truncated uleb128 in export trie"); const auto byte = data[cursor++]; if (shift >= 64 || (shift == 63 && (byte & 0x7e) != 0)) throw std::runtime_error("uleb128 overflow in export trie"); value |= std::uint64_t{byte & 0x7fu} << shift; if ((byte & 0x80) == 0) return value; shift += 7; } } void WriteUleb(std::vector& out, std::uint64_t value) { do { std::uint8_t byte = value & 0x7f; value >>= 7; if (value != 0) byte |= 0x80; out.push_back(byte); } while (value != 0); } std::size_t UlebSize(std::uint64_t value) { std::size_t size = 1; while (value >>= 7) ++size; return size; } std::string ReadTrieString(const std::vector& data, std::size_t& cursor, std::size_t end) { const auto start = cursor; while (cursor < end && data[cursor] != 0) ++cursor; if (cursor >= end) throw std::runtime_error("unterminated string in export trie"); std::string text(reinterpret_cast(data.data() + start), cursor - start); ++cursor; return text; } struct Image { MachHeader64 Header {}; std::vector Commands; }; Image ParseImage(const std::vector& binary) { Image image; image.Header = Read(binary, 0); if (image.Header.Magic != kMachOMagic64) throw std::runtime_error("not a 64-bit little-endian Mach-O image"); RequireRange(binary, sizeof(MachHeader64), image.Header.CommandBytes); std::size_t cursor = sizeof(MachHeader64); const auto end = cursor + image.Header.CommandBytes; for (std::uint32_t index = 0; index < image.Header.CommandCount; ++index) { if (cursor + sizeof(LoadCommand) > end) throw std::runtime_error("load command out of bounds"); const auto command = Read(binary, cursor); if (command.Size < sizeof(LoadCommand) || command.Size % 8 != 0 || command.Size > end - cursor) throw std::runtime_error("invalid load command size"); image.Commands.push_back(cursor); cursor += command.Size; } return image; } struct TrieLocation { std::size_t OffsetField; std::size_t SizeField; }; std::optional FindTrie(const std::vector& binary, const Image& image) { std::optional found; for (const auto offset : image.Commands) { const auto cmd = Read(binary, offset).Cmd; std::optional here; if (cmd == kLcDyldExportsTrie) here = TrieLocation{offset + offsetof(LinkeditDataCommand, DataOffset), offset + offsetof(LinkeditDataCommand, DataSize)}; else if (cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly) { if (Read(binary, offset).ExportSize != 0) here = TrieLocation{offset + offsetof(DyldInfoCommand, ExportOffset), offset + offsetof(DyldInfoCommand, ExportSize)}; } if (!here) continue; if (found) throw std::runtime_error("Mach-O image has more than one export trie"); found = here; } return found; } void ParseTrieNode(const std::vector& data, std::size_t start, std::size_t end, std::size_t node, std::string& prefix, std::set& visited, std::vector& out) { if (node >= end - start) throw std::runtime_error("export trie node out of bounds"); if (!visited.insert(node).second) throw std::runtime_error("export trie has a cycle"); std::size_t cursor = start + node; const auto terminalSize = ReadUleb(data, cursor, end); if (terminalSize > end - cursor) throw std::runtime_error("export trie terminal out of bounds"); const auto children = cursor + terminalSize; if (terminalSize != 0) { MachOExport entry; entry.Name = prefix; entry.Flags = ReadUleb(data, cursor, children); if (entry.Flags & kExportReexport) { entry.Other = ReadUleb(data, cursor, children); entry.ImportName = ReadTrieString(data, cursor, children); } else { entry.Address = ReadUleb(data, cursor, children); if (entry.Flags & kExportStubAndResolver) entry.Other = ReadUleb(data, cursor, children); } out.push_back(std::move(entry)); } cursor = children; if (cursor >= end) throw std::runtime_error("export trie child count out of bounds"); const auto childCount = data[cursor++]; for (unsigned index = 0; index < childCount; ++index) { const auto label = ReadTrieString(data, cursor, end); const auto child = ReadUleb(data, cursor, end); const auto length = prefix.size(); prefix += label; ParseTrieNode(data, start, end, static_cast(child), prefix, visited, out); prefix.resize(length); } } std::vector ParseTrie(const std::vector& binary, std::uint32_t offset, std::uint32_t size) { RequireRange(binary, offset, size); std::vector exports; if (size == 0) return exports; std::string prefix; std::set visited; ParseTrieNode(binary, offset, std::size_t{offset} + size, 0, prefix, visited, exports); return exports; } class TrieBuilder { public: explicit TrieBuilder(const std::vector& exports) { for (const auto& entry : exports) Insert(entry); } std::vector Build() { std::vector order; Collect(&root, order); for (bool changed = true; changed;) { changed = false; std::uint64_t offset = 0; for (auto* node : order) { if (node->Offset != offset) { node->Offset = offset; changed = true; } offset += NodeSize(*node); } } std::vector out; for (auto* node : order) { if (out.size() != node->Offset) throw std::runtime_error("export trie layout mismatch"); const auto terminal = TerminalBytes(*node); WriteUleb(out, terminal.size()); out.insert(out.end(), terminal.begin(), terminal.end()); if (node->Children.size() > 255) throw std::runtime_error("export trie node has too many children"); out.push_back(static_cast(node->Children.size())); for (const auto& [label, child] : node->Children) { out.insert(out.end(), label.begin(), label.end()); out.push_back(0); WriteUleb(out, child->Offset); } } while (out.size() % 8 != 0) out.push_back(0); return out; } private: struct Node { std::vector>> Children; std::optional Entry; std::uint64_t Offset = 0; }; Node root; void Insert(const MachOExport& entry) { Node* node = &root; std::string_view rest = entry.Name; while (!rest.empty()) { bool descended = false; for (auto& [label, child] : node->Children) { std::size_t common = 0; while (common < label.size() && common < rest.size() && label[common] == rest[common]) ++common; if (common == 0) continue; if (common < label.size()) { auto middle = std::make_unique(); middle->Children.emplace_back(label.substr(common), std::move(child)); label.resize(common); child = std::move(middle); } node = child.get(); rest.remove_prefix(common); descended = true; break; } if (!descended) { node->Children.emplace_back(std::string(rest), std::make_unique()); node = node->Children.back().second.get(); rest = {}; } } if (node->Entry) throw std::runtime_error("duplicate patched export name: " + entry.Name); node->Entry = entry; } static void Collect(Node* node, std::vector& order) { order.push_back(node); for (auto& [label, child] : node->Children) Collect(child.get(), order); } static std::vector TerminalBytes(const Node& node) { std::vector bytes; if (!node.Entry) return bytes; const auto& entry = *node.Entry; WriteUleb(bytes, entry.Flags); if (entry.Flags & kExportReexport) { WriteUleb(bytes, entry.Other); bytes.insert(bytes.end(), entry.ImportName.begin(), entry.ImportName.end()); bytes.push_back(0); } else { WriteUleb(bytes, entry.Address); if (entry.Flags & kExportStubAndResolver) WriteUleb(bytes, entry.Other); } return bytes; } static std::uint64_t NodeSize(const Node& node) { const auto terminal = TerminalBytes(node).size(); std::uint64_t size = UlebSize(terminal) + terminal + 1; for (const auto& [label, child] : node.Children) size += label.size() + 1 + UlebSize(child->Offset); return size; } }; std::string StripUnderscore(const std::string& name) { return !name.empty() && name[0] == '_' ? name.substr(1) : name; } bool IsGuestFacingImport(const std::string& name) { using namespace Internal; const bool hasNidPostfix = name.size() >= kNidPostfixLen && name.compare(name.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; const bool hasScePrefix = name.size() >= 3u && std::tolower(static_cast(name[0])) == 's' && std::tolower(static_cast(name[1])) == 'c' && std::tolower(static_cast(name[2])) == 'e'; return hasNidPostfix || hasScePrefix; } std::vector DylibOrdinalsArePrx(const std::vector& binary, const Image& image) { std::vector result; for (const auto offset : image.Commands) { const auto command = Read(binary, offset); if (command.Cmd != kLcLoadDylib && command.Cmd != kLcLoadWeakDylib && command.Cmd != kLcReexportDylib && command.Cmd != kLcLazyLoadDylib && command.Cmd != kLcLoadUpwardDylib) continue; const auto nameOffset = Read(binary, offset + 8); if (nameOffset >= command.Size) throw std::runtime_error("dylib name out of bounds"); const auto name = Internal::ReadCStr(binary, offset + nameOffset); result.push_back(name.size() >= 4 && name.compare(name.size() - 4, 4, ".prx") == 0); } return result; } void PatchChainedImports(std::vector& binary, const Image& image) { std::optional fixups; for (const auto offset : image.Commands) { const auto cmd = Read(binary, offset).Cmd; if (cmd == kLcDyldChainedFixups) fixups = Read(binary, offset); if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) { const auto info = Read(binary, offset); if (info.BindSize != 0 || info.LazyBindSize != 0 || info.WeakBindSize != 0) throw std::runtime_error("Mach-O bind opcodes are not supported; link with chained fixups"); } } if (!fixups || fixups->DataSize == 0) return; RequireRange(binary, fixups->DataOffset, fixups->DataSize); const auto base = std::size_t{fixups->DataOffset}; const auto header = Read(binary, base); if (header.Version != 0) throw std::runtime_error("unsupported chained fixups version"); if (header.SymbolsFormat != 0) throw std::runtime_error("compressed chained fixup symbols are not supported"); const std::size_t entrySize = header.ImportsFormat == kImportFormatPlain ? 4 : header.ImportsFormat == kImportFormatAddend ? 8 : header.ImportsFormat == kImportFormatAddend64 ? 16 : 0; if (entrySize == 0) throw std::runtime_error("unsupported chained import format"); if (header.ImportsOffset > fixups->DataSize || std::uint64_t{header.ImportsCount} * entrySize > fixups->DataSize - header.ImportsOffset || header.SymbolsOffset > fixups->DataSize) throw std::runtime_error("chained imports out of bounds"); const auto symbols = base + header.SymbolsOffset; const auto symbolsEnd = base + fixups->DataSize; const auto prx = DylibOrdinalsArePrx(binary, image); struct Import { std::uint32_t NameOffset; std::uint32_t Ordinal; }; std::vector imports; for (std::uint32_t index = 0; index < header.ImportsCount; ++index) { const auto at = base + header.ImportsOffset + index * entrySize; if (header.ImportsFormat == kImportFormatAddend64) { const auto raw = Read(binary, at); imports.push_back({static_cast(raw >> 32), static_cast(raw & 0xffff)}); } else { const auto raw = Read(binary, at); imports.push_back({raw >> 9, raw & 0xff}); } } std::set nameOffsets; for (const auto& item : imports) nameOffsets.insert(item.NameOffset); std::set rewritten; for (const auto& item : imports) { const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue; if (rewritten.contains(item.NameOffset)) continue; const auto at = symbols + item.NameOffset; if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds"); const auto name = Internal::ReadCStr(binary, at); if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name"); const auto bare = StripUnderscore(name); if (bare == name || !IsGuestFacingImport(bare)) continue; const auto patched = "_" + ResolveOneName(bare); if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name); const auto inside = nameOffsets.upper_bound(item.NameOffset); if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name); std::memcpy(binary.data() + at, patched.data(), patched.size()); std::fill(binary.begin() + static_cast(at + patched.size()), binary.begin() + static_cast(at + name.size() + 1), 0); rewritten.insert(item.NameOffset); } } void WriteTrie(std::vector& binary, const Image& image, const TrieLocation& location, const std::vector& trie) { const auto oldOffset = Read(binary, location.OffsetField); const auto oldSize = Read(binary, location.SizeField); if (trie.size() <= oldSize) { std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset); std::fill(binary.begin() + oldOffset + static_cast(trie.size()), binary.begin() + oldOffset + oldSize, 0); Write(binary, location.SizeField, static_cast(trie.size())); return; } std::optional linkedit; for (const auto offset : image.Commands) { const auto cmd = Read(binary, offset).Cmd; if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow their export trie"); if (cmd != kLcSegment64) continue; const auto segment = Read(binary, offset); if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset; } if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment"); auto segment = Read(binary, *linkedit); if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file"); const auto newOffset = (binary.size() + 7) & ~std::size_t{7}; if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("export trie offset overflow"); binary.resize(newOffset, 0); binary.insert(binary.end(), trie.begin(), trie.end()); std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0); segment.FileSize = binary.size() - segment.FileOffset; segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1)); Write(binary, *linkedit, segment); Write(binary, location.OffsetField, static_cast(newOffset)); Write(binary, location.SizeField, static_cast(trie.size())); } } std::vector ReadMachOExports(const std::vector& binary) { const auto image = ParseImage(binary); const auto location = FindTrie(binary, image); if (!location) return {}; return ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); } void MachONidPatcher::PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const { const auto image = ParseImage(binary); const auto location = FindTrie(binary, image); if (!location) throw std::runtime_error("no export trie"); auto exports = ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); if (exports.empty()) throw std::runtime_error("no exported names"); std::vector names; names.reserve(exports.size()); for (const auto& entry : exports) { if (entry.Name.empty() || entry.Name[0] != '_') throw std::runtime_error("exported name without a C underscore: " + entry.Name); names.push_back(entry.Name.substr(1)); } const auto nidMap = ResolveNids(names, libraryName, excludedExports); for (std::size_t index = 0; index < exports.size(); ++index) { const auto& name = names[index]; const bool marked = name.ends_with(Internal::kNidPostfix) || Internal::IsNidNoPatch(name) || Internal::IsNidNoPatchCut(name); if ((exports[index].Flags & kExportWeakDefinition) != 0 && !marked) continue; const auto found = nidMap.find(name); if (found == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + name); exports[index].Name = "_" + found->second; } WriteTrie(binary, image, *location, TrieBuilder(exports).Build()); PatchChainedImports(binary, ParseImage(binary)); } }