All pastes #612051 Raw Edit

Unnamed

public text v1 · immutable
·published 2007-07-10 04:54 UTC
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"));
    }   
}