Taking apart iOS OTA Updates
To Part 2

Taking apart iOS OTA Updates

Peeking into Over-The-Air Update bundles in iOS

Jonathan Levin, http://newosxbook.com/ - 03/18/14

About

iOS introduced Over-The-Air (OTA) updates sometime back in 5.x, which was after the 1st edition of MOXiI was already published. As such was the case, this very important discussion was left out of where it should have been included (Chapter 6). With the 2nd edition in the works, OTAs will naturally be included - but since this information is immediately useful (as you'll see shortly), I figured it made sense to publish it as an article.

Why should you care? (Target Audience)

As you probably know, iOS software images (the well known .IPSWs, which I did cover in the book) are encrypted. Thanks to the hard work of the folks at the iPhoneWiki, decryption keys are made available fairly regularly for A4 and A5 devices - the former via limera1n, and the latter through an undisclosed but pretty well known exploit somewhere in the boot chain. The problem, however, is that no keys exist for A6+ devices, most notably the 64-bit devices (5S, 6, iPad Air, etc). This is exacerbated by Apple effectively phasing out A4 with 8, and the very likely end-of-line for A5 when iOS 9 is likely to be announced this summer.

Thankfully, OTA updates exist and are unencrypted. This means that if you have a jailbroken device of version 8.1.2 or lower, you can do two pretty amazing things:

  1. Obtain the OTA update, and - using the techniques demonstrated here - unpack it so as to reverse engineer it to your heart's content
  2. or
  3. Update the iOS filesystem image manually - while maintaining the jailbreak. That's actually a long and very delicate process which, if done improperly, can render your device unbootable and force you to restore to the latest, non jailbreakable version. If carried out correctly, however, you get the latest and greatest iOS - without sacrificing a jailbreak. A manual update is effectively indistinguishable from a stock update, provided you don't mess with the kernelcache or dyld, thanks to which the jailbreak remains feasible. In fact, this is important enough to merit a nice div of its own:
  4. If you ARE going to try this at home on a real device - caveat - if you accidentally update dyld or the core libs you might end up locking yourself out of the user mode component of a given jailbreak. If you touch the kernelcache you will render the apticket.der invalid - which is even worse, since iBoot will refuse to boot you - and force you to restore, when the only IPSWs Apple presently signs are no longer jailbreakable. At least, publicly. A future post will demonstrate how to do so - but it requires meticulous attention to detail. Believe me.

Both the above are great, but especially the latter. In the case of 8.2, for example, that gives you the Apple Watch support, in case you absolutely must send your heartbeat to your friends (Awwww). But going forward, this will likely be useful for whatever 9.x brings - if only to reverse the hell out of it. So let's get started, but before that - the usual disclaimer:

If you know me, you know I'm all for keeping iOS jailbreakable - despite Apple's herculean efforts. I wouldn't be publishing this if I thought Apple could plug this. Since an OTA is started from user mode when iOS is already running - unlike a full restore/upgrade which is carried out via iBoot - it does not require (and, in fact, can't access) the UID/GID keys. This also means that Apple can't easily encrypt the OTA updates. At least not on the present generation of devices.

OTA Update bundles

OTA update bundles are zip files which Apple makes readily available for download at http://appldnld.apple.com/. The zip files are cryptically named, in what's probably a SHA-1 of their "real" name. The convention appears to be:

http://appldnld.apple.com/ios#.#/system-dmg-##-YYYYMMDD-GUID/com_apple_MobileAsset_SoftwareUpdate/40-hex-digits.zip

Fortunately, you can find all the files easily through the iPhoneWiki OTA page - and download them easily. You don't even have to fake a User-Agent (Have I mentioned, Cupertino Syndrome? Lock the door (encrypt the fs), but leave the Window wide open :).

As you can see, each OTA update is basically keyed to two factors - the device build, and the base iOS version from which the update is taking place. This makes it easy to pick out the update that's just "right for you", given whichever device you have, and whichever version you're stuck in with the jailbreak. In our example, let's pick the version I used, which is to iOS 8.2 from iOS 8.1.2 on an iPhone 6. That's http://appldnld.apple.com/ios8.2/031-15794-20150309-8FF6FDB8-C21F-11E4-8DFB-9EA63780E501/com_apple_MobileAsset_SoftwareUpdate/58af7e9d9d6ade039f47f01bcd420887645204bb.zip.

