├── .gitignore ├── README.md └── src ├── LICENSE.txt ├── Makefile └── ota2tar.c /.gitignore: -------------------------------------------------------------------------------- 1 | ota2tar 2 | *.o 3 | ref/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ota2tar 2 | 3 | Parses an Apple iOS OTA file and converts it to tar. 4 | 5 | Only the Format 3.0 described [here](https://www.theiphonewiki.com/wiki/OTA_Updates) is supported. 6 | 7 | This code is based heavily on Jonathan Levin's example code from his [blog article on the OTA file format](http://newosxbook.com/articles/OTA.html). 8 | 9 | ## Dependencies: 10 | 11 | - libarchive (a "recent-ish version"?) - I recommend installing it via homebrew on OS X 12 | 13 | 14 | ## Building: 15 | 16 | Run: 17 | 18 | cd src 19 | make ota2tar 20 | 21 | On linux, you may want to tweak src/Makefile a bit depending on where and how you got libarchive. 22 | 23 | ## Usage: 24 | 25 | Usage: ./ota2tar [?hvkzEo:] /path/to/ota/payload 26 | Options: 27 | -?/-h Show this help message 28 | -v Verbose output 29 | -k Keep intermediate payload.ota file 30 | -z Compress the resulting tar-ball 31 | -E Extract executables only 32 | -o path Output to path 33 | 34 | -------------------------------------------------------------------------------- /src/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Eric Monti 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | 2 | CC=cc 3 | 4 | # These are here for OS X assuming libarchive is installed via homebrew. 5 | # You may want to remove or tweak them on Linux 6 | CFLAGS=-I/usr/local/opt/libarchive/include 7 | LDFLAGS=-L/usr/local/opt/libarchive/lib 8 | 9 | ota2tar: ota2tar.o 10 | $(CC) $(LDFLAGS) -o $@ $^ -larchive 11 | 12 | 13 | clean: 14 | rm -f ota2tar *.o 15 | -------------------------------------------------------------------------------- /src/ota2tar.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Eric Monti 2 | // 3 | // ota2tar 4 | // 5 | // Parses an Apple iOS OTA file and converts it to tar 6 | // 7 | // Only the Format 3.0 described here is supported: 8 | // https://www.theiphonewiki.com/wiki/OTA_Updates 9 | // 10 | // This code is based heavily on Jonathan Levin's example code 11 | // found here: http://newosxbook.com/articles/OTA.html 12 | // 13 | // Dependencies: 14 | // libarchive (a "recent-ish version"?) 15 | // 16 | // Credit: 17 | // 18 | // Thanks to Jonathan Levin for his awesome series of posts about 19 | // the OTA format. 20 | // 21 | // License: 22 | // See LICENSE.TXT 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include 38 | #include 39 | 40 | #define PBZX_MAGIC ((uint32_t)0x70627a78) 41 | #define XZ_HEAD_MAGIC ("\xfd""7zXZ\x00") 42 | #define XZ_TAIL_MAGIC ("YZ") 43 | 44 | const char *cmdlineopts = "?hvkzEo:"; 45 | bool g_verbose = false; 46 | bool g_executables = false; 47 | 48 | #define error_msg(fmt, msg...) fprintf(stderr, "!!! Error (%s): " fmt, __func__, ##msg) 49 | #define warn_msg(fmt, msg...) fprintf(stderr, "... Warning (%s): " fmt, __func__, ##msg) 50 | #define verbose_msg(fmt, msg...) if (g_verbose) { printf("... " fmt, ##msg); } 51 | 52 | static void usage(const char *progname) 53 | { 54 | fprintf(stderr, "Usage: %s [%s] /path/to/ota/payload\n", progname, cmdlineopts); 55 | fprintf(stderr, " Options:\n"); 56 | fprintf(stderr, " -?/-h Show this help message\n"); 57 | fprintf(stderr, " -v Verbose output\n"); 58 | fprintf(stderr, " -k Keep intermediate payload.ota file\n"); 59 | fprintf(stderr, " -z Compress the resulting tar-ball\n"); 60 | fprintf(stderr, " -E Extract executables only\n"); 61 | fprintf(stderr, " -o path Output to path\n"); 62 | } 63 | 64 | struct ota_entry 65 | { 66 | uint8_t unk1[6]; // usually 10 01 00 00 00 00 67 | uint32_t fileSize; 68 | uint32_t unk2; 69 | uint32_t timestamp; 70 | uint32_t unk3; 71 | uint16_t namelen; 72 | uint16_t uid; 73 | uint16_t gid; 74 | uint16_t perms; 75 | 76 | char name[0]; 77 | // Followed by file contents 78 | } __attribute__((packed)); 79 | 80 | #define DYLD_SHARED_CACHE_CMP "System/Library/Caches/com.apple.dyld/dyld_shared_cache" 81 | 82 | 83 | bool is_executable(const char *name, void* filedata, size_t filesize) 84 | { 85 | if (memcmp(name, DYLD_SHARED_CACHE_CMP, sizeof(DYLD_SHARED_CACHE_CMP)-1) == 0) { 86 | return true; 87 | } 88 | 89 | if (filesize < sizeof(uint32_t)) { 90 | return false; 91 | } 92 | 93 | uint32_t magic = *(uint32_t*)filedata; 94 | return ( (magic & 0xFFFFFFF0) == 0xFEEDFAC0 || (magic & 0xF0FFFFFF) == 0xC0FAEDFE || magic == 0xCAFEBABE || magic == 0xBEBAFECA ); 95 | } 96 | 97 | 98 | static bool add_to_archive(const struct ota_entry *ota_ent, struct archive *ar_out) 99 | { 100 | uint16_t namelen = ntohs(ota_ent->namelen); 101 | char name[PATH_MAX + 1]; 102 | if (namelen > PATH_MAX) { 103 | error_msg("Filename too large\n"); 104 | return false; 105 | } 106 | 107 | memcpy(name, ota_ent->name, namelen); 108 | name[namelen] = 0; 109 | 110 | mode_t mode = (mode_t)ntohs(ota_ent->perms); 111 | uint32_t timestamp = ntohl(ota_ent->timestamp); 112 | size_t filesize = ntohl(ota_ent->fileSize); 113 | 114 | uid_t uid = ntohs(ota_ent->uid); 115 | gid_t gid = ntohs(ota_ent->gid); 116 | uint8_t *filedata = (void *)ota_ent + sizeof(struct ota_entry) + namelen; 117 | 118 | if (g_executables) { 119 | if (!is_executable(name, filedata, filesize)) { 120 | verbose_msg("Skipping %s - not a mach-o file\n", name); 121 | return true; 122 | } 123 | } 124 | 125 | verbose_msg("Archiving %s (filesize:%zu mode:0%o uid:%u gid:%u timestamp:%u)\n", name, filesize, mode, uid, gid, timestamp); 126 | 127 | struct archive_entry *ar_ent = archive_entry_new(); 128 | assert (ar_ent != NULL); 129 | 130 | archive_entry_set_pathname(ar_ent, name); 131 | archive_entry_set_filetype(ar_ent, (mode & S_IFMT)); 132 | archive_entry_set_perm(ar_ent, (mode & (~S_IFMT))); 133 | archive_entry_set_size(ar_ent, filesize); 134 | archive_entry_set_uid(ar_ent, uid); 135 | archive_entry_set_gid(ar_ent, gid); 136 | 137 | archive_entry_set_atime(ar_ent, timestamp, 0); 138 | archive_entry_set_birthtime(ar_ent, timestamp, 0); 139 | archive_entry_set_ctime(ar_ent, timestamp, 0); 140 | archive_entry_set_mtime(ar_ent, timestamp, 0); 141 | 142 | int r = archive_write_header(ar_out, ar_ent); 143 | archive_entry_free(ar_ent); 144 | 145 | if (r != ARCHIVE_OK) { 146 | error_msg("archive_write_header error %s - %s\n", name, archive_error_string(ar_out)); 147 | return false; 148 | } 149 | 150 | 151 | if (S_ISLNK(mode)) { 152 | // For symlinks, the link target is stored where file contents would normally go 153 | char link_name[PATH_MAX + 1]; 154 | assert(filesize < PATH_MAX); 155 | if (filesize > PATH_MAX) { 156 | error_msg("Link target filename for %s too large: %zu\n", name, filesize); 157 | return false; 158 | } 159 | 160 | memcpy(link_name, filedata, filesize); 161 | link_name[filesize] = 0; 162 | 163 | } else if (filesize != 0) { 164 | ssize_t written = archive_write_data(ar_out, filedata, filesize); 165 | if (written != filesize) { 166 | error_msg("archive_write_data error %s (%zi != %zu) %s\n", name, written, filesize, archive_error_string(ar_out)); 167 | return false; 168 | } 169 | } 170 | 171 | return true; 172 | } 173 | 174 | 175 | static bool extract_ota(const uint8_t *chunk, size_t chunk_size, struct archive *ar_out) 176 | { 177 | const uint8_t *p = chunk; 178 | ssize_t chunk_left = chunk_size; 179 | 180 | while (chunk_left > 0) { 181 | if (chunk_left < sizeof(struct ota_entry)) { 182 | error_msg("Entry out of bounds (chunk_left=%zx > sizeof(struct ota_entry)\n", chunk_left); 183 | return false; 184 | } 185 | 186 | const struct ota_entry *ent = (const struct ota_entry *)p; 187 | uint16_t namelen = ntohs(ent->namelen); 188 | size_t filesize = ntohl(ent->fileSize); 189 | 190 | size_t ent_size = sizeof(struct ota_entry) + namelen + filesize; 191 | if (ent_size > chunk_left) { 192 | error_msg("Entry out of bounds (ent_size=%zx > chunk_left=%zx)\n", ent_size, chunk_left); 193 | return false; 194 | } 195 | 196 | if (! add_to_archive(ent, ar_out)) { 197 | error_msg("Archive error - quitting\n"); 198 | return false; 199 | } 200 | 201 | chunk_left -= ent_size; 202 | p += ent_size; 203 | } 204 | 205 | return true; 206 | } 207 | 208 | 209 | static bool decompress_xz(const void *buf, uint64_t length, int outfd) 210 | { 211 | if (length > (size_t)-2) { 212 | error_msg("Compressed chunk is too big\n"); 213 | return false; 214 | } 215 | 216 | struct archive *ar = archive_read_new(); 217 | assert(ar != NULL); 218 | 219 | #if ARCHIVE_VERSION_NUMBER < 3000000 220 | archive_read_support_compression_xz(ar); 221 | #else 222 | archive_read_support_filter_xz(ar); 223 | #endif 224 | 225 | archive_read_support_format_empty(ar); 226 | archive_read_support_format_raw(ar); 227 | 228 | bool ret = false; 229 | 230 | if (archive_read_open_memory(ar, (void*)buf, length) == ARCHIVE_OK) { 231 | struct archive_entry *ae; 232 | int r = archive_read_next_header(ar, &ae); 233 | if (r == ARCHIVE_OK) { 234 | size_t bufsize = 0x8000; 235 | uint8_t buf[bufsize]; 236 | for (;;) { 237 | ssize_t readsz = archive_read_data(ar, buf, bufsize); 238 | 239 | if (readsz < 0) { 240 | error_msg("archive_read_data error - %s\n", archive_error_string(ar)); 241 | break; 242 | } else if (bufsize < readsz) { 243 | error_msg("bad size returned by archive_read_data - %zi\n", readsz); 244 | break; 245 | } else if (readsz == 0) { 246 | // read finished 247 | ret = true; 248 | break; 249 | } else { 250 | write(outfd, buf, readsz); 251 | } 252 | } 253 | } else if (r == ARCHIVE_EOF) { 254 | error_msg("Empty archive entry?\n"); 255 | } 256 | archive_read_close(ar); 257 | } else { 258 | error_msg("archive_read_open_memory error - %s\n", archive_error_string(ar)); 259 | 260 | } 261 | 262 | 263 | if (!ret) { 264 | } 265 | return ret; 266 | } 267 | 268 | 269 | static bool extract_pbzx(const void *pbzx_data, size_t pbzx_size, int outfd) 270 | { 271 | if (pbzx_size < 32) { 272 | error_msg("Input is too small to be a pbzx\n"); 273 | return false; 274 | } 275 | 276 | uint32_t magic = ntohl( *((uint32_t*)pbzx_data) ); // XXX assumes little-endian 277 | 278 | if (magic != PBZX_MAGIC) { 279 | error_msg("Invalid magic value 0x%0.8x\n", magic); 280 | return false; 281 | } 282 | 283 | const void *p = pbzx_data + 4; 284 | const void *file_end = pbzx_data + pbzx_size; 285 | 286 | uint64_t flags = __builtin_bswap64(*((uint64_t*)p)); // one flag before all the chunks 287 | p += sizeof(uint64_t); 288 | 289 | int chunk_idx = 0; 290 | 291 | while(p < file_end) { // have more chunks 292 | if (20 > file_end-p) { 293 | error_msg("Reached premature end of file\n"); 294 | return false; 295 | } 296 | chunk_idx++; 297 | 298 | flags = __builtin_bswap64(*((uint64_t*)p)); 299 | p += sizeof(uint64_t); 300 | 301 | uint64_t length = __builtin_bswap64(*((uint64_t*)p)); 302 | p += sizeof(uint64_t); 303 | 304 | verbose_msg("Processing PBZX Chunk #%d @0x%zx (flags: %llx, length: %lld bytes)\n", 305 | chunk_idx, (p - pbzx_data), flags, length); 306 | 307 | if (length > (file_end - p)) { 308 | error_msg("Chunk length out of bounds: %lld bytes?", length); 309 | } 310 | 311 | bool is_xz_chunk = (memcmp(p, XZ_HEAD_MAGIC, sizeof(XZ_HEAD_MAGIC)-1) == 0); 312 | if (is_xz_chunk) { 313 | const void *xz_tail = (p + length) - 2; 314 | if (memcmp(xz_tail, XZ_TAIL_MAGIC, sizeof(XZ_TAIL_MAGIC)-1) != 0) { 315 | error_msg("Expected XZ trailer magic at offset 0x%zx\n", (xz_tail - pbzx_data)); 316 | return false; 317 | } 318 | 319 | if (!decompress_xz(p, length, outfd)) { 320 | return false; 321 | } 322 | } else { 323 | verbose_msg("Non-XZ chunk detected #%d @0x%zx (written as is to file)\n", chunk_idx, (p - pbzx_data)); 324 | uint64_t written = 0; 325 | while (written < length) { 326 | size_t wlen = ((length-written) < SIZE_T_MAX)? (length-written) : SIZE_T_MAX; 327 | ssize_t n = write(outfd, (p+written), wlen); 328 | assert(n > 0); 329 | written += n; 330 | } 331 | } 332 | p += length; 333 | } 334 | 335 | if (p == file_end) { 336 | printf("PBZX Parsing Complete\n"); 337 | return true; 338 | } else { 339 | error_msg("Parse failed early at file offset 0x%zx\n", (p - pbzx_data)); 340 | return false; 341 | } 342 | } 343 | 344 | 345 | int main(int argc, char * argv[]) 346 | { 347 | const char *progname = argv[0]; 348 | 349 | bool compress = false; 350 | bool keep = false; 351 | const char *ar_ext = "tar"; 352 | char *ar_outfile = NULL; 353 | 354 | int ch; 355 | while ((ch = getopt(argc, argv, cmdlineopts)) != -1) { 356 | switch (ch) { 357 | case 'h': 358 | case '?': 359 | { 360 | usage(progname); 361 | return EXIT_SUCCESS; 362 | break; 363 | } 364 | 365 | case 'o': 366 | { 367 | ar_outfile = optarg; 368 | break; 369 | } 370 | 371 | case 'z': 372 | { 373 | compress = true; 374 | ar_ext = "tar.bz2"; 375 | break; 376 | } 377 | 378 | case 'k': 379 | { 380 | keep = true; 381 | break; 382 | } 383 | 384 | case 'v': 385 | { 386 | g_verbose = true; 387 | break; 388 | } 389 | 390 | case 'E': 391 | { 392 | g_executables = true; 393 | break; 394 | } 395 | 396 | default: 397 | { 398 | usage(progname); 399 | return EXIT_FAILURE; 400 | break; 401 | } 402 | } 403 | } 404 | 405 | argc -= optind; 406 | argv += optind; 407 | 408 | if (argc < 1) { 409 | usage(progname); 410 | return EXIT_FAILURE; 411 | } 412 | 413 | char infile[PATH_MAX + 1]; 414 | if (!realpath(argv[0], infile)) { 415 | error_msg("realpath error - %s\n", strerror(errno)); 416 | return EXIT_FAILURE; 417 | } 418 | 419 | // we need a little extra pathname room for extensions 420 | if (strlen(infile) > (PATH_MAX - 10)) { 421 | error_msg("Filename too long\n"); 422 | return EXIT_FAILURE; 423 | } 424 | 425 | char ota_outfile[PATH_MAX + 1]; 426 | 427 | snprintf(ota_outfile, PATH_MAX, "%s.ota", infile); 428 | if (!ar_outfile) { 429 | ar_outfile = alloca(PATH_MAX + 1); 430 | assert(ar_outfile != NULL); 431 | snprintf(ar_outfile, PATH_MAX, "%s.%s", infile, ar_ext); 432 | } 433 | 434 | bool ota_done = false; 435 | bool pbzx_done = false; 436 | 437 | int ota_fd; 438 | 439 | bool our_pbzx = false; 440 | if (access(ota_outfile, F_OK) == 0) { 441 | printf("*** PBZX appears to have already been extracted. Using: %s\n", ota_outfile); 442 | ota_fd = open(ota_outfile, O_RDONLY); 443 | pbzx_done = true; 444 | } else { 445 | printf("*** Extracting PBZX payload file %s -> %s\n", infile, ota_outfile); 446 | ota_fd = open(ota_outfile, (O_RDWR | O_TRUNC | O_CREAT), 0600); 447 | our_pbzx = true; 448 | } 449 | 450 | if (ota_fd < 0) { 451 | error_msg("Unable to open file: %s - %s\n", ota_outfile, strerror(errno)); 452 | return EXIT_FAILURE; 453 | } 454 | 455 | if (!pbzx_done) { 456 | int pbzx_fd = open(infile, O_RDONLY); 457 | if (pbzx_fd >= 0) { 458 | struct stat st; 459 | if (fstat(pbzx_fd, &st) == 0) { 460 | if (S_ISREG(st.st_mode)) { 461 | void *pbzx_data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, pbzx_fd, 0); 462 | if (pbzx_data) { 463 | pbzx_done = extract_pbzx(pbzx_data, st.st_size, ota_fd); 464 | munmap(pbzx_data, st.st_size); 465 | } else { 466 | error_msg("mmap failed: %s\n", strerror(errno)); 467 | } 468 | } else { 469 | error_msg("Not a file: %s\n", infile); 470 | } 471 | } else { 472 | error_msg("Unable to fstat %s - %s\n", infile, strerror(errno)); 473 | } 474 | close(pbzx_fd); 475 | } else { 476 | error_msg("Unable to open %s - %s\n", infile, strerror(errno)); 477 | } 478 | } 479 | 480 | if (pbzx_done) { 481 | struct archive *ar_out = archive_write_new(); 482 | assert(ar_out != NULL); 483 | assert(archive_write_set_format_gnutar(ar_out) == ARCHIVE_OK); 484 | 485 | if (compress) { 486 | #if ARCHIVE_VERSION_NUMBER < 3000000 487 | assert(archive_write_set_compression_bzip2(ar_out) == ARCHIVE_OK); 488 | #else 489 | assert(archive_write_add_filter_bzip2(ar_out) == ARCHIVE_OK); 490 | #endif 491 | } 492 | 493 | if (archive_write_open_filename(ar_out, ar_outfile) == ARCHIVE_OK) { 494 | printf("*** Converting OTA archive to tarball %s -> %s\n", ota_outfile, ar_outfile); 495 | struct stat st; 496 | if (fstat(ota_fd, &st) == 0) { 497 | if (S_ISREG(st.st_mode)) { 498 | void *ota_data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, ota_fd, 0); 499 | if (ota_data) { 500 | ota_done = extract_ota(ota_data, st.st_size, ar_out); 501 | munmap(ota_data, st.st_size); 502 | } else { 503 | error_msg("mmap failed: %s\n", strerror(errno)); 504 | } 505 | } else { 506 | error_msg("Not a file: %s\n", ota_outfile); 507 | } 508 | } else { 509 | error_msg("Unable to fstat %s - %s\n", ota_outfile, strerror(errno)); 510 | } 511 | } else { 512 | error_msg("Unable to create archive %s - %s\n", ar_outfile, archive_error_string(ar_out)); 513 | } 514 | 515 | printf("*** Finished %s writing %s\n", (ota_done ? "successfulliy" : "with errors"), ar_outfile); 516 | archive_write_close(ar_out); 517 | } 518 | 519 | close(ota_fd); 520 | 521 | if (!keep && our_pbzx) { 522 | verbose_msg("Cleaning up %s\n", ota_outfile); 523 | unlink(ota_outfile); 524 | } else { 525 | verbose_msg("Leaving %s\n", ota_outfile); 526 | } 527 | 528 | return (pbzx_done && ota_done) ? EXIT_SUCCESS : EXIT_FAILURE; 529 | } 530 | --------------------------------------------------------------------------------