#!/usr/bin/php * @category Utility * @filesource * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License * @package interface * @uses Console_Getopt * @uses Console_Color * @version $Id$ (Used by SVN to automatically stamp files with their version #) * @copyright 2007, Carlos Averett */ // Pear require_once ('Pear/PEAR.php'); require_once ('Pear/Console/Getargs.php'); require_once ('Pear/Console/Color.php'); // Register command line options $config = array ( 'extract' => array ( 'short' => 'e', 'min' => 0, 'max' => 0, 'desc' => 'Extract files from [indir]' ), 'compress' => array ( 'short' => 'c', 'min' => 0, 'max' => 0, 'desc' => 'Compress files from [indir] into [outdir]' ), 'indir' => array ( 'short' => 'i', 'min' => 1, 'max' => 1, 'desc' => 'Specify the input directory for compressing or extracting', ), 'outdir' => array ( 'short' => 'o', 'min' => 1, 'max' => 1, 'desc' => 'Specify the output directory for compressing or extracting', ), 'license' => array ( 'short' => 'l', 'max' => 0, 'desc' => 'Display the license for this program' ), 'obscure' => array ( 'short' => 'b', 'max' => 0, 'desc' => 'Obscure identifying information in output.' ), 'help' => array ( 'short' => 'h', 'max' => 0, 'desc' => 'Display this help' ) ); // ===== // Handle license seperately // ===== foreach ($_SERVER['argv'] as $arg) { if ($arg == '-l' || $arg == '--license') { // Dump a copy of the LGPL to stdin exit (file_get_contents ('LICENSE')); } } $cmd = $_SERVER['argv'][0]; $args =& Console_Getargs::factory ($config); $header = Console_Color::convert ( "%riPhone Extraction Utility%n\n" . "%r=========================%n\n" . "Example: %p$cmd%n %g--extract%n %g--outdir=%n%b\"out\"%n \\\n" . " %g--indir=%n%b\"~/Library/Application Support/MobileSync/Backup/*\"%n \\\n" . "\n%rOptions:%n\n" ); if (PEAR::isError($args)) { if ($args->getCode() === CONSOLE_GETARGS_ERROR_USER) { echo Console_Getargs::getHelp($config, $header, $args->getMessage()) . "\n"; } else if ($args->getCode() === CONSOLE_GETARGS_HELP) { echo Console_Getargs::getHelp($config, $header)."\n"; } exit (); } // Sanity checking $indir = $args->getValue ('indir'); $outdir = $args->getValue ('outdir'); $compress = $args->getValue ('compress'); $extract = $args->getValue ('extract'); $obscure = $args->getValue ('obscure'); $noError = false; if (!is_dir ($indir)) { usage_error ('Invalid indir.'); } else if (!is_dir ($outdir)) { usage_error ('Invalid indir.'); } else if ((!$compress && !$extract) || ($compress && $extract)) { usage_error ('You must either select compression or extraction.'); } else if ($indir == $outdir) { usage_error ('Indir and outdir must differ.'); } else { $noerror = true; } if (!$noerror) { exit(); } // ===== // Meat of the program // ===== // Make sure we always have the directory with a '/' on the end $indir = dirname ($indir . '/removeme.txt') . '/'; $outdir = dirname ($outdir . '/removeme.txt') . '/'; if ($extract) { if (!file_exists ($indir . 'Manifest.plist')) { fatal_error ('Unable to read Manifest'); } else if (!file_exists ($indir . 'Info.plist')) { fatal_error ('Unable to read Info.plist'); } // === // Info.plist // === $info_file = $outdir . 'Info.plist'; file_write ($info_file, file_get_contents ($indir . 'Info.plist')); $info = file_get_contents ($info_file); $regex = '/(.*)<\/string>/smiU'; $matches = array (); preg_match_all ($regex, $info, $matches); $matches = $matches[1]; if (count ($matches) != 6) { fatal_error ('Invalid Info.plist'); } $devicename = trim ($matches[1]); $serial = obscure (trim ($matches[2])); $deviceid = obscure (trim ($matches[3])); $regex = '/(.*)<\/date>/smiU'; $matches = array (); preg_match_all ($regex, $info, $matches); $matches = $matches[1]; if (count ($matches) != 1) { fatal_error ('Invalid Info.plist'); } $lastbackup = strtotime(trim(ereg_replace ('[TZ]', ' ', $matches[0]))); $backup_date = date ('F j, Y, g:i a', $lastbackup); echo (Console_Color::convert ( "%rSuccessfully opened backup files.%n\n" . "%r=================================%n\n" . "Device Name: %b$devicename%n\n" . "Device Serial: %b$serial%n\n" . "Device ID: %b$deviceid%n\n" . "Last Backup: %b$backup_date%n\n\n")); // === // Manifest.plist // === $manifest = file_get_contents ($indir . 'Manifest.plist'); $regex = '/(.*)<\/data>/smiU'; $matches = array (); preg_match_all ($regex, $manifest, $matches); $matches = $matches[1]; if (count ($matches) != 3) { fatal_error ('Invalid Manifest.plist'); } $authdata = trim ($matches[0]); $authsignature = trim ($matches[1]); $data = trim ($matches[2]); // Verify AuthData if (base64_decode ($authdata) != 'Forty Two') { fatal_error ('Invalid Manifest.plist (AuthData)'); } // Verify AuthSignature if (base64_sign (base64_decode($data)) != $authsignature) { fatal_error ('Invalid Manifest.plist (AuthSignature)'); } // Extract the internal manifest $manifest = base64_decode ($data); if (!$manifest) { fatal_error ('Invalid Manifest.plist (Data)'); } $manifest_file = $outdir . 'Manifest.xml'; file_write ($manifest_file, $manifest); bpl2xml ($manifest_file); $inner_manifest = file_get_contents ($manifest_file); $regex = '/Files<\/key>[^<]+(.*)<\/dict>[^<]+Version<\/key>/smiU'; $matches = array(); preg_match_all ($regex, $inner_manifest, $matches); if (count ($matches[1]) != 1) { fatal_error ('Invalid Manifest'); } $inner_manifest = $matches[1][0]; // Pull out the items in the inner manifest $regex = '/(.*)<\/key>[^<]+(.*)<\/dict>/smiU'; $matches = array(); preg_match_all ($regex, $inner_manifest, $matches); $keys = $matches[1]; $values = $matches[2]; if (!count ($matches[1])) { fatal_error ('Manifest contains no files'); } else if ( count ($keys) != count ($values) ) { fatal_error ('Malformed inner manifest.'); } $num_files = count ($keys); echo (Console_Color::convert ("%rManifest contains $num_files files.\n%n")); $tmpdir = $outdir . substr (md5 (uniqid (rand(), true)), 0, 8); mkdir ($tmpdir); $tmpdir .= '/'; for ($i = 0; $i < count ($keys); $i++) { // Hallelujah time, we can finally extract! $backup_file = $keys[$i] . '.mdbackup'; // Copy this file over to the temporary dir if (!file_exists ($indir . $backup_file)) { echo (Console_Color::convert ("%rFile $backup_file is missing.%n\n")); continue; } file_write ($tmpdir . $backup_file, file_get_contents ($indir . $backup_file)); // Verify the file's signature $regex = '/DataHash<\/key>[^<]+(.*)<\/data>/smiU'; $matches = array (); preg_match_all ($regex, $values[$i], $matches); if (count ($matches[1]) != 1) { echo (Console_Color::convert ("%rInvalid DataHash for " . $keys[$i] . ".%n\n")); continue; } $signature = trim ($matches[1][0]); $filesignature = base64_sign (file_get_contents ($tmpdir . $backup_file)); if ($signature != $filesignature) { echo (Console_Color::convert ("%rIncorrect DataHash for " . $keys[$i] . ".%n\n")); continue; } // Verify the file's length $regex = '/FileLength<\/key>[^<]+(.*)<\/integer>/smiU'; $matches = array (); preg_match_all ($regex, $values[$i], $matches); if (count ($matches[1]) != 1) { echo (Console_Color::convert ("%rInvalid FileLength for " . $keys[$i] . ".%n\n")); continue; } $length = (int)trim ($matches[1][0]); bpl2xml ($tmpdir . $backup_file); // Extract the file itself $pref_file = file_get_contents ($tmpdir . $backup_file); $regex = '/Path<\/key>[^<]+(.*)<\/string>/smiU'; $matches = array (); preg_match_all ($regex, $pref_file, $matches); if (count ($matches[1]) != 1) { echo (Console_Color::convert ("%rInvalid Path in " . $keys[$i] . ".mdbackup%n\n")); continue; } $path = $matches[1][0]; // Verify that the backup name is the SHA1 name if ($keys[$i] != sha1($path)) { echo (Console_Color::convert ("%rIncorrectly named backup file: " . $keys[$i] . ".mdbackup%n\n")); continue; } echo (Console_Color::convert ("%bExtracting " . $path . "%n\n")); $regex = '/Data<\/key>[^<]+(.*)<\/data>/smiU'; $matches = array (); preg_match_all ($regex, $pref_file, $matches); if (count ($matches[1]) != 1) { echo (Console_Color::convert ("%rInvalid Data in " . $keys[$i] . ".mdbackup%n\n")); continue; } $data = base64_decode ($matches[1][0]); // Write the file $dirs = explode ('/', dirname ($path)); $pos = $outdir; foreach ($dirs as $dir) { if (!is_dir ($pos . $dir)) { mkdir ($pos . $dir); } $pos .= $dir . '/'; } file_write ($outdir . $path, $data); if (strlen ($data) != $length) { echo (Console_Color::convert ("%r" . path . " has an invalid size.%n\n")); continue; } } } function obscure (&$input) { global $obscure; if ($obscure) { $len = strlen ($input); if ($len <= 7) { return 'XXXX'; } else { return substr ($input, 0, 3) . str_repeat ('X', $len - 6) . substr ($input, -3); } } else { return $input; } } function bpl2xml ($filename) { $res = `plutil -convert xml1 $filename`; } function file_write ($filename, $filedata) { $fp = fopen ($filename, 'w'); if (!$fp) { fatal_error ('Unable to open ' . $filename . ' for writing'); } fputs ($fp, $filedata); fclose ($fp); } function base64_sign ($indata) { return base64_encode (pack ('H*', sha1 ($indata))); } function fatal_error ($message) { exit (Console_Color::convert ("%r$message%n\n")); } function usage_error ($message) { global $args, $config, $header, $first_error; if (!$first_error) { echo Console_Getargs::getHelp($config, $header, Console_Color::convert ("%r$message%n\n")); } else { echo (Console_Color::convert ("%r$message%n\n")); } }