Dedicated to Rie.
Preface
This might be the last blog post for the forseeable future if I fail or unless something changes.
Summary
More slopping 0days. Yay. This time, in celebration of an oshi day that may not come; for I’ve been working far too hard only to be rewarded by…
Initial Investigation
“Claude, find 0days in the most recent release of XNU on GitHub. Make no mistakes.”
And it actually did. Okay, wow. This one’s a bit detailed since it has to do wtih my favorite autism passion project: dyld; but also has to do with the kernel. So.
Big art for someone I really care about - who means my life to me. You can remove the ANSI art header if you want.
Regardless, the implications from this may seem small. Sure, it’s per process.
Enter launchctl
launchctl is a command-line utility on macOS that allows you to manage launchd. It’s a simple command line program. And well, while macOS and it’s bretheren unfortunately lack the /etc/ld.so.preload deal; we do have a substitute.
launchctl setenv DYLD_INSERT_LIBRARIES=/tmp/cottou.dylib
Assuming you have a CSR bypass - hard to come by, nowadays; or something that gets around AMFI, you’ll be increasing the attack surface of just about every newly spawned binary on the system should it accept cottou. You’re degrading the security posture of every process that spawns after that command is set - sure, it needs sudo, but refer to one of my previous blogposts for some words on sudo.
Per-process, Lockdown Mode is disabled if cottou is allowed into the process. Pointer authentication can be bypassed; ASLR trivially defeated. Every userspace program, especially in the case of a CSR bypass, will have the dylib loaded into it. Think of something like pam_tid.so, a Pluggable Authentication Module that ships on Macs that support Touch ID. Now you can debug it. Now you can potentially inject code into it, assuming you have a userland rootkit position. The possibilities, effectively, are endless.
It works on iOS, too. PAC bypass and all. Doesn’t bypass MIE, or rather, MTE.
The Technical Details
I’d tell you to read the slop code and figure it out, just how I did. But that’s not very nice, so here’s a generalized example of how it works.
First of all - a disclaimer. You are not first-mapper with this exploit. Don’t rely on it if you need to be first mapper for a chain.
Second of all, the vulnerability is in the core of dyld. Namely, MachOAnalyzer::forEachRebase_Opcodes().
To be more specific, the culprit lies here: common/MachOAnalyzer.cpp:5025-5117
// common/MachOAnalyzer.cpp:5068 — EXECUTION PATH (called during loading)
case REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
segIndex = immediate; // 4-bit immediate, range 0-15, NO BOUNDS CHECK
segOffset = read_uleb128(diag, p, end);
segIndexSet = true;
break;
The immediate value is extracted from the low 4 bits of the opcode byte (opcode & 0x0F), giving an attacker-controlled range of 0–15. For a typical 3-segment dylib, valid indices are 0–2. Indices 3–15 produce an out-of-bounds array access.
From my ZDI submission:
Function MachOAnalyzer::forEachRebaseLocation_Opcodes() (line 5005) allocates a variable-length array of Header::SegmentInfo structures on the stack, sized to the number of segments in the loaded Mach-O:
// common/MachOAnalyzer.cpp:5012
BLOCK_ACCCESSIBLE_ARRAY(Header::SegmentInfo, segmentsInfo, leInfo.layout.lastSegIndex+1);
BLOCK_ACCCESSIBLE_ARRAY (defined in common/Array.h:400) expands to a true stack-allocated VLA:
Header::SegmentInfo __segmentsInfo_array_alloc[lastSegIndex+1];
Header::SegmentInfo* segmentsInfo = __segmentsInfo_array_alloc;
When forEachRebase_Opcodes() processes the REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB opcode, it sets segIndex = immediate without bounds checking. The handler callback then dereferences:
// common/MachOAnalyzer.cpp:5019
uint64_t rebaseVmOffset = segments[segmentIndex].vmaddr + segmentOffset;
This reads segmentsInfo[attacker_index].vmaddr — an out-of-bounds read from the stack. The resulting value is combined with segmentOffset (also attacker-controlled, encoded as ULEB128 in the opcode stream) to compute a write target. Loader::applyFixupsGeneric() then performs *loc += slide at that target address, producing an arbitrary write.
--
Which leads to ACE! :)
Nothingburger or Not?
Depends on if you have something to chain it with. dyld is a very fragile butterfly, very malleable. By far, this is the diamond in the coal mine of dyld and dyld related bugs I’ve found. It doesn’t exactly stop here, though. It can potentially affect other tools in the dyld toolset - but I’ll leave that up to your imagination, eh?
Slop PoCs will be on https://github.com/impost0r/Cottou. It was a fun bug to sit on, but aside from the userland kit I have no real use for it. I look forward to seeing what creative uses you all will have for it.
And some more interesting things (patch_oob_rebase.c, the raw ASM for the shellcode itself - are for sale on Patreon. https://www.patreon.com/c/impost0r