The TaiG Jailbreak for 8.3 and 8.4 came out of the blue, entirely unannounced, almost a month ago. During that time, there has been no public documentation about how it actually works. I plan to discuss this jailbreak at length in Technologeeks' upcoming OSX/iOS Internals for Reverse Enginners training, as it makes a perfect case study. There is no monopoly on knowledge, however, so - in what might become a tradition (after my previous writeup on the 8.1.2 jailbreak) - I decided to post a writeup here. This will also make it to the 2nd edition of MOXiI, which covers jailbreaks in a dedicated chapter.
I'm posting this because Apple is readying 8.4.1b, which along with "performance improvements" to Apple Music it will likely snuff the really clever 0-days used by TaiG - and I'm certain that these will be gone forever by iOS 9... As we've said before, I'll say again - it would have made sense to wait with the JailBreak for iOS 9. While I disagree with TaiG's haste, I guess brilliance cannot be contained. Update to 8.3/8.4 before the ticket window closes..
Tools used
I've made extensive use of my own jtool for the purposes of static analysis on Taig's binary. jtool is fast approaching its 1.0 release, and my ARM64 support is almost entirely complete (I gave up on ARM32 down the road since writing your own disassembler for both ARM and THUMB is just painful). Following my previous writeups I added a --html option to automatically produce colorized and hyperlinked output. Another important feature is that of a companion file. At some point I hope to support DWARF, but for now it's a simple textual file formatted as address:label:comments, with a few limitations (i.e. must be sorted, function labels start with a '_'). This made it easy to augment jtool's already pretty useful symbolication skills. If you want the companion file to use with it as you go along, you can find that in the downloads, below.
Static analysis, though, has its limitations, particularly in figuring out runtime derived data. So where jtool inevitably faltered, I opted to use dynamic analysis, with a debugger. The debugger of (only) choice is, of course, lldb, which can be found in the DeveloperDiskImage, and massaged to debug any binaries by re-signing debugserver with the proper entitlements.
For runtime statistics and process tracking I use my own tools:
Process Explorer (procexp) - my top replacement, which has since evolved to include vmmap support, integrated lsof and much more. Get it here
File Monitor (filemon) - my fsevents client, which tracks all filesystem activity (arguably, to the extents of what fsevents can provide) - which you can get here, with source.
Downloads
If you want to follow along, there is no time like the present. Since 8.4.1 is not out yet, 8.4 (and 8.3, possibly?) is still within AAPL's signing window, so you can get the binaries onto your device and do this as a rolling exercise. Worst that can happen if you lose the jailbreak, is that you can re-jailbreak and restore to 8.4. An unpleasant, yet doable process.
Grab a copy of the files from right here. The tgz includes the DMGs and trojan files (not the untether - I cover that later). You can get my tools from various places in this site, so that should be easy. Make sure you're using the latest version of JTool, because otherwise earlier versions (as well as other tools) will crash on the Taig messed up binaries!
I. Getting on the device
The TaiG2 jailbreak is remarkably similar to the original TaiG. In particular, the method used to get on the device initially (TaiG's 20-40%) is the tried and true DevelopDiskImage race condition, which I've discussed in the first part of the previous version's writeup. What makes the DeveloperDiskImage (DDI) so darn attractive is the "right mix", namely:
The root filesystem (/) is mounted read-only - so you can't really upload anything else there
The DDI can be mounted from iTunes (or generally any client using the AppleMobileDevice frameworks)
Apple still (what is it, now, four? five years?) hasn't fixed that darn race condition. Tsk tsk.
Lockdown is just plain stupid.
The Race Condition
A nice feature of TaiG is that it allows you to try the jailbreak even if the device has already been previously jailbroken. That's useful, in that one can get a tool like filemon. The volume of filesystem activity is such that fsevents drops a few events here and there, but the gist of it can be clearly seen (excuse the overflow of the dirnames below - the UUID names are a mile long):
Those links in c/c/c/c/c/c are all symbolic links to /var
And if you catch the DMG while the jailbreak is in action (a little bit tricky since it keeps getting deleted), you will see:
And of course, we have a symlink from mobile_image_mounter to us:
If you're reading this and getting a deja vu - no, I didn't cut/paste from the previous writeup - these are results from a real device. But it's the exact same method. I may have said this before, but since nobody heard it the first time(s) - Learn from history or be condemned to repeat it. And repeat it. And repeat it. In fact, TaiG repeats it twice.. Not one but two Disk Images this time.
DDI(s)
Once mounted, the fake DDI (input.dmg in the download bundle) looks like so:
Note the multitude of lockdown agents there. At 40%-60%, another DDI is inserted but not mounted (yet) this time packed with pure goodness - a fake lib (slice 2) and Caches (slice 3). The launchagents for com.apple.mount_lib_[1..8] and com.apple_mount_cache_[1..8] can then be called. Why so many? because they are essentially the same, trying to mount the two slices over /usr/lib and /System/Library/Caches, respectively. For example, com.apple.mount_lib_1.plist looks like:
(com.apple.mount_cache_#.plist is defined similarly, with disk#s3)
When the service agents have run, one of them will get the right disk (BSD device) number, and as a result the slices (partitions of the DMG) will be mounted... right over /usr/lib and /System/Library/Caches)
It takes both the filesystems to make the magic happen: The /Caches hides the real /System/Library/Caches underneath it and "creates" /System/Library/Caches/com.apple.dyld/enable-dylibs-to-override-cache - the well known hard coded fault into dyld without which jailbreaks would have been immeasurably harder. Because the file exists, dyld can be cajoled to ignore the shared library caches - and look through frameworks and libraries on disk.
This is where the second filesystem - the fake /usr/lib - comes into play. It contains the (by now) usual libmis.dylib which redirects the MISValidate logic of the despicable AMFId to always return true. This time, however, there's a new dyld replacement - linked to /var/mobile/Media/install/file_d.
Meanwhile, in /var/mobile/Media/install..
While all the action was on the race condition, the loader program also dropped a few more files in /var/mobile/Media/install:
So what have we here? It's easier to figure out who's who by looking at a device once it's jailbroken fully:
The taig binary, once run with the "-s" switch (for setup, as started by the com.apple.exec_s.plist injected Lockdown service), performs the deployment of these files (in function 0x10000a3b4, called from 0x10000a928, called from 0x10000be6c in the main, right after / is remounted writable).
Note: I'll cover the untether with full disassembly/partial decompilation, in a follow up. This part is already getting long, and there's still code signing to cover..
As you can see,taig replaces not just libmis.dylib, but amfid, and - initially - dyld (which was the file_d), which - while not copied, is used before amfid gets replaced! To get past code signing, they pull off an awesome trick, discussed next.
II. Bypassing code signing
(and the really bright exploit)
Just replacing libmis.dylib won't work anymore since Apple patched almost all the overlapping segments (I've discussed those in an RSA 2015 talk in the wee hours of the morning). Overwriting amfid itself will fail since it can only be replaced with another file whose CDHash is in the kernel trust cache. And we can't patch the kernel at this stage, nor we can do a permanent patch on disk (since it would violate the APTicket).
So what is this trojan amfid? jtool reveals:
So many things here are wrong! Consider:
Duplicate architecture binaries in same fat binary? Shouldn't that be illegal?
Empty architectures? Shouldn't THAT be illegal?
27 architectures?!
But, XNU, your friendly kernel, just aims to please. So, what the heck. Let's load these anyway. This is where it gets even more interesting: Using jtool -arch # (a switch I had to add because of Taig :-), you can see specific architectures in a fat binary, by number. And so:
So once more, fake segments (right at the end there). But also a file containing just a code signature(!) and another containing an empty segment (__DATA) which nonethless contains sections, and then some unknown load commands.jtool now dumps unknown load commands as raw bytes, and you can see where one would expect an LC_LOAD_DYLINKER, there's a blob - the ASCII in which reads .../usr/lib/dyld. Though the dylinker of choice is /usr/libexec/amfid_d, down below. Note this is exploiting a decades old bug/feature from back in NeXTSTEP allowing a program to state its dylinker. (One would think Apple would solve this trivially by hardcoding the choice, at least in iOS..)
The /usr/libexec/amfid_d is another messed up binary with 28 or so architectures, again with the same setup: two binaries (ARMv7s = 0 and ARM64 = 1), their detached signatures (ARMv7s = 26 and ARM64 = 27) and 24 empty architectures (0/0) in between - similar to amfid. This is also similar to /var/mobile/Media/install/file_d, which is used to bootstrap taig before it can install the libraries permanently.
With the binaries so obviously malformed, Apple's lipo(1) tool cowardly refuses to deal with them (since FAT binaries aren't allowed to contain two instances of the same architecture). I therefore extended the jtool to handle architectures by number as well as type, and enabled the -extract feature to also handle architectures. So we can look inside that __TEXT_FAKE - it maps to the file's 0x8000, so we can use :
So, we have a 32k file - truncated, but who can we compare it to...? The original /usr/libexec/amfid_0, of course!
Inspecting the code signatures
so what about the code signatures? Note that the trojan amfid nor its nested Mach-O have a code signature. Where is it? In arch 0. Using --sig, peeking inside the signature. When used with -v, jtool displays all page hashes, instead of only complaining about signature mismatches.
Note that even though the Mach-O only claims a single LC_CODE_SIGNATURE, there is file content. Comparing this with the original file this purports to be - /usr/libexec/amfid_0, we see:
In other words, the trojan amfid's "code signature" is derived from the real /usr/libexec/amfid_0. A similar trick, btw, is used with /usr/libexec/amfid_d, the trojan dyld, whose signature is identical save for the first 8 pages, and the very last page, which have been modified.
If we take up the trojan amfid.arch_26 - which has no code signature, and try to calculate it ourselves, we'd get:
Looking at the original amfid_0, we can see the breakdown of pages. You can do this with pagestuff(1) on OS X, or (for a better output, IMHO, jtool --pages:
Inspecting the process
At this point, static analysis shows its clear limitations - we can't figure out exactly which #$%$#% code signature or which page goes where. It's time to inspect the code signature of the running amfid. We do this with a simple call to csops(1), which can return the code signing blob of any given PID (in this case, amfid - 165) for us:
That '81d...734' is the hash of the code directory which - as far as XNU is concerned - is what gets validated in order to load the process. Any individual access to pages will be through the slots.. But '81d...734' is not the CD Hash we would find in the trojan /usr/libexec/amfid.. it's the CDHash of the ORIGINAL amfid (backed up as /usr/libexec/amfid_0). This CDHash is recognized by AppleMobileFileIntegrity.kext as it resides in its trust cache - and the process is allowed to execute. Note the page (slot hashes) match those of the real /usr/libexec/amfid_0 as well (compare with jtool --sig -v /usr/libexec/amfid_0 ), which makes sense, otherwise the CDHash wouldn't have validated in the first place. So all this convolution loaded the original amfidcode.
So what gives? Why go to all this trouble? We have to look deeper. Using procexp regions (the integrated vmmap support, we have the process looking like this (ignore the 2DOs - I'm still working on identifying memory tags):
So, where we'd expect to normally see dyld, we see amfid_d - corroborating what we'd expect from the jtool -l listing. But that's still a partial picture: We don't know what's in the code of these Mach-O objects that were loaded
Going deeper still
To really understand what goes where, we need to inspect the trojan amfid_d. It doesn't have a code signature (aside from the detached one), but we can easily calculate what it would be using sha1 on the individual pages:
The two binaries, then - trojan dyld and dyld - are exactly the same with respect to code, though the former is crazy mapped ("folded", if you will). They're also almost the same data wise, with a major divergence happening in dyld's 57th page, which should have been the same as the amfid_d's 53. What's so special there? Look at jtool --pages (on either binary):
That page 57 is right at the end of the string table (0x39 * 0x1000 = 0x39000). Along with the code signature, which follows, the last two pages of the file are deliberately malformed. How?
So, 0x39000 + c50 = 0x39c50 = exactly where the code signature would be. To make this clearer, do the bindiff and actually examine the output:
And indeed, 0xfade0cc0 is the CSMAGIC_EMBEDDED_SIGNATURE. But what is it being replaced with?!
III. Shellcode!
Random looking junk distributed too uniformly could imply shellcode. so it would be great to try and disassemble..
Problem - I don't have arbitrary disassembly in jtool yet (it needs to operate on a Mach-O..).
Solution - copy the mystery code into a random binary , and disassemble!
We do need to fix the addresses first -- the mystery code is loaded into __LINKEDIT, at offset 0x39c50. __LINKEDIT starts at offset 0x2c000.. So we are therefore 0xdc50 bytes into the segment. This would therefore translate to the (nonslid) address of 0x12006dc50. 944 bytes are up to 236 instructions (thankfully, ARM64 has fixed 4-byte), so a simple empty binary (full of "MOV X0,X0" or some other garbage) would work well here. I have a sample program (already patched, with the companion file for it), Right here.
And the code - right from the HTML - is included here. The comments were also generated with the help of a companion file ,which is kind of like jtool's skimmed version of DWARF (-:
And now you know why we needed that page56, above (the third invalid page, with hash 489d86b8874964f514f8baf1a8c8921ac1667d97 ) - It contains the strings "/usr/libexec/amfid" and "/usr/libexec/amfid_d"). All pieces are present and accounted for.
Long story short: The shellcode opens both /usr/libexec/amfid and /usr/libexec/amfid_d and uses fcntl with the little known F_ADDFILESIGS operation (q.v. /usr/include/sys/fcntl.h - "/* add signature from same file (used by dyld for shared libs) */")to ...well... add their signatures!. That's it! The trojan amfid can now execute uninhibited!
Edit: The Comex Clarification
Ok. Maybe that was a little bit too short :-) They tell me that @comex noted I left out where the code exec happens. Sorry. I thought that was obvious. Writing this loooong screed at the wee hours, it must have eluded me. Once more, I bring jtool to hunt for the evidence:
Note the entry point: 12006dc60. I marked that in the shellcode output, above. As for the five segments you see here, those are the 1..5 I talked about earlier in the procexp output. For the lazy:
The main thing to note here is that __LINKEDIT is made executable - and that's where the entrypoint/area-formely-known-as-the-code-signature gets executed. Capiche?
(btw, people, I don't watch TWTR or Reddit regularly - feedback is handled faster by the Book forum thread)
This is, for lack of a better word, $#%$#%$#%$# Brilliant! 精彩.. 卓越的越狱! My only qualm is that this could have been used for iOS 9...
IV. libmis.dylib
This one is super easy, since TaiG is reusing this proven method (though I wouldn't be surprised if they were accused of stealing it..) Extracting /usr/lib/libmis.dylib shows us this is clearly the trojan library. To extract memory from running processes, I use my coreruption tool. This is a set of tools all-in-one for process intrusion (think jtool versatile, on running code.. kind of like the Volatility framework, but for iOS, and with more features) which I use.
Though it's hard to see without obtrusive inspection of the process (which is difficult, since lldb crashes on it, though fortunately coreruption prevails), the "trojan" amfid is doing exactly what the original would do - waiting for requests on the Mach port (host special port 18) from AppleMobileFileIntegrity.kext. This is required because the taig untether is not validly code signed, and would be killed unless amfid vouches for it. As you'll see later, as part of its patches it disables MACF partially via security.mac.proc_enforce, but it can only do that if it successfully executes. Its libmis.dylib (unsigned) performs the well known evasi0n
From my experiments, amfid is extremely unstable and sooner or later crashes. For most jailbreak users, this is inconsequental, and they won't notice anything. amfid's other role, however, is to answer the kext when it checks for unrestricted debugging (i.e. if amfid dies or crashes, lldb won't be able to use task_for_pid freely, even with an entitlement - lldb will hang, and AMFId will complain (via dmesg/syslog) with the ominous message "int _permitUnrestrictedDebugging(): permit_unrestricted_debugging server is dead" coming from the kext. (@comex: - that's an unintended consequence of crashing).
All this was just the backdrop to allow the untether (/taig/taig) to run. But that's enough for one looooong article. More to follow soon.
Greets
TaiG @taig_jailbreak) - You rock. But seriously, quit burning amazing exploits, ok? Apple's not making this easier, you know.
Comex @comex - Come work with us @TG :-) We're hiring
Notes:
For comments/questions, please use the New OS X Book forum. If you're interested in learning more about our trainings @Technologeeks (the upcoming one is at max capacity, but we're planning another mid December!), find details here.