From 9a8faad50f56d748fcd1498c170797c67bb53217 Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Wed, 22 Oct 2025 19:52:19 -0700 Subject: [PATCH] [KernelCache] Set segment permissions based on how XNU initially maps them XNU maps kernel cache segments in with different permissions than the load commands indicate. For instance, `__DATA_CONST` is initially mapped as read-write before later being re-mapped as read-only. Treating it as read-only results in analysis falsely assuming that global variables cannot change. To work around this we maintain a mapping from segment name to initial permissions (i.e., most lax permissions) and favor them over permissions derived from the segment load command. Section semantics are also derived from the segment's permissions when the segment is present in the mapping. The mapping is based on the initial permissions established by `arm_vm_prot_init` within the XNU source. --- view/kernelcache/core/KernelCache.cpp | 2 +- .../core/KernelCacheController.cpp | 7 +- view/kernelcache/core/MachOProcessor.cpp | 5 +- view/kernelcache/core/Utility.cpp | 146 ++++++++++++++++-- view/kernelcache/core/Utility.h | 8 +- 5 files changed, 146 insertions(+), 22 deletions(-) diff --git a/view/kernelcache/core/KernelCache.cpp b/view/kernelcache/core/KernelCache.cpp index 3750e7956c..ef5fd8a53f 100644 --- a/view/kernelcache/core/KernelCache.cpp +++ b/view/kernelcache/core/KernelCache.cpp @@ -77,7 +77,7 @@ bool KernelCache::ProcessEntryImage(Ref bv, const std::string& path, // Associate this region with this image, this makes it easier to identify what image owns this region. sectionRegion.imageStart = image.headerFileAddress; - uint32_t flags = SegmentFlagsFromMachOProtections(segment.initprot, segment.maxprot); + uint32_t flags = SegmentFlagsForSegment(segment); // if we're positive we have an entry point for some reason, force the segment // executable. this helps with kernel images. for (const auto& entryPoint : imageHeader->m_entryPoints) diff --git a/view/kernelcache/core/KernelCacheController.cpp b/view/kernelcache/core/KernelCacheController.cpp index 40d4a1df62..753c28ad06 100644 --- a/view/kernelcache/core/KernelCacheController.cpp +++ b/view/kernelcache/core/KernelCacheController.cpp @@ -124,10 +124,13 @@ bool KernelCacheController::ApplyImage(BinaryView& view, const CacheImage& image loadedRegion = true; for (const auto& segment : image.header->segments) { - auto flags = SegmentFlagsFromMachOProtections(segment.initprot, segment.maxprot); + if (segment.vmsize == 0) + continue; + + auto flags = SegmentFlagsForSegment(segment); view.AddAutoSegment(segment.vmaddr, segment.vmsize, segment.fileoff, segment.filesize, flags); - auto relocations = m_cache.GetRelocations(); + const auto& relocations = m_cache.GetRelocations(); auto begin = std::lower_bound(relocations.begin(), relocations.end(), segment.vmaddr, [](const std::pair& reloc, uint64_t addr) { diff --git a/view/kernelcache/core/MachOProcessor.cpp b/view/kernelcache/core/MachOProcessor.cpp index 42faba246c..fb3d2b2753 100644 --- a/view/kernelcache/core/MachOProcessor.cpp +++ b/view/kernelcache/core/MachOProcessor.cpp @@ -193,8 +193,9 @@ uint64_t KernelCacheMachOProcessor::ApplyHeaderSections(KernelCacheMachOHeader& semantics = ReadWriteDataSectionSemantics; if (strncmp(section.sectname, "__auth_got", sizeof(section.sectname)) == 0) semantics = ReadOnlyDataSectionSemantics; - if (strncmp(section.segname, "__DATA_CONST", sizeof(section.segname)) == 0) - semantics = ReadOnlyDataSectionSemantics; + + if (auto overriddenSemantics = SectionSemanticsForSection(section)) + semantics = static_cast(overriddenSemantics); // Typically a view would add auto sections but those won't persist when loading the BNDB. // if we want to use an auto section here we would need to allow the core to apply auto sections from the database. diff --git a/view/kernelcache/core/Utility.cpp b/view/kernelcache/core/Utility.cpp index 515d62c7dd..d2788fafc0 100644 --- a/view/kernelcache/core/Utility.cpp +++ b/view/kernelcache/core/Utility.cpp @@ -4,22 +4,6 @@ using namespace BinaryNinja; -BNSegmentFlag SegmentFlagsFromMachOProtections(int initProt, int maxProt) -{ - uint32_t flags = 0; - if (initProt & MACHO_VM_PROT_READ) - flags |= SegmentReadable; - if (initProt & MACHO_VM_PROT_WRITE) - flags |= SegmentWritable; - if (initProt & MACHO_VM_PROT_EXECUTE) - flags |= SegmentExecutable; - if (((initProt & MACHO_VM_PROT_WRITE) == 0) && ((maxProt & MACHO_VM_PROT_WRITE) == 0)) - flags |= SegmentDenyWrite; - if (((initProt & MACHO_VM_PROT_EXECUTE) == 0) && ((maxProt & MACHO_VM_PROT_EXECUTE) == 0)) - flags |= SegmentDenyExecute; - return static_cast(flags); -} - int64_t readSLEB128(const uint8_t*& current, const uint8_t* end) { uint8_t cur; @@ -157,3 +141,133 @@ bool IsSameFolder(Ref a, Ref b) return a->GetId() == b->GetId(); return false; } + +namespace { + +// Protection combinations used in XNU. Named to match the conventions in arm_vm_init.c +constexpr uint32_t PROT_RNX = SegmentReadable | SegmentContainsData | SegmentDenyWrite | SegmentDenyExecute; +constexpr uint32_t PROT_ROX = SegmentReadable | SegmentExecutable | SegmentContainsCode | SegmentDenyWrite; +constexpr uint32_t PROT_RWNX = SegmentReadable | SegmentWritable | SegmentContainsData | SegmentDenyExecute; + +struct XNUSegmentProtection { + std::string_view name; + uint32_t protection; +}; + +// Protections taken from arm_vm_prot_init at +// https://github.com/apple-oss-distributions/xnu/blob/xnu-12377.1.9/osfmk/arm64/arm_vm_init.c +constexpr std::array s_initialSegmentProtections = {{ + // Core XNU Kernel Segments + {"__TEXT", PROT_RNX}, + {"__TEXT_EXEC", PROT_ROX}, + {"__DATA_CONST", PROT_RWNX}, + {"__DATA", PROT_RWNX}, + {"__HIB", PROT_RWNX}, + {"__BOOTDATA", PROT_RWNX}, + {"__KLD", PROT_ROX}, + {"__KLDDATA", PROT_RNX}, + {"__LINKEDIT", PROT_RWNX}, + {"__LAST", PROT_ROX}, + {"__LASTDATA_CONST", PROT_RWNX}, + + // Prelinked Kext Segments + {"__PRELINK_TEXT", PROT_RWNX}, + {"__PLK_DATA_CONST", PROT_RWNX}, + {"__PLK_TEXT_EXEC", PROT_ROX}, + {"__PRELINK_DATA", PROT_RWNX}, + {"__PLK_LINKEDIT", PROT_RWNX}, + {"__PRELINK_INFO", PROT_RWNX}, + {"__PLK_LLVM_COV", PROT_RWNX}, + + // PPL (Page Protection Layer) Segments + {"__PPLTEXT", PROT_ROX}, + {"__PPLTRAMP", PROT_ROX}, + {"__PPLDATA_CONST", PROT_RNX}, + {"__PPLDATA", PROT_RWNX}, +}}; + +std::string FormatSegmentFlags(uint32_t flags) +{ + std::string perms; + perms += (flags & SegmentReadable) ? 'R' : '-'; + perms += (flags & SegmentWritable) ? 'W' : '-'; + perms += (flags & SegmentExecutable) ? 'X' : '-'; + + std::string type; + if (flags & SegmentContainsCode) + type = " [CODE]"; + else if (flags & SegmentContainsData) + type = " [DATA]"; + + std::string denies; + if (flags & SegmentDenyWrite) + denies += 'W'; + if (flags & SegmentDenyExecute) + denies += 'X'; + if (!denies.empty()) + denies = fmt::format(" (deny:{})", denies); + + return fmt::format("{}{}{}", perms, type, denies); +} + +// XNU maps certain segments with specific protections regardless of what is in the load command. +uint32_t SegmentFlagsForKnownXNUSegment(std::string_view segmentName) +{ + for (const auto& entry : s_initialSegmentProtections) + { + if (segmentName == entry.name) + return entry.protection; + } + return 0; +} + +uint32_t SegmentFlagsFromMachOProtections(int initProt, int maxProt) +{ + uint32_t flags = 0; + if (initProt & MACHO_VM_PROT_READ) + flags |= SegmentReadable; + if (initProt & MACHO_VM_PROT_WRITE) + flags |= SegmentWritable; + if (initProt & MACHO_VM_PROT_EXECUTE) + flags |= SegmentExecutable; + if ((initProt & MACHO_VM_PROT_WRITE) == 0 && (maxProt & MACHO_VM_PROT_WRITE) == 0) + flags |= SegmentDenyWrite; + if ((initProt & MACHO_VM_PROT_EXECUTE) == 0 && (maxProt & MACHO_VM_PROT_EXECUTE) == 0) + flags |= SegmentDenyExecute; + return static_cast(flags); +} + +} // unnamed namespace + +uint32_t SegmentFlagsForSegment(const segment_command_64& segment) +{ + std::string_view segmentName(segment.segname, std::find(segment.segname, std::end(segment.segname), '\0')); + uint32_t flagsFromLoadCommand = SegmentFlagsFromMachOProtections(segment.initprot, segment.maxprot); + if (uint32_t flagsFromKnownXNUSegment = SegmentFlagsForKnownXNUSegment(segmentName)) + { + constexpr int MASK = ~(SegmentContainsData | SegmentContainsCode); + if ((flagsFromKnownXNUSegment & MASK) != (flagsFromLoadCommand & MASK)) + LogDebugF("Overriding segment protections from load command ({}) with known segment protections {} for segment {} ({:#x} - {:#x})", + FormatSegmentFlags(flagsFromLoadCommand), FormatSegmentFlags(flagsFromKnownXNUSegment), segmentName, + segment.vmaddr, segment.vmaddr + segment.vmsize); + return flagsFromKnownXNUSegment; + } + + return flagsFromLoadCommand; +} + +uint32_t SectionSemanticsForSection(const section_64& section) +{ + std::string_view segmentName(section.segname, std::find(section.segname, std::end(section.segname), '\0')); + int flags = SegmentFlagsForKnownXNUSegment(segmentName); + if (!flags) + return 0; + + if (flags & SegmentExecutable) + return ReadOnlyCodeSectionSemantics; + + if (flags & SegmentWritable) + return ReadWriteDataSectionSemantics; + + return ReadOnlyDataSectionSemantics; +} diff --git a/view/kernelcache/core/Utility.h b/view/kernelcache/core/Utility.h index 5664fd9b5f..6d0bf0a0c3 100644 --- a/view/kernelcache/core/Utility.h +++ b/view/kernelcache/core/Utility.h @@ -25,7 +25,13 @@ inline int CountTrailingZeros(uint64_t value) } #endif -BNSegmentFlag SegmentFlagsFromMachOProtections(int initProt, int maxProt); +namespace BinaryNinja { + struct segment_command_64; + struct section_64; +} + +uint32_t SegmentFlagsForSegment(const BinaryNinja::segment_command_64& segment); +uint32_t SectionSemanticsForSection(const BinaryNinja::section_64& section); int64_t readSLEB128(const uint8_t*& current, const uint8_t* end);