Downloading this (or any other) update, an unpacking the zip, expect to see the same directory structure, which is something like this:

  • META-INF/: Subdirectory containing a single file, com.apple.ZipMetadata.plist, which is a binary plist:
    morpheus@Zephyr(/tmp/OTA)$ cat META-INF/com.apple.ZipMetadata.plist   | plutil -convert xml1 -o - - 
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>RecordCount</key>
    	<integer>3162</integer>   <!-- Count of files in Zip -->
    	<key>StandardDirectoryPerms</key>
    	<integer>16877</integer>  <!-- 40755 for directories -->
    	<key>StandardFilePerms</key>
    	<integer>-32348</integer> <!-- 644 for files         -->
    	<key>Version</key>
    	<integer>2</integer>
    </dict>
    </plist>
    
    all in all, nothing too interesting.
  • Info.plist - As with any bundle, the required manifest. This is a standard Plist, which looks like the following:
    morpheus@Zephyr (/tmp/OTA)$ cat Info.plist 
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
            <key>CFBundleIdentifier</key>
            <string>com.apple.MobileAsset.SoftwareUpdate</string>
            <key>MobileAssetProperties</key>
            <dict>
                    <key>ActualMinimumSystemPartition</key>
                    <integer>2648</integer>
                    <key>Build</key>
                    <string>12D508</string>          Build of update (12D508 = 8.2)
                    <key>InstallationSize</key>
                    <string>0</string>
                    <key>MinimumSystemPartition</key>Min. partition size for upgrade
                    <integer>2648</integer>
                    <key>OSVersion</key>             Version of update 
                    <string>8.2</string>
                    <key>PrerequisiteBuild</key>     Build of origin (12B440 = 8.1.2)
                    <string>12B440</string>
                    <key>PrerequisiteOSVersion</key> Version of origin
                    <string>8.1.2</string>
                    <key>SUDocumentationID</key>
                    <string>PreRelease</string>      
                    <key>SUProductSystemName</key>   as can be expected..
                    <string>iOS</string>
                    <key>SUPublisher</key>
                    <string>Apple Inc.</string>
                    <key>SupportedDevices</key>
                    <array>
                            <string>iPhone7,2</string> Internal number of device (7,2 = 6)
                    </array>
                    <key>SystemPartitionPadding</key> Specified in MB
                    <dict>
                            <key>128</key>
                            <integer>1280</integer>
                            <key>16</key>
                            <integer>260</integer>
                            <key>32</key>
                            <integer>320</integer>
                            <key>64</key>
                            <integer>640</integer>
                            <key>8</key>
                            <integer>180</integer>
                    </dict>
                    <key>__AssetDefaultGarbageCollectionBehavior</key>
                    <string>NeverCollected</string>
            </dict>
    </dict>
    </plist>
    
  • AssetData - Where the update binaries, etc are.
  • AssetData has an internal directory structure, like so:

  • Info.plist - Once again (*sigh*) we have a plist here, containing similar keys to the previous one, but with the addition of a SystemUpdatePathMap dictionary, containing the names of the non-filesystem images (i.e. baseband, RAMdisk, glyphs, etc) which (from what I can see) aren't part of the update, or are present only in patch form)
  • boot/ - A subdirectory containing actual non-filesystem images. These are all in the boot/Firmware/... directory, and contain the kernelcache.release.boardId, iBec/iBSS (in boot/Firmware/dfu), and iBoot and friends (in boot/Firmware/all_flash/all_flash.boardIdap.production.. These are all in im4p (or, pre-A7, IMG3) format, which means that they're encrypted. :-( Apple can encrypt those because they're handled during the boot process, wherein the GID key is still accessible.
  • boot/ also contains a BuildManifest.plist, a lengthy manifest containing the update rules for the various im4ps. I'll leave this rather detailed description for the book..
  • payloadv2/patches: A directory containing deltae to be patched. This is, of course, assuming, that the versions you're patching from are exactly the ones which came with stock iOS. These are in BXDIFF format, specifying offsets and patch data.
  • payloadv2/prepare_payload: A PBZX file (see later), containing an (encrypted) IM4P. Likely used by iBoot and friends
  • payloadv2/removed.txt: Specifying a list of files which will be removed (potentially replaced), one per line.
  • payloadv2/links.txt: Specifying a list of symbolic links to link to (=) and add (+).
  • payloadv2/payload: More on that in a moment
  • Various BOM files

The BOM files

Apple uses the venerable NeXTSTEP Bill-Of-Materials (BOM) proprietary format for their bundles. The private Bom.Framework (which likely hasn't changed from NextSTEP, aside from 64-bit support..) handles these files, and you can view them on OS X using the lsbom(8) command, or package them yourself with mkbom(8). The files are:
pre.bomBill of materials of files pre-application of payload
post.bomBill of materials of files post-application of payload
payload.bomBill of materials of files in payload

An additional file, payload.bom.signature protects payload.bom from tampering, though in practice that's meaningless (binaries in BOM have to be codesigned anyway, and if you're on a jailbroken device this, too, can be bypassed). I've discussed the BOM format in the book (Chapter 6), so no sense adding more here. What's more interesting is the payload file itself.

The payload and pbzx files

The patches in the payloadv2 directory are only applicable to files which already exist on the system, and - by means of patch application - can be updated to a new file version. iOS updates, however, often contain new functionality (again, the Watch support in 8.2 being a great example).

New functionality can be found in the payloadv2/payload directory. Inspecting the file reveals a "pbzx" magic, like so:

morpheus@zephyr(/tmp/OTA/payloadv2)$ od -t x1 -t c payload  | more
0000000    70  62  7a  78  00  00  00  00  01  00  00  00  00  00  00  00
           p   b   z   x  \0  \0  \0  \0 001  \0  \0  \0  \0  \0  \0  \0
0000020    01  00  00  00  00  00  00  00  00  ed  37  20  fd  37  7a  58
         001  \0  \0  \0  \0  \0  \0  \0  \0 355   7     <fd>   7   z   X
0000040    5a  00  00  00  ff  12  d9  41  04  c0  e2  ed  b4  07  80  80
           Z  \0  \0  \0 377 022 331   A 004 300 342 355 264  \a 200 200
...
........   a1  86  1f  56  0d  d3  56  37  03  00  00  00  00  00  59  5a
         241 206 037   V  \r 323   V   7 003  \0  \0  \0  \0  \0   Y   Z

The <FD>7zX you see in the first row, and the YZ at the end of the file are clearly indicative of the XZ compression header and trailer. That Apple adopted LZMA is not surprising, considering that it's already used in the kernelcaches (as of 10.10/8.0). Outright using XZ was a surprise. But that makes our life easier - grabbing the XZ sources from tukaani.org, you can quickly build the XZ binary which will decompress it. But just stripping the pbzx encapsulation won't work - the XZ data is separated into chunks - somewhat akin to how Apple compresses DMGs, if you saw the other article of mine. So you need a small "extractor" for these chunks, before you can get to apply XZ on them. I've just the tool for that, which you can compile yourself from this listing:

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>


typedef unsigned long long uint64_t;

int main(int argc, const char * argv[])
{

    // Dumps a pbzx to stdout. Can work as a filter if no argument is specified

    char buffer[1024];
    int fd = 0;

    if (argc < 2) { fd  = 0 ;}
    else { fd = open (argv[1], O_RDONLY);
           if (fd < 0) { perror (argv[1]); exit(5); }
         }


    read (fd, buffer, 4);
    if (strncmp(buffer, "pbzx", 4)) { fprintf(stderr, "Can't find pbzx magic\n"); exit(0);}

    // Now, if it IS a pbzx

    uint64_t length = 0, flags = 0;

    read (fd, &flags, sizeof (uint64_t));
    flags = __builtin_bswap64(flags);

    fprintf(stderr,"Flags: 0x%llx\n", flags);

    int i = 0;
    int off = 0;

    while (flags &   0x01000000) { // have more chunks
    i++;
    read (fd, &flags, sizeof (uint64_t));
    flags = __builtin_bswap64(flags);
    read (fd, &length, sizeof (uint64_t));
    length = __builtin_bswap64(length);

    fprintf(stderr,"Chunk #%d (flags: %llx, length: %lld bytes)\n",i, flags,length);
     
    // Let's ignore the fact I'm allocating based on user input, etc..
    char *buf = malloc (length);
    read (fd, buf, length);

   // We want the XZ header/footer if it's the payload, but prepare_payload doesn't have that, 
    // so just warn.
    

    if (strncmp(buf, "\xfd""7zXZ\0", 6))  { fprintf (stderr, "Warning: Can't find XZ header. This is likely not XZ data.\n"); }
    else // if we have the header, we had better have a footer, too
    if (strncmp(buf + length -2, "YZ", 2)) { fprintf (stderr, "Warning: Can't find XZ footer. This is bad.\n"); }

    write (1, buf, length);

    }

    return 0;
} 
EDIT: There's a fix and better version of this now - q.v. Part III

And use like so:

morpheus@Zephyr (/tmp/OTA/payloadv2)$ cc pbzx.c -o pbzx
morpheus@Zephyr (/tmp/OTA/payloadv2)$ ./pbzx < payload > p.xz
morpheus@Zephyr (/tmp/OTA/payloadv2)$ ls -l payload p.xz
-rw-r--r--  1 root      admin  443537788 Mar 18 13:47 p.xz
-rw-r--r--@ 1 morpheus  admin  443539064 Feb 26 07:16 payload
morpheus@Zephyr (/tmp/OTA/payloadv2)$ xz --decompress p.xz
morpheus@Zephyr (/tmp/OTA/payloadv2)$ ls -l p
-rw-r--r--  1 root  admin  1310837211 Mar 18 13:47 p
morpheus@Zeyphr (/tmp/OTA/payloadv2)$ file p
p: VAX demand paged (first page unmapped) pure executable not stripped 

The compression is evident - and quite impressive (1.2GB compressed into 423MB - some 65% - not bad, considering this is binary data). And we now have a single archive - that p - which is the last part of the bundle we need to deal with, and is obviously not a VAX executable..

The payload

At this point, it's obvious that "p" is some proprietary format, of some archive. This is further corroborated by attempting to view it with more:

morpheus@Zeyphr (/tmp/OTA/payloadv2)$ more p
^P^A^@^@^@^@^@^@^@.^@^@^@^@T<EE><FB>^E^@^@^@^@^@ESC^@c^@c<81><80>.fseventsd/000000002633e7a7^_<8B>^H^@^@^@^@^@^@^C2^L
<F6>q<F1>=^^<FD>Y<92>^A^H<96>=7Vc<80>^@&^@^@^@^@<FF><FF>^C^@=<EF>R<DF>^Y^@^@^@^P^A^@^@^@^@^@^@^@H^@^@^@^@T<EE><FB>
^E^@^@^@^@^@ESC^@c^@c<81><80>.fseventsd/000000002633e7a8^_<8B>^H^@^@^@^@^@^@^C2^L<F6>q<89>^K<8B>^R<B5>b``<D0>K+N-K
<CD>+)N<D1>/<CE><D1>M<CE><CF>-H,aX<FE><DC>X<8D>^A^B^X^YV 8<8A>^@^@^@^@<FF><FF>^C^@<BA><ED>^WH:^@^@^@^P^A^@^@^@^@^@^@^@
$^@^@^@^@T<EE><FB>^E^@^@^@^@^@^Y^@c^@c<81><80>.fseventsd/fseventsd-uuid8076230C-C685-4C73-88B5-DF66AEB7E122^P^A^@^@^@^@^@^@
^E^E^@^@^@^@T<EE><F4><D7>^@^@^@ ^@6^@^@^@P<81><B4>Applications/AACredentialRecoveryDialog.app/Info.plistbplist00<DF>^P^]^A^B^C^D^E^F^G^H      
^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^ZESC^\^]^^^_ !#$%&()%*+-.012789:9<^^=>?B\CFBundleNameYDTSDKNameWDTXcodeYSBAppTagsZDTSDKBuild_^P^YCFBundleDevelopmentRegion_^P^OCFBundleVersion_^P^QUIBackgroundModes_^P^SBuildMachineOSBuild^DTPlatformName_^P^ZCFBundleShortVersionString_^P^SCFBundlePackageType_^P^ZCFBundleSupportedPlatforms_^P^]CFBundleInfoDictionaryVersion_^P^\UIRequiredDeviceCapabilities_^P^RCFBundleExecutableZDTCompiler_^P%UISupportedInterfaceOrientations~ipad_^P^RCFBundleIdentifier_^P^PMinimumOSVersion_^P^Y_UILaunchAlwaysFullScreen\DTXcodeBuild_^P^RLSRequiresIPhoneOS_^P UISupportedInterfaceOrientations_^P^SCFBundleDisplayName_^P^QDTPlatformVersion_^P^QCFBundleSignature^UIDeviceFamily_^P^ODTPlatformBuild_^P^ZAACredentialRecoveryDialog_^P^Tiphoneos8.2.internalT0610<A1>"VhiddenV12D432RenS1.0<A1>'ZcontinuousV13A603XiphoneosTAPPL<A1>,XiPhoneOSS6.0<A1>/Uarmv7_^P^ZAACredentialRecoveryDialog_^P"com.apple.compilers.llvm.clang.1_0<A4>3456_^P^^UIInterfaceOrientationPortrait_^P(UIInterfaceOrientationPortraitUpsideDown_^P#UIInterfaceOrientationLandscapeLeft_^P$UIInterfaceOrientationLandscapeRight_^P1com.apple.appleaccount.AACredentialRecoveryDialogS8.2    W6A1052c        
<A3>356S8.2T????<A2>@A^P^A^P^BP^@^H^@E^@R^@\^@d^@n^@y^@<95>^@<A7>^@<BB>^@<D1>^@<E0>^@<FD>^A^S^A0^AP^Ao^A<84>^A<8F>^A<B7>^A<CC>^A
<DF>^A<FB>^B^H^B^]^B@^BV^Bj^B~^B<8D>^B<9F>^B<BC>^B<D3>^B<D8>^B<DA>^B<E1>^B<E8>^B<EB>^B<EF>^B<F1>^B<FC>^C^C^C^L^C^Q^C^S^C^\^C ^C"^C(^CE^Cj^Co^C<90>^C<BB>^C<E1>^D^H^D<^D@^DA^DI^DJ^DN^DR^DW^DZ^D\^D^^@^@^@^@^@^@^B^A^@^@^@^@^@^@^@C^@^@^@^@^@^@^@^@^@^@^@^@^@^@^D_^P^A
^@^@^@^@^@^@^L<D9>^@^@^@^@T<EE><F4><D7>^@^@^@ ^@H^@^@^@P<81><B4>Applications/AACredentialRecoveryDialog.app/_CodeSignature/CodeResources<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
...

