Skip to content

Commit cb6e021

Browse files
committed
[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.
1 parent 321fd67 commit cb6e021

File tree

5 files changed

+146
-22
lines changed

5 files changed

+146
-22
lines changed

view/kernelcache/core/KernelCache.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ bool KernelCache::ProcessEntryImage(Ref<BinaryView> bv, const std::string& path,
7777
// Associate this region with this image, this makes it easier to identify what image owns this region.
7878
sectionRegion.imageStart = image.headerFileAddress;
7979

80-
uint32_t flags = SegmentFlagsFromMachOProtections(segment.initprot, segment.maxprot);
80+
uint32_t flags = SegmentFlagsForSegment(segment);
8181
// if we're positive we have an entry point for some reason, force the segment
8282
// executable. this helps with kernel images.
8383
for (const auto& entryPoint : imageHeader->m_entryPoints)

view/kernelcache/core/KernelCacheController.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,13 @@ bool KernelCacheController::ApplyImage(BinaryView& view, const CacheImage& image
124124
loadedRegion = true;
125125
for (const auto& segment : image.header->segments)
126126
{
127-
auto flags = SegmentFlagsFromMachOProtections(segment.initprot, segment.maxprot);
127+
if (segment.vmsize == 0)
128+
continue;
129+
130+
auto flags = SegmentFlagsForSegment(segment);
128131
view.AddAutoSegment(segment.vmaddr, segment.vmsize, segment.fileoff, segment.filesize, flags);
129132

130-
auto relocations = m_cache.GetRelocations();
133+
const auto& relocations = m_cache.GetRelocations();
131134

132135
auto begin = std::lower_bound(relocations.begin(), relocations.end(), segment.vmaddr,
133136
[](const std::pair<uint64_t, uint64_t>& reloc, uint64_t addr) {

view/kernelcache/core/MachOProcessor.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ uint64_t KernelCacheMachOProcessor::ApplyHeaderSections(KernelCacheMachOHeader&
193193
semantics = ReadWriteDataSectionSemantics;
194194
if (strncmp(section.sectname, "__auth_got", sizeof(section.sectname)) == 0)
195195
semantics = ReadOnlyDataSectionSemantics;
196-
if (strncmp(section.segname, "__DATA_CONST", sizeof(section.segname)) == 0)
197-
semantics = ReadOnlyDataSectionSemantics;
196+
197+
if (auto overriddenSemantics = SectionSemanticsForSection(section))
198+
semantics = static_cast<BNSectionSemantics>(overriddenSemantics);
198199

199200
// Typically a view would add auto sections but those won't persist when loading the BNDB.
200201
// if we want to use an auto section here we would need to allow the core to apply auto sections from the database.

view/kernelcache/core/Utility.cpp

Lines changed: 130 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,6 @@
44

55
using namespace BinaryNinja;
66

7-
BNSegmentFlag SegmentFlagsFromMachOProtections(int initProt, int maxProt)
8-
{
9-
uint32_t flags = 0;
10-
if (initProt & MACHO_VM_PROT_READ)
11-
flags |= SegmentReadable;
12-
if (initProt & MACHO_VM_PROT_WRITE)
13-
flags |= SegmentWritable;
14-
if (initProt & MACHO_VM_PROT_EXECUTE)
15-
flags |= SegmentExecutable;
16-
if (((initProt & MACHO_VM_PROT_WRITE) == 0) && ((maxProt & MACHO_VM_PROT_WRITE) == 0))
17-
flags |= SegmentDenyWrite;
18-
if (((initProt & MACHO_VM_PROT_EXECUTE) == 0) && ((maxProt & MACHO_VM_PROT_EXECUTE) == 0))
19-
flags |= SegmentDenyExecute;
20-
return static_cast<BNSegmentFlag>(flags);
21-
}
22-
237
int64_t readSLEB128(const uint8_t*& current, const uint8_t* end)
248
{
259
uint8_t cur;
@@ -157,3 +141,133 @@ bool IsSameFolder(Ref<ProjectFolder> a, Ref<ProjectFolder> b)
157141
return a->GetId() == b->GetId();
158142
return false;
159143
}
144+
145+
namespace {
146+
147+
// Protection combinations used in XNU. Named to match the conventions in arm_vm_init.c
148+
constexpr uint32_t PROT_RNX = SegmentReadable | SegmentContainsData | SegmentDenyWrite | SegmentDenyExecute;
149+
constexpr uint32_t PROT_ROX = SegmentReadable | SegmentExecutable | SegmentContainsCode | SegmentDenyWrite;
150+
constexpr uint32_t PROT_RWNX = SegmentReadable | SegmentWritable | SegmentContainsData | SegmentDenyExecute;
151+
152+
struct XNUSegmentProtection {
153+
std::string_view name;
154+
uint32_t protection;
155+
};
156+
157+
// Protections taken from arm_vm_prot_init at
158+
// https://github.com/apple-oss-distributions/xnu/blob/xnu-12377.1.9/osfmk/arm64/arm_vm_init.c
159+
constexpr std::array<XNUSegmentProtection, 22> s_initialSegmentProtections = {{
160+
// Core XNU Kernel Segments
161+
{"__TEXT", PROT_RNX},
162+
{"__TEXT_EXEC", PROT_ROX},
163+
{"__DATA_CONST", PROT_RWNX},
164+
{"__DATA", PROT_RWNX},
165+
{"__HIB", PROT_RWNX},
166+
{"__BOOTDATA", PROT_RWNX},
167+
{"__KLD", PROT_ROX},
168+
{"__KLDDATA", PROT_RNX},
169+
{"__LINKEDIT", PROT_RWNX},
170+
{"__LAST", PROT_ROX},
171+
{"__LASTDATA_CONST", PROT_RWNX},
172+
173+
// Prelinked Kext Segments
174+
{"__PRELINK_TEXT", PROT_RWNX},
175+
{"__PLK_DATA_CONST", PROT_RWNX},
176+
{"__PLK_TEXT_EXEC", PROT_ROX},
177+
{"__PRELINK_DATA", PROT_RWNX},
178+
{"__PLK_LINKEDIT", PROT_RWNX},
179+
{"__PRELINK_INFO", PROT_RWNX},
180+
{"__PLK_LLVM_COV", PROT_RWNX},
181+
182+
// PPL (Page Protection Layer) Segments
183+
{"__PPLTEXT", PROT_ROX},
184+
{"__PPLTRAMP", PROT_ROX},
185+
{"__PPLDATA_CONST", PROT_RNX},
186+
{"__PPLDATA", PROT_RWNX},
187+
}};
188+
189+
std::string FormatSegmentFlags(uint32_t flags)
190+
{
191+
std::string perms;
192+
perms += (flags & SegmentReadable) ? 'R' : '-';
193+
perms += (flags & SegmentWritable) ? 'W' : '-';
194+
perms += (flags & SegmentExecutable) ? 'X' : '-';
195+
196+
std::string type;
197+
if (flags & SegmentContainsCode)
198+
type = " [CODE]";
199+
else if (flags & SegmentContainsData)
200+
type = " [DATA]";
201+
202+
std::string denies;
203+
if (flags & SegmentDenyWrite)
204+
denies += 'W';
205+
if (flags & SegmentDenyExecute)
206+
denies += 'X';
207+
if (!denies.empty())
208+
denies = fmt::format(" (deny:{})", denies);
209+
210+
return fmt::format("{}{}{}", perms, type, denies);
211+
}
212+
213+
// XNU maps certain segments with specific protections regardless of what is in the load command.
214+
uint32_t SegmentFlagsForKnownXNUSegment(std::string_view segmentName)
215+
{
216+
for (const auto& entry : s_initialSegmentProtections)
217+
{
218+
if (segmentName == entry.name)
219+
return entry.protection;
220+
}
221+
return 0;
222+
}
223+
224+
uint32_t SegmentFlagsFromMachOProtections(int initProt, int maxProt)
225+
{
226+
uint32_t flags = 0;
227+
if (initProt & MACHO_VM_PROT_READ)
228+
flags |= SegmentReadable;
229+
if (initProt & MACHO_VM_PROT_WRITE)
230+
flags |= SegmentWritable;
231+
if (initProt & MACHO_VM_PROT_EXECUTE)
232+
flags |= SegmentExecutable;
233+
if ((initProt & MACHO_VM_PROT_WRITE) == 0 && (maxProt & MACHO_VM_PROT_WRITE) == 0)
234+
flags |= SegmentDenyWrite;
235+
if ((initProt & MACHO_VM_PROT_EXECUTE) == 0 && (maxProt & MACHO_VM_PROT_EXECUTE) == 0)
236+
flags |= SegmentDenyExecute;
237+
return static_cast<BNSegmentFlag>(flags);
238+
}
239+
240+
} // unnamed namespace
241+
242+
uint32_t SegmentFlagsForSegment(const segment_command_64& segment)
243+
{
244+
std::string_view segmentName(segment.segname, std::find(segment.segname, std::end(segment.segname), '\0'));
245+
uint32_t flagsFromLoadCommand = SegmentFlagsFromMachOProtections(segment.initprot, segment.maxprot);
246+
if (uint32_t flagsFromKnownXNUSegment = SegmentFlagsForKnownXNUSegment(segmentName))
247+
{
248+
constexpr int MASK = ~(SegmentContainsData | SegmentContainsCode);
249+
if ((flagsFromKnownXNUSegment & MASK) != (flagsFromLoadCommand & MASK))
250+
LogDebugF("Overriding segment protections from load command ({}) with known segment protections {} for segment {} ({:#x} - {:#x})",
251+
FormatSegmentFlags(flagsFromLoadCommand), FormatSegmentFlags(flagsFromKnownXNUSegment), segmentName,
252+
segment.vmaddr, segment.vmaddr + segment.vmsize);
253+
return flagsFromKnownXNUSegment;
254+
}
255+
256+
return flagsFromLoadCommand;
257+
}
258+
259+
uint32_t SectionSemanticsForSection(const section_64& section)
260+
{
261+
std::string_view segmentName(section.segname, std::find(section.segname, std::end(section.segname), '\0'));
262+
int flags = SegmentFlagsForKnownXNUSegment(segmentName);
263+
if (!flags)
264+
return 0;
265+
266+
if (flags & SegmentExecutable)
267+
return ReadOnlyCodeSectionSemantics;
268+
269+
if (flags & SegmentWritable)
270+
return ReadWriteDataSectionSemantics;
271+
272+
return ReadOnlyDataSectionSemantics;
273+
}

view/kernelcache/core/Utility.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ inline int CountTrailingZeros(uint64_t value)
2525
}
2626
#endif
2727

28-
BNSegmentFlag SegmentFlagsFromMachOProtections(int initProt, int maxProt);
28+
namespace BinaryNinja {
29+
struct segment_command_64;
30+
struct section_64;
31+
}
32+
33+
uint32_t SegmentFlagsForSegment(const BinaryNinja::segment_command_64& segment);
34+
uint32_t SectionSemanticsForSection(const BinaryNinja::section_64& section);
2935

3036
int64_t readSLEB128(const uint8_t*& current, const uint8_t* end);
3137

0 commit comments

Comments
 (0)