rendered paste body#!/usr/bin/php
<?php
/**
* iPhone Backup Extraction Utility
*
* @author Carlos Averett <caverett@corecodec.net>
* @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>(.*)<\/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>(.*)<\/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>(.*)<\/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 = '/<key>Files<\/key>[^<]+<dict>(.*)<\/dict>[^<]+<key>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>(.*)<\/key>[^<]+<dict>(.*)<\/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 = '/<key>DataHash<\/key>[^<]+<data>(.*)<\/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 = '/<key>FileLength<\/key>[^<]+<integer>(.*)<\/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 = '/<key>Path<\/key>[^<]+<string>(.*)<\/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 = '/<key>Data<\/key>[^<]+<data>(.*)<\/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"));
}
}