It would make sense if this were a cpio or (Apple's favorite) pax archive, but it's apparently neither (pax actually uses cpio internally, and complains about name lengths being out of range when fed this). Some of my readers are reverse-engineers, so this makes for a nice (and easy) exercise in reversing. Observe the following:

Using od makes it a bit easier to figure things out - specifically, where the file contents are raw XML and not a binary plist:

0000690    00  00  00  00  00  00  00  00  00  00  04  5f  10  01  00  00
          \0  \0  \0  \0  \0  \0  \0  \0  \0  \0 004   _ 020 001  \0  \0
                           |-- file size--|
00006a0    00  00  00  00  0c  d9  00  00  00  00  54  ee  f4  d7  00  00
          \0  \0  \0  \0  \f 331  \0  \0  \0  \0   T 356 364 327  \0  \0
                   |Nlen|  |uid |  |gid |  |perms|  |---Name starts here--
00006b0    00  20  00  48  00  00  00  50  81  b4  41  70  70  6c  69  63
          \0      \0   H  \0  \0  \0   P 201 264   A   p   p   l   i   c
00006c0    61  74  69  6f  6e  73  2f  41  41  43  72  65  64  65  6e  74
           a   t   i   o   n   s   /   A   A   C   r   e   d   e   n   t
00006d0    69  61  6c  52  65  63  6f  76  65  72  79  44  69  61  6c  6f
           i   a   l   R   e   c   o   v   e   r   y   D   i   a   l   o
00006e0    67  2e  61  70  70  2f  5f  43  6f  64  65  53  69  67  6e  61
           g   .   a   p   p   /   _   C   o   d   e   S   i   g   n   a
00006f0    74  75  72  65  2f  43  6f  64  65  52  65  73  6f  75  72  63
           t   u   r   e   /   C   o   d   e   R   e   s   o   u   r   c
           end (48) |-------------------------------File Contents -------
0000700    65  73  3c  3f  78  6d  6c  20  76  65  72  73  69  6f  6e  3d
           e   s   <   ?   x   m   l       v   e   r   s   i   o   n   =
0000710    22  31  2e  30  22  20  65  6e  63  6f  64  69  6e  67  3d  22
           "   1   .   0   "       e   n   c   o   d   i   n   g   =   "
0000720    55  54  46  2d  38  22  3f  3e  0a  3c  21  44  4f  43  54  59
           U   T   F   -   8   "   ?   >  \n   <   !   D   O   C   T   Y
...
00013c0    0a  09  3c  2f  64  69  63  74  3e  0a  3c  2f  64  69  63  74
          \n  \t   <   /   d   i   c   t   >  \n   <   /   d   i   c   t
00013d0    3e  0a  3c  2f  70  6c  69  73  74  3e  0a  10  01  00  00  00
           >  \n   <   /   p   l   i   s   t   >   \n 020 001  \0  \0  \0
                             Contents End here-----|   |-----------|
            --------| |--file size--|
00013e0    00  00  00 09  5a  00  00  00  00  54  ee  f4  c6  00  00  00
                   |--namelen---|  .........   |------- Name Starts here
00013f0    20  00  37  00  00  00  50  81  b4  41  70  70  6c  69  63  61
              \0   7  \0  \0  \0   P 201 264   A   p   p   l   i   c   a
0001400    74  69  6f  6e  73  2f  41  63  63  6f  75  6e  74  41  75  74
           t   i   o   n   s   /   A   c   c   o   u   n   t   A   u   t
0001410    68  65  6e  74  69  63  61  74  69  6f  6e  44  69  61  6c  6f
           h   e   n   t   i   c   a   t   i   o   n   D   i   a   l   o
0001420    67  2e  61  70  70  2f  49  6e  66  6f  2e  70  6c  69  73  74
            g   .   a   p   p   /   I   n   f   o   .   p   l   i   s   t
				------- end ends here  (0x37) ----------|
0001430    3c  3f  78  6d  6c  20  76  65  72  73  69  6f  6e  3d  22  31
           <   ?   x   m   l       v   e   r   s   i   o   n   =   "   1
...                                                |------------|  |----|
0001d80    0a  3c  2f  70  6c  69  73  74  3e  0a  10  01  00  00  00  00
          \n   <   /   p   l   i   s   t   >  \n  020 001  \0  \0  \0  \0
           |-- file size-|        |---------------------------|   |----|
0001d90    00  00  0b  3c  00  00  00  00  54  ee  f4  c6  00  00  00  20
           |Nlen|  | uid|  | gid|  |perms|  |--- Name starts here
0001da0    00  49  00  00  00  50  81  b4  41  70  70  6c  69  63  61  74
          \0   I  \0  \0  \0   P 201 264   A   p   p   l   i   c   a   t
0001db0    69  6f  6e  73  2f  41  63  63  6f  75  6e  74  41  75  74  68
           i   o   n   s   /   A   c   c   o   u   n   t   A   u   t   h
0001dc0    65  6e  74  69  63  61  74  69  6f  6e  44  69  61  6c  6f  67
           e   n   t   i   c   a   t   i   o   n   D   i   a   l   o   g
0001dd0    2e  61  70  70  2f  5f  43  6f  64  65  53  69  67  6e  61  74
           .   a   p   p   /   _   C   o   d   e   S   i   g   n   a   t
0001de0    75  72  65  2f  43  6f  64  65  52  65  73  6f  75  72  63  65
           u   r   e   /   C   o   d   e   R   e   s   o   u   r   c   e
0001df0    73  3c  3f  78  6d  6c  20  76  65  72  73  69  6f  6e  3d  22
           s   <   ?   x   m   l       v   e   r   s   i   o   n   =   "
0001e00    31  2e  30  22  20  65  6e  63  6f  64  69  6e  67  3d  22  55

How does the file format fall apart so quickly? Well, in this case, it's possible to cheat, using lsbom(1):

morpheus@Zephyr(/tmp/OTA)$ lsbom ../post.bom | grep AccountAuthenticationDialog.app/_CodeSignature/CodeReso
./Applications/AccountAuthenticationDialog.app/_CodeSignature/CodeResources	100664	0/80	2876	3005659373
morpheus@Zephyr(/tmp/OTA)$ printf "%x\n" 0100664
81b4

(If you're wondering why the endian-ness seems off - thank PPC)

If you just want the struct, it's something like:

#pragma pack(1)
struct entry
{ 
 // Don't care about most of the fields here, really.
 // N.B : use ntohl/ntohs on the fields (or swap16/32) to 
 //       correct for legacy PPC endian-ness nobody will 
 //       ever use nor care about, but Apple still supports
 unsigned int usually_0x10_01_00_00;
 unsigned short  usually_0x00_00; //_00_00; 
 unsigned int  fileSize;  // But we need this .. 
 unsigned short whatever;
 unsigned long long timestamp_likely;
 unsigned short _usually_0x20_yada_yada;
 unsigned short nameLen; // and this..
 // And we don't need these, though I have them figured out.
 unsigned short uid;     
 unsigned short gid;
 unsigned short perms;

 char name[0]; // nameLen bytes here..
 // Followed by file contents
};

and you can get the source of my OTA payload extractor (that is, past the pbxz and the xz) right here. Using it, it super simple:
Zephyr:payloadv2 morpheus$ curl -download http://newosxbook.com/code/listings/ota.c > otaa.c
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4185  100  4178  100     7  63171    105 --:--:-- --:--:-- --:--:-- 63303
Zephyr:payloadv2 morpheus$ cc otaa.c -o otaa
Zephyr:xx morpheus$ mkdir xx; cd xx
Zephyr:xx morpheus$ ../otaa -e '*' ../p
Got anchor
Entry @0x84: UID: 0 GID: 80 Size: 1285 (0x505) Namelen: 54 Name: Applications/AACredentialRecoveryDialog.app/Info.plist
Entry @0x1471: UID: 0 GID: 80 Size: 3289 (0xcd9) Namelen: 72 Name: Applications/AACredentialRecoveryDialog.app/_CodeSignature/CodeResources
Entry @0x4845: UID: 0 GID: 80 Size: 2394 (0x95a) Namelen: 55 Name: Applications/AccountAuthenticationDialog.app/Info.plist
Entry @0x7342: UID: 0 GID: 80 Size: 2876 (0xb3c) Namelen: 73 Name: Applications/AccountAuthenticationDialog.app/_CodeSignature/CodeResources
Entry @0x10283: UID: 0 GID: 80 Size: 1732 (0x6c4) Namelen: 35 Name: Applications/AdSheet.app/Info.plist
...
Entry @0x1310821791: UID: 0 GID: 0 Size: 57 (0x39) Namelen: 42 Name: usr/share/zoneinfo.default/Pacific/Pohnpei
Entry @0x1310821928: UID: 0 GID: 0 Size: 14960 (0x3a70) Namelen: 50 Name: usr/standalone/firmware/oscar/memtest-oscar2.image
# Make sure the extraction makes sense
Zephyr:xx morpheus$ file Applications/AACredentialRecoveryDialog.app/Info.plist
Applications/AACredentialRecoveryDialog.app/Info.plist: Apple binary property list # Yep :-)
#
# and, the two most important files, boys and girls:
Zephyr:xx morpheus$ ../otaa -v -e 'dyld' ../p
Got anchor
Dumping 507404290 bytes to System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64
Dumping 416062019 bytes to System/Library/Caches/com.apple.dyld/dyld_shared_cache_armv7s
# Use Jtool to list, extract dylibs from, or otherwise manipulate shared cache
Zephyr:xx morpheus$ jtool  -v -l System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 
File is a shared cache containing 921 images
mapping ??  309MB        180000000 ->        193510000
mapping ??   63MB        193510000 ->        197494000
mapping ??   47MB        197494000 ->        19a480000
   0:        180008000 /System/Library/AccessibilityBundles/AXSpeechImplementation.bundle/AXSpeechImplementation
   1:        180010000 /System/Library/AccessibilityBundles/AccessibilitySettingsLoader.bundle/AccessibilitySettingsLoader
   2:        18001c000 /System/Library/AccessibilityBundles/AccountsUI.axbundle/AccountsUI
   3:        180020000 /System/Library/AccessibilityBundles/AddressBookUIFramework.axbundle/AddressBookUIFramework
...

The code is quick and dirty (I haven't removed the anchor, etc), but - it works. And it can extract all the files from the update payload (and you can patch the rest). Happy reversing :-)

And..

This is only a part of the picture, but it's the important part. I'll cover mobileassetd and friends in the 2nd ed. So that's all for now. As usual, comments, questions, flames can go to j@.... And I'm still looking for more topics for the 2nd Edition At the Website Forum. Also, check out the RSS Feed for updates, or follow @Technologeeks for more. You might want to consider the Training we offer, especially OS X/iOS Reversing.


Update: QnA on the NewOSXBook Forum as per Reddit thread.
Other articles in the series:
II
III
Episode IV
Episode V
Episode VI
Episode VII
Episode VIII (the conclusion)