Yalu’s bug chain started shortly after Apple Watch’s release. Writing a full jailbreak was something I had never done before, and after researching a lot of common techniques used by previous jailbreak techniques I had a plan. The first step of said plan was acquiring a kernel exploit. Thankfully, I had a few stashed away at the time.
Main issue at that point was getting code to run on the watch. And for further kernel exploits, it had to be unsandboxed, too.
I had been tipped by a friend that WatchOS’s installd was hacked up to only allow Apple-signed binaries, and that it was possible that the kernel did not enforce such a check. I also recalled previous jailbreaks used installd toc-tous to install invalid apps on a phone. After playing around a bit I noticed something interesting: symlinks that pointed out of the sandbox container would be honored by installd, as long as CFBundleExecutable isn’t a symlink. So I tried to replace Info.plist instead. The result was quite amazing: installd would follow the link, use an Info.plist that was in an AFCable path, validating the CFBundleExecutable. The app installed correctly. After switching CFBundleExecutable with another binary and rebooting, I could execute a binary at will by running the app.
This primitive was quite interesting but it had never found it’s use.
Fast-forward a few months, shortly before iOS 9’s release. I had access to a jailbroken device on which I could carry my tests out. And it turned out “allow_dylibs_to_override_cache” was a string still present in dyld (red herring by the way; it isn’t usable anymore, but i realised this later).
I had a plan: escape sandbox, find yet another segment overlap, replace libmis.dylib, neuter amfid, get kernel exploit running (bug of choice was CVE-2015-6974, later exploited by Pangu9).
I began looking for segment overlaps, quickly realising that Apple’s latest dyld changes were a real challenge. I had also noticed that many of the checks were related to load commands, and segment overlaps between a mach-o’s own segments.
What about Mach-O segments overlapping another allocation? There were no obvious checks for that. I however realised that position independent executables couldn’t trivially overlap other allocations. And so I dug deeper..
It turned out that non-position independent executables would allow me to specify direct arguments to a “mmap” call; dynamic libraries cannot however be non-position independent. But dlopening a non-PIE MH_EXECUTE Mach-O was no problem.
Also, due to load commands not being overlapped, crashIfInvalidSignature would still be a problem. An easy one to solve, however: gain code execution before the function could ever be called. Overlapping the stack was a viable way to accomplish this, and it also allowed easy non-codesigned code execution via ROP.
And so I began writing a PoC.
After some testing I realised I missed a check: for non-PIE Mach-Os a vm_allocate call would be performed before mmaping the segment in. Since the stack memory was already allocated this would fail. Looking at the dyld source code however pointed out that this was not performed for “__PAGEZERO” (this is reasonable because __PAGEZERO is never actually mapped by dyld itself, and the mmap call for it is a null-op since filesize is supposed to be zero. However, since vmsize is non-zero, vm_allocate would still be called and it’d fail because kernel had that already covered).
And so I had a non-PIE MH_EXECUTE with a __PAGEZERO segment overlapping the stack at mmap time, way before load commands (that had to be marked as R-X) could be faulted in (causing a code sign fault).
ASLR was still a problem, but after looking at the respective xnu functions it turned out that stack ASLR and main binary ASLR were derived from the same random number, and each possible ASLR value could be covered by mapping in 0x100 pages. For each page a single possible main binary ASLR value could be derived. I simply filled pages with ROP NOPs followed by a stack pivot to a chain crafted for a particular main binary base address.
To improve the gadgets available, I decided to rip off signed pages off dyld and got them mapped as executable- and page faults wouldn’t cause code sign faults since they were correctly signed. This could be mapped at a fixed address, thus bypassing the whole ASLR issue entirely.
I still had no way to get dyld to load my Mach-O. And so I began digging and I ended up in the DeveloperDiskImage.dmg. I noticed that some binaries were linked with a framework, GPUTools, which resided in the DDI itself. Said binaries would link to the framework via @rpath, and the binaries would contain a LC_RPATH indicating ../../PrivateFrameworks/ as a rpath.
And I recalled about my installd TOC-TOU. Plan at this point was to install any application with an Info.plist replaced with a symlink, placing the original plist as symlink target and swapping it with a symlink pointing to a plist indicating as CFBundleExecutable a binary (MobileReplayer) ripped off the DeveloperDiskImage, restored with MobileBackup (to add the executable bit, which AFC does not set), and made sure ../../PrivateFrameworks/GPUTools.framework/GPUTools was my overlapping Mach-O.
I tested this all on my unjailbroken 8.4.1 device, effectively gaining unsigned unsandboxed code execution. And the dyld attack worked great on the jailbroken 9.0 device.
I proceeded to re-enable AMFI and tested again. And I had a sad face.
In iOS 9 Apple added a function that validates load command signature before even mapping segments, effectively killing all TOC-TOUs in sniffLoadCommands/mapSegments/crashIfInvalidCodeSignature.
I also tested the installd TOCTOU and it turned otu iOS 9 installd was also hardened to prevent extracting symlinks pointing out of the app container, effectively mitigating the attack.
PanguTeam also had documented a known heap overflow on 8.4.1, in GasGauge. My context allowed me to IOServiceOpen the userclient, and while doing it in ROP was a pain, i could effectively gain kernel r/w. So, I decided to focus on 8.4.1 rather than 9.
Persistence could be achieved the same way, but instead using neagent to load my library. Easy peasy, although using Pangu9’s codesign bug is way easier, since you just need to copy the untether in and patch the dyld shared cache.
The name “yalu” is a Kim Jong Crack-esque joke about the river on North Korea’s border, joke being that getting into NK is akin to “jailbreaking”, achieving unlimited freedom!
Note that yalu development has halted due to better things to do, constant harassment from the jailbreak community and a thousand other reasons.
Thanks to everyone who made it possible, especially windknown / _Morpheus_ / posixninja / iH8sn0w / Esser / AriX et al.