Parcourir la source

Feature/manifest mbdb (#20)

* Manifest.mbdb parse
* Updated `manifest` report to support iOS `>=5.0,<10.0`
Alberto Güerere il y a 7 ans
Parent
commit
293983bb35
5 fichiers modifiés avec 206 ajouts et 55 suppressions
  1. 5 0
      package-lock.json
  2. 1 0
      package.json
  3. 122 55
      tools/reports/manifest.js
  4. 8 0
      tools/util/iphone_backup.js
  5. 70 0
      tools/util/manifest_mbdb_parse.js

+ 5 - 0
package-lock.json

@@ -160,6 +160,11 @@
         "concat-map": "0.0.1"
       }
     },
+    "buffer-reader": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-reader/-/buffer-reader-0.1.0.tgz",
+      "integrity": "sha1-YpU/lVu8UNiqjodfl4kyajsMdkQ="
+    },
     "bufferpack": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/bufferpack/-/bufferpack-0.0.6.tgz",

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
   "dependencies": {
     "binary-cookies": "^0.1.1",
     "bplist-parser": "^0.1.1",
+    "buffer-reader": "^0.1.0",
     "chalk": "^1.1.3",
     "commander": "^2.12.2",
     "fs-extra": "^4.0.3",

+ 122 - 55
tools/reports/manifest.js

@@ -5,7 +5,7 @@ const fs = require('fs-extra')
 const chalk = require('chalk')
 const path = require('path')
 module.exports.name = 'manifest'
-module.exports.description = 'List all the files contained in the backup (iOS 10+)'
+module.exports.description = 'List all the files contained in the backup (iOS 5+)'
 
 // Specify this reporter requires a backup. 
 // The second parameter to func() is now a backup instead of the path to one.
@@ -14,69 +14,136 @@ module.exports.requiresBackup = true
 // Specify this reporter supports the promises API for allowing chaining of reports.
 module.exports.usesPromises = true
 
-// Specify this only works for iOS 10+
-module.exports.supportedVersions = '>=10.0'
-
-module.exports.func = function (program, backup, resolve, reject) {
-
-  backup.getFileManifest()
-  .then((items) => {
-
-    // Extract items for analysis on-disk.
-    if (program.extract) {
-      for (var item of items) {
-        // Filter by the domain.
-        // Simple "Contains" Search
-        if (program.filter === 'all' || (program.filter && item.domain.indexOf(program.filter) > -1)) {
-          // Do nothing, we'll process later.
-        } else {
-          // Skip to the next iteration of the loop.
-          console.log(chalk.yellow('skipped'), item.relativePath)
-          continue
-        }
+// Specify this only works for iOS 5+
+module.exports.supportedVersions = '>=5.0'
+
+// You can also provide an array of functions instead of using `module.exports.func`.
+// These functions *should* be independent ranges to ensure reliable execution
+module.exports.functions = {
+  '>=10.0': function(program,backup,resolve,reject) {
+    // This function would be called for iOS 10+
+      
+    backup.getFileManifest()
+    .then((items) => {
+
+      // Extract items for analysis on-disk.
+      if (program.extract) {
+        for (var item of items) {
+          // Filter by the domain.
+          // Simple "Contains" Search
+          if (program.filter === 'all' || (program.filter && item.domain.indexOf(program.filter) > -1)) {
+            // Do nothing, we'll process later.
+          } else {
+            // Skip to the next iteration of the loop.
+            console.log(chalk.yellow('skipped'), item.relativePath)
+            continue
+          }
 
-        try {
-          var sourceFile = backup.getFileName(item.fileID)
-          var stat = fs.lstatSync(sourceFile)
+          try {
+            var sourceFile = backup.getFileName(item.fileID)
+            var stat = fs.lstatSync(sourceFile)
 
-          // Only process files that exist.
-          if (stat.isFile() && fs.existsSync(sourceFile)) {
-            console.log(chalk.green('export'), item.relativePath)
+            // Only process files that exist.
+            if (stat.isFile() && fs.existsSync(sourceFile)) {
+              console.log(chalk.green('export'), item.relativePath)
 
-              // Calculate the output dir.
-            var outDir = path.join(program.extract, item.domain, item.relativePath)
+                // Calculate the output dir.
+              var outDir = path.join(program.extract, item.domain, item.relativePath)
 
-              // Create the directory and copy
-            fs.ensureDirSync(path.dirname(outDir))
-            fs.copySync(sourceFile, outDir)
+                // Create the directory and copy
+              fs.ensureDirSync(path.dirname(outDir))
+              fs.copySync(sourceFile, outDir)
 
-              // Save output info to the data item.
-            item.output_dir = outDir
-          } else if (stat.isDirectory()) {
-            // Do nothing..
-          } else {
-            console.log(chalk.blue('not found'), item.relativePath)
+                // Save output info to the data item.
+              item.output_dir = outDir
+            } else if (stat.isDirectory()) {
+              // Do nothing..
+            } else {
+              console.log(chalk.blue('not found'), item.relativePath)
+            }
+          } catch (e) {
+            console.log(chalk.red('fail'), item.relativePath, e.toString())
           }
-        } catch (e) {
-          console.log(chalk.red('fail'), item.relativePath, e.toString())
         }
+
+        resolve(result)
+      } else {
+
+        var result = program.formatter.format(items, {
+          program: program,
+          columns: {
+            'ID': el => el.fileID,
+            'Domain/Path': el => el.domain + ': ' + el.relativePath
+          }
+        })
+
+        resolve(result)
       }
+    })
+    .catch((e) => {
+        console.log('[!] Encountered an Error:', e)
+    })
+  }, 
+
+  '>=5.0,<10.0': function(program,backup,resolve,reject) {
+    // This function would be called for all iOS 5 up to iOS 9.x.
+    backup.getOldFileManifest()
+    .then((items) => {
 
-      resolve(result)
-    } else {
+      // Extract items for analysis on-disk.
+      if (program.extract) {
+        for (var item of items) {
+          // Filter by the domain.
+          // Simple "Contains" Search
+          if (program.filter === 'all' || (program.filter && item.domain.indexOf(program.filter) > -1)) {
+            // Do nothing, we'll process later.
+          } else {
+            // Skip to the next iteration of the loop.
+            console.log(chalk.yellow('skipped'), item.filename)
+            continue
+          }
+
+          try {
+            var sourceFile = backup.getFileName(item.fileID)
+            var stat = fs.lstatSync(sourceFile)
+
+            // Only process files that exist.
+            if (stat.isFile() && fs.existsSync(sourceFile)) {
+              console.log(chalk.green('export'), item.filename)
+
+                // Calculate the output dir.
+              var outDir = path.join(program.extract, item.domain, item.filename)
+
+                // Create the directory and copy
+              fs.ensureDirSync(path.dirname(outDir))
+              fs.copySync(sourceFile, outDir)
 
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
-          'ID': el => el.fileID,
-          'Domain/Path': el => el.domain + ': ' + el.relativePath
+                // Save output info to the data item.
+              item.output_dir = outDir
+            } else if (stat.isDirectory()) {
+              // Do nothing..
+            } else {
+              console.log(chalk.blue('not found'), item.filename)
+            }
+          } catch (e) {
+            console.log(chalk.red('fail'), item.filename, e.toString())
+          }
         }
-      })
-
-      resolve(result)
-    }
-  })
-  .catch((e) => {
-      console.log('[!] Encountered an Error:', e)
-  })
+
+        resolve(result)
+      } else {
+        var result = program.formatter.format(items, {
+          program: program,
+          columns: {
+            'ID': el => el.fileID,
+            'Domain/Path': el => (el.domain + ': ' + el.filename).substr(0,70),
+            'Size': el => el.filelen
+          }
+        })
+
+        resolve(result)
+      }
+    })
+    .catch(reject)
+  }
 }

+ 8 - 0
tools/util/iphone_backup.js

@@ -6,6 +6,7 @@ const plist = require('plist')
 const mac_address_parse = require('./mac_address_parse')
 const cookieParser = require('binary-cookies')()
 const tz_offset = 5
+const manifest_mbdb_parse = require('./manifest_mbdb_parse')
 
 const databases = {
   SMS: '3d0d7e5fb2ce288813306e4d4636395e047a3d28',
@@ -318,6 +319,13 @@ class iPhoneBackup {
     })
   }
 
+  getOldFileManifest () {
+    return new Promise((resolve, reject) => {
+      let mbdbPath = this.getFileName('Manifest.mbdb', true)
+      manifest_mbdb_parse.process(mbdbPath, resolve, reject)
+    })
+  }
+
   getOldNotes () {
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.Notes)

+ 70 - 0
tools/util/manifest_mbdb_parse.js

@@ -0,0 +1,70 @@
+const crypto = require('crypto')
+const fs = require('fs')
+const BufferReader = require('buffer-reader');
+
+const getInt = (reader, intSize, debug) => {
+  //Retrieve an integer (big-endian)
+  let value = 0
+  while (intSize > 0) {
+    let int8 = reader.nextUInt8()
+    value = (value << 8) + int8
+    intSize = intSize - 1
+  }
+  return value
+}
+
+const getString = (reader) => {
+  if (reader.nextBuffer(1).toString('hex') === 'ff') {
+    if (reader.nextBuffer(1).toString('hex') === 'ff')
+      return '' // Blank string
+    else reader.move(-2) //Move reader back if is a valid string length
+  } else reader.move(-1) //Move reader back accordingly
+
+  const length = getInt(reader, 2) // 2-byte length
+  value = reader.nextString(length)
+  return value
+}
+
+module.exports.process = (filename, resolve, reject) => {
+  let mbdb = []
+  let contents = null
+  try {
+    contents = fs.readFileSync(filename)
+    
+    const reader = new BufferReader(contents);
+    if (reader.nextString(4) === 'mbdb' && reader.nextBuffer(2).toString('hex') === '0500'){
+      while (reader.tell() < contents.byteLength) {
+        let fileInfo = {}
+        fileInfo['domain'] = getString(reader)
+        fileInfo['filename'] = getString(reader)
+        fileInfo['linktarget'] = getString(reader)
+        fileInfo['datahash'] = getString(reader)
+        fileInfo['enckey'] = getString(reader)
+        fileInfo['mode'] = getInt(reader, 2)
+        fileInfo['inode'] = getInt(reader, 8)
+        fileInfo['userid'] = getInt(reader, 4)
+        fileInfo['groupid'] = getInt(reader, 4)
+        fileInfo['mtime'] = getInt(reader, 4)
+        fileInfo['atime'] = getInt(reader, 4)
+        fileInfo['ctime'] = getInt(reader, 4)
+        fileInfo['filelen'] = getInt(reader, 8)
+        fileInfo['flag'] = getInt(reader, 1)
+        fileInfo['numprops'] = getInt(reader, 1)
+        fileInfo['properties'] = {}
+        for (let i = 0; i < fileInfo['numprops']; i++) {
+          fileInfo['properties'][getString(reader)] = getString(reader)
+        }
+        const fullpath = fileInfo['domain'] + '-' + fileInfo['filename']
+        fileInfo['fileID'] = crypto.createHash('sha1').update(fullpath).digest('hex')
+        mbdb.push(fileInfo)
+      }
+    } else {
+      const err = 'This does not look like an MBDB file'
+      reject(err)
+    }
+  } catch (e) {
+    console.log('Cannot open Manifest.mbdb')
+    reject(e)
+  }
+  resolve(mbdb)
+}