382 lines
16 KiB
Python
382 lines
16 KiB
Python
|
from crypto.aes import AESdecryptCBC, AESencryptCBC
|
||
|
from hfs.emf import cprotect_xattr, EMFVolume
|
||
|
from hfs.hfs import HFSVolume, hfs_date, HFSFile
|
||
|
from hfs.journal import carveBtreeNode, isDecryptedCorrectly
|
||
|
from hfs.structs import *
|
||
|
from util import sizeof_fmt, makedirs, hexdump
|
||
|
import hashlib
|
||
|
import os
|
||
|
import struct
|
||
|
|
||
|
class NANDCarver(object):
|
||
|
def __init__(self, volume, image, outputdir=None):
|
||
|
self.volume = volume
|
||
|
self.image = image
|
||
|
self.nand = image
|
||
|
self.ftlhax = False
|
||
|
self.userblocks = None
|
||
|
self.lpnToVpn = None
|
||
|
self.files = {}
|
||
|
self.keys = {}
|
||
|
self.encrypted = image.encrypted and hasattr(volume, "emfkey")
|
||
|
self.encrypted = hasattr(volume, "cp_root") and volume.cp_root != None
|
||
|
if outputdir == None:
|
||
|
if image.filename != "remote": outputdir = os.path.join(os.path.dirname(image.filename), "undelete")
|
||
|
else: outputdir = os.path.join(".", "undelete")
|
||
|
print "Carver output %s" % outputdir
|
||
|
self.outputdir = outputdir
|
||
|
self.okfiles = 0
|
||
|
self.first_lba = self.volume.bdev.lbaoffset
|
||
|
self.pageSize = image.pageSize
|
||
|
self.blankPage = "\xDE\xAD\xBE\xEF" * (self.pageSize/4)
|
||
|
self.emfkey = None
|
||
|
self.fileIds = None
|
||
|
self.fastMode = False
|
||
|
if hasattr(volume, "emfkey"):
|
||
|
self.emfkey = volume.emfkey
|
||
|
|
||
|
def carveFile(self, hfsfile, callback, lbas=None, filter_=None):
|
||
|
for e in hfsfile.extents:
|
||
|
if e.blockCount == 0:
|
||
|
break
|
||
|
for i in xrange(e.startBlock, e.startBlock+e.blockCount):
|
||
|
if lbas and not i in lbas:
|
||
|
continue
|
||
|
|
||
|
if self.fastMode:
|
||
|
for vpn in self.ftlhax.get(self.first_lba+i, []):
|
||
|
usn = 0
|
||
|
s,d = self.nand.ftl.YAFTL_readPage(vpn, self.emfkey, self.first_lba+i)
|
||
|
callback(d, usn, filter_)
|
||
|
else:
|
||
|
# s,d = self.nand.ftl.YAFTL_readPage(vpn, self.emfkey, self.first_lba+i)
|
||
|
# callback(d, 0)
|
||
|
usnsForLbn = self.ftlhax.get(self.first_lba+i, [])
|
||
|
for usn in sorted(usnsForLbn.keys())[:-1]:
|
||
|
for vpn in usnsForLbn[usn]:
|
||
|
s,d = self.nand.ftl.YAFTL_readPage(vpn, self.emfkey, self.first_lba+i)
|
||
|
callback(d, usn, filter_)
|
||
|
|
||
|
def _catalogFileCallback(self, data, usn, filter_):
|
||
|
for k,v in carveBtreeNode(data,HFSPlusCatalogKey, HFSPlusCatalogData):
|
||
|
if v.recordType != kHFSPlusFileRecord:
|
||
|
continue
|
||
|
if filter_ and not filter_(k,v):
|
||
|
continue
|
||
|
name = getString(k)
|
||
|
#if not self.filterFileName(name):
|
||
|
# continue
|
||
|
h = hashlib.sha1(HFSPlusCatalogKey.build(k)).digest()
|
||
|
if self.files.has_key(h):
|
||
|
continue
|
||
|
if not self.fileIds.has_key(v.data.fileID):
|
||
|
print "Found deleted file record", v.data.fileID, name.encode("utf-8"), "created", hfs_date(v.data.createDate)
|
||
|
self.files[h] = (name,v, usn)
|
||
|
|
||
|
def _attributesFileCallback(self, data, usn, filter_):
|
||
|
for k,v in carveBtreeNode(data,HFSPlusAttrKey, HFSPlusAttrData):
|
||
|
if getString(k) != "com.apple.system.cprotect":
|
||
|
continue
|
||
|
if self.fileIds.has_key(k.fileID):
|
||
|
continue
|
||
|
filekeys = self.keys.setdefault(k.fileID, [])
|
||
|
try:
|
||
|
cprotect = cprotect_xattr.parse(v.data)
|
||
|
except:
|
||
|
continue
|
||
|
if cprotect.key_size == 0:
|
||
|
continue
|
||
|
filekey = self.volume.keybag.unwrapKeyForClass(cprotect.persistent_class, cprotect.persistent_key, False)
|
||
|
if filekey and not filekey in filekeys:
|
||
|
#print "Found key for file ID ", k.fileID
|
||
|
filekeys.append(filekey)
|
||
|
|
||
|
def carveCatalog(self, lbas=None, filter_=None):
|
||
|
return self.carveFile(self.volume.catalogFile, self._catalogFileCallback, lbas, filter_)
|
||
|
|
||
|
def carveKeys(self, lbas=None):
|
||
|
return self.carveFile(self.volume.xattrFile, self._attributesFileCallback, lbas)
|
||
|
|
||
|
def pagesForLBN(self, lbn):
|
||
|
return self.ftlhax.get(self.first_lba + lbn, {})
|
||
|
|
||
|
def decryptFileBlock(self, pn, filekey, lbn, decrypt_offset):
|
||
|
s, ciphertext = self.nand.ftl.YAFTL_readPage(pn, None, lbn)
|
||
|
if not self.encrypted:
|
||
|
return ciphertext
|
||
|
if not self.image.isIOS5():
|
||
|
return AESdecryptCBC(ciphertext, filekey, self.volume.ivForLBA(lbn))
|
||
|
clear = ""
|
||
|
ivkey = hashlib.sha1(filekey).digest()[:16]
|
||
|
for i in xrange(len(ciphertext)/0x1000):
|
||
|
iv = self.volume.ivForLBA(decrypt_offset, False)
|
||
|
iv = AESencryptCBC(iv, ivkey)
|
||
|
clear += AESdecryptCBC(ciphertext[i*0x1000:(i+1)*0x1000], filekey, iv)
|
||
|
decrypt_offset += 0x1000
|
||
|
return clear
|
||
|
|
||
|
def writeUndeletedFile(self, filename, data):
|
||
|
knownExtensions = (".m4a", ".plist",".sqlite",".sqlitedb", ".jpeg", ".jpg", ".png", ".db",".json",".xml",".sql")
|
||
|
#windows invalid chars \/:*?"<>|
|
||
|
filename = str(filename.encode("utf-8")).translate(None, "\\/:*?\"<>|,")
|
||
|
folder = self.outputdir
|
||
|
if self.outputdir == "./":
|
||
|
folder = folder + "/undelete"
|
||
|
elif filename.lower().endswith(knownExtensions):
|
||
|
ext = filename[filename.rfind(".")+1:]
|
||
|
folder = folder + "/" + ext.lower()
|
||
|
makedirs(folder)
|
||
|
open(folder + "/" + filename, "wb").write(data)
|
||
|
|
||
|
def getFileAtUSN(self, filename, filerecord, filekey, usn, previousVersion=None, exactSize=True):
|
||
|
missing_pages = 0
|
||
|
decrypt_offset = 0
|
||
|
file_pages = []
|
||
|
logicalSize = filerecord.dataFork.logicalSize
|
||
|
for extent in self.volume.getAllExtents(filerecord.dataFork, filerecord.fileID):
|
||
|
for bn in xrange(extent.startBlock, extent.startBlock + extent.blockCount):
|
||
|
pn = self.pagesForLBN(bn).get(usn) #fail
|
||
|
if pn:
|
||
|
clear = self.decryptFileBlock(pn[-1], filekey, bn, decrypt_offset)
|
||
|
file_pages.append(clear)
|
||
|
elif previousVersion:
|
||
|
file_pages.append(previousVersion[len(file_pages)])
|
||
|
else:
|
||
|
file_pages.append(self.blankPage)
|
||
|
missing_pages += 1
|
||
|
decrypt_offset += self.pageSize
|
||
|
|
||
|
print "Recovered %d:%s %d missing pages, size %s, created %s, contentModDate %s" % \
|
||
|
(filerecord.fileID, filename.encode("utf-8"), missing_pages, sizeof_fmt(logicalSize), hfs_date(filerecord.createDate), hfs_date(filerecord.contentModDate))
|
||
|
filename = "%d_%d_%s" % (filerecord.fileID, usn, filename)
|
||
|
if missing_pages == 0:
|
||
|
filename = "OK_" + filename
|
||
|
self.okfiles += 1
|
||
|
data = "".join(file_pages)
|
||
|
if exactSize:
|
||
|
data = data[:logicalSize]
|
||
|
self.writeUndeletedFile(filename, data)
|
||
|
return file_pages
|
||
|
|
||
|
#test for SQLite
|
||
|
def rollbackExistingFile(self, filename):
|
||
|
filerecord = self.volume.getFileRecordForPath(filename)
|
||
|
filekey = self.volume.getFileKeyForFileId(filerecord.fileID)
|
||
|
print "filekey", filekey.encode("hex")
|
||
|
z = None
|
||
|
for extent in filerecord.dataFork.HFSPlusExtentDescriptor:
|
||
|
for bn in xrange(extent.startBlock, extent.startBlock + extent.blockCount):
|
||
|
pages = self.pagesForLBN(bn)
|
||
|
print pages
|
||
|
for usn in sorted(pages.keys()):
|
||
|
d = self.decryptFileBlock(pages[usn][-1], filekey, bn, 0)
|
||
|
if d.startswith("SQL") or True:
|
||
|
filechangecounter = struct.unpack(">L", d[24:28])
|
||
|
print usn, "OK", filechangecounter
|
||
|
z = self.getFileAtUSN(os.path.basename(filename), filerecord, filekey, usn, z)
|
||
|
else:
|
||
|
print usn, "FAIL"
|
||
|
return
|
||
|
|
||
|
def filterFileName(self, filename):
|
||
|
return filename.lower().endswith(".jpg")
|
||
|
|
||
|
def getExistingFileIDs(self):
|
||
|
print "Collecting existing file ids"
|
||
|
self.fileIds = self.volume.listAllFileIds()
|
||
|
print "%d file IDs" % len(self.fileIds.keys())
|
||
|
|
||
|
def carveDeletedFiles_fast(self, catalogLBAs=None, filter_=None):
|
||
|
self.fastMode = True
|
||
|
if not self.ftlhax:
|
||
|
hax, userblocks = self.nand.ftl.YAFTL_lookup1()
|
||
|
self.ftlhax = hax
|
||
|
self.userblocks = userblocks
|
||
|
|
||
|
self.files = {}
|
||
|
if not self.fileIds:
|
||
|
self.getExistingFileIDs()
|
||
|
print "Carving catalog file"
|
||
|
#catalogLBAs = None
|
||
|
self.carveCatalog(catalogLBAs, filter_)
|
||
|
|
||
|
keysLbas = []
|
||
|
for name, vv, usn in self.files.values():
|
||
|
for i in xrange(vv.data.fileID, vv.data.fileID + 100):
|
||
|
if self.volume.xattrTree.search((i, "com.apple.system.cprotect")):
|
||
|
keysLbas.extend(self.volume.xattrTree.getLBAsHax())
|
||
|
break
|
||
|
|
||
|
#print "keysLbas", keysLbas
|
||
|
if self.encrypted and len(self.keys) == 0:
|
||
|
print "Carving attribute file for file keys"
|
||
|
#self.carveKeys(keysLbas)
|
||
|
self.carveKeys()
|
||
|
|
||
|
self.okfiles = 0
|
||
|
total = 0
|
||
|
print "%d files, %d keys" % (len(self.files), len(self.keys))
|
||
|
for name, vv, usn in self.files.values():
|
||
|
if not self.keys.has_key(vv.data.fileID):
|
||
|
print "No file key for %s" % name.encode("utf-8")
|
||
|
keys = set(self.keys.get(vv.data.fileID, [self.emfkey]))
|
||
|
print "%s" % (name.encode("utf-8"))
|
||
|
if self.readFileHax(name, vv.data, keys):
|
||
|
total += 1
|
||
|
|
||
|
print "Carving done, recovered %d deleted files, %d are most likely OK" % (total, self.okfiles)
|
||
|
|
||
|
def carveDeleteFiles_slow(self, catalogLBAs=None, filter_=None):
|
||
|
self.fastMode = False
|
||
|
self.files = {}
|
||
|
if not self.ftlhax:
|
||
|
self.ftlhax = self.nand.ftl.YAFTL_hax2()
|
||
|
if not self.fileIds:
|
||
|
self.getExistingFileIDs()
|
||
|
if self.encrypted and len(self.keys) == 0:
|
||
|
print "Carving attribute file for file keys"
|
||
|
self.carveKeys()
|
||
|
print "Carving catalog file"
|
||
|
self.carveCatalog(catalogLBAs, filter_)
|
||
|
|
||
|
self.okfiles = 0
|
||
|
total = 0
|
||
|
print "%d files" % len(self.files)
|
||
|
for name, vv, usn in self.files.values():
|
||
|
keys = set(self.keys.get(vv.data.fileID, [self.emfkey]))
|
||
|
print "%s num keys = %d" % (name, len(keys))
|
||
|
good_usn = 0
|
||
|
for filekey in keys:
|
||
|
if good_usn:
|
||
|
break
|
||
|
first_block = vv.data.dataFork.HFSPlusExtentDescriptor[0].startBlock
|
||
|
for usn, pn in self.pagesForLBN(first_block).items():
|
||
|
d = self.decryptFileBlock(pn[-1], filekey, self.first_lba+first_block, 0)
|
||
|
if isDecryptedCorrectly(d):
|
||
|
#print "USN for first block : ", usn
|
||
|
good_usn = usn
|
||
|
break
|
||
|
if good_usn == 0:
|
||
|
continue
|
||
|
self.getFileAtUSN(name, vv.data, filekey, good_usn)
|
||
|
|
||
|
def getBBTOC(self, block):
|
||
|
btoc = self.nand.ftl.readBTOCPages(block, self.nand.ftl.totalPages)
|
||
|
if not btoc:
|
||
|
return self.nand.ftl.block_lpn_to_vpn(block)
|
||
|
bbtoc = {}
|
||
|
for i in xrange(len(btoc)):
|
||
|
bbtoc[btoc[i]] = block*self.nand.ftl.vfl.pages_per_sublk + i
|
||
|
return bbtoc
|
||
|
|
||
|
def readFileHax(self, filename, filerecord, filekeys):
|
||
|
lba0 = self.first_lba + filerecord.dataFork.HFSPlusExtentDescriptor[0].startBlock
|
||
|
filekey = None
|
||
|
good_usn = None
|
||
|
first_vpn = 0
|
||
|
first_usn = 0
|
||
|
hax = self.ftlhax
|
||
|
print "%d versions for first lba" % len(hax.get(lba0, []))
|
||
|
for k in filekeys:
|
||
|
for vpn in hax.get(lba0, []):
|
||
|
s, ciphertext = self.nand.ftl.YAFTL_readPage(vpn, key=None, lpn=None)
|
||
|
if not ciphertext:
|
||
|
continue
|
||
|
d = self.decryptFileBlock2(ciphertext, k, lba0, 0)
|
||
|
#hexdump(d[:16])
|
||
|
if isDecryptedCorrectly(d):
|
||
|
filekey = k
|
||
|
first_vpn = vpn
|
||
|
first_usn = good_usn = s.usn
|
||
|
block = vpn / self.nand.ftl.vfl.pages_per_sublk
|
||
|
break
|
||
|
if not filekey:
|
||
|
return False
|
||
|
logicalSize = filerecord.dataFork.logicalSize
|
||
|
missing_pages = 0
|
||
|
file_pages = []
|
||
|
lbns = []
|
||
|
for extent in self.volume.getAllExtents(filerecord.dataFork, filerecord.fileID):
|
||
|
for bn in xrange(extent.startBlock, extent.startBlock + extent.blockCount):
|
||
|
lbns.append(self.first_lba + bn)
|
||
|
datas = {}
|
||
|
usnblocksToLookAT = sorted(filter(lambda x: x >= good_usn, self.userblocks.keys()))[:5]
|
||
|
print usnblocksToLookAT
|
||
|
usnblocksToLookAT.insert(0, 0)
|
||
|
first_block = True
|
||
|
done = False
|
||
|
for usn in usnblocksToLookAT:
|
||
|
if first_block:
|
||
|
bbtoc = self.getBBTOC(block)
|
||
|
first_block = False
|
||
|
else:
|
||
|
bbtoc = self.getBBTOC(self.userblocks[usn])
|
||
|
for lbn in bbtoc.keys():
|
||
|
if not lbn in lbns:
|
||
|
continue
|
||
|
idx = lbns.index(lbn)
|
||
|
s, ciphertext = self.nand.ftl.YAFTL_readPage(bbtoc[lbn], key=None, lpn=None)
|
||
|
if not ciphertext:
|
||
|
continue
|
||
|
ciphertext = self.decryptFileBlock2(ciphertext, filekey, lbn, idx*self.pageSize)
|
||
|
if idx == 0:
|
||
|
if not isDecryptedCorrectly(ciphertext):
|
||
|
continue
|
||
|
datas[idx*self.pageSize] = (ciphertext, lbn - self.first_lba)
|
||
|
#if idx == len(lbns):
|
||
|
if len(datas) == len(lbns):
|
||
|
done=True
|
||
|
break
|
||
|
|
||
|
if done:
|
||
|
break
|
||
|
cleartext = ""
|
||
|
decrypt_offset = 0
|
||
|
for i in xrange(0,logicalSize, self.pageSize):
|
||
|
if datas.has_key(i):
|
||
|
ciphertext, lbn = datas[i]
|
||
|
cleartext += ciphertext
|
||
|
else:
|
||
|
cleartext += self.blankPage
|
||
|
missing_pages += 1
|
||
|
decrypt_offset += self.pageSize
|
||
|
|
||
|
print "Recovered %d:%s %d missing pages, size %s, created %s, contentModDate %s" % \
|
||
|
(filerecord.fileID, filename.encode("utf-8"), missing_pages, sizeof_fmt(logicalSize), hfs_date(filerecord.createDate), hfs_date(filerecord.contentModDate))
|
||
|
filename = "%d_%d_%s" % (filerecord.fileID, first_usn, filename)
|
||
|
if missing_pages == 0:
|
||
|
filename = "OK_" + filename
|
||
|
self.okfiles += 1
|
||
|
if True:#exactSize:
|
||
|
cleartext = cleartext[:logicalSize]
|
||
|
self.writeUndeletedFile(filename, cleartext)
|
||
|
return True
|
||
|
|
||
|
def decryptFileBlock2(self, ciphertext, filekey, lbn, decrypt_offset):
|
||
|
if not self.encrypted:
|
||
|
return ciphertext
|
||
|
if not self.image.isIOS5():
|
||
|
return AESdecryptCBC(ciphertext, filekey, self.volume.ivForLBA(lbn, add=False))
|
||
|
clear = ""
|
||
|
ivkey = hashlib.sha1(filekey).digest()[:16]
|
||
|
for i in xrange(len(ciphertext)/0x1000):
|
||
|
iv = self.volume.ivForLBA(decrypt_offset, False)
|
||
|
iv = AESencryptCBC(iv, ivkey)
|
||
|
clear += AESdecryptCBC(ciphertext[i*0x1000:(i+1)*0x1000], filekey, iv)
|
||
|
decrypt_offset += 0x1000
|
||
|
return clear
|
||
|
|
||
|
def getFileRanges(self, hfsfile):
|
||
|
res = []
|
||
|
for e in hfsfile.extents:
|
||
|
if e.blockCount == 0:
|
||
|
break
|
||
|
res.append(xrange(e.startBlock, e.startBlock+e.blockCount))
|
||
|
return res
|
||
|
|
||
|
def readFSPage(self, vpn, lba):
|
||
|
s,d = self.nand.ftl.YAFTL_readPage(vpn, self.emfkey, self.first_lba+lba)
|
||
|
if s:
|
||
|
return d
|
||
|
|