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