class APFSXfieldPadding: """ contains methods to write, read and clean hidden data using the padding created by the inode extended fields in an APFS filesystem. """ def __init__(self, stream: typ.BinaryIO): """ :param stream: typ.BinaryIO, filedescriptor of an APFS filesystem """ self.stream = stream self.apfs = APFS(stream) self.blocksize = self.apfs.getBlockSize() self.inodetable = InodeTable(stream) self.inodelist = self.inodetable.getAllInodes(self.stream) def write(self, instream: typ.BinaryIO): """ writes from instream to extended field padding via subfunction. also checks for availability and calculates the extended field padding size and offset. :param instream: typ.BinaryIO, instream containing data that is supposed to be hidden :return: APFSXfieldPaddingMetadata object """ metadata = APFSXfieldPaddingMetadata() padding = [] nopadding = [] all = [] for i in range(0, len(self.inodelist)): inodeaddress = self.inodelist[i][0] + self.inodelist[i][1] #get xfield table of contents address & number of extended fields per inode: inodeaddress += 92 self.stream.seek(inodeaddress) xfield_nr = self.stream.read(2) xfield_nr = int.from_bytes(xfield_nr, byteorder='little') #print(self.inodelist[i][0]/4096) #print(xfield_nr) #iterate over xfields, calculate space, use prev size to calculate offset if multiple xfields prev_size = 0 self.stream.seek(0) for j in range(0, xfield_nr): xhdr_size_addr = inodeaddress + (j*4) + 6 self.stream.seek(xhdr_size_addr) xhdr_size = self.stream.read(2) xhdr_size = int.from_bytes(xhdr_size, byteorder='little') all.append(xhdr_size) #print(xhdr_size) #add xfield size & potential padding size to prev_size; prev_size += xhdr_size self.stream.seek(0) if not (xhdr_size % 8): nopadding.append(xhdr_size) if (xhdr_size % 8): prev_size += (8 - xhdr_size % 8) # calculate slack address & size if j > 0: slack_address = inodeaddress + 4 + (xfield_nr * 4) + xhdr_size + prev_size if j == 0: slack_address = inodeaddress + 4 + (xfield_nr * 4) + xhdr_size #print(slack_address) slack_size = (xhdr_size +(8 - xhdr_size % 8)) - xhdr_size #print(slack_size) padding.append([slack_address, slack_size, self.inodelist[i][0]]) usedPadding = self.writeToPadding(instream, padding) #send padding tuple list and instream to subfunction, subfunction writes until instream size is 0 for i in range(0, len(usedPadding)): self.stream.seek(usedPadding[i][2]+8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(usedPadding[i][2]) self.stream.write(chksm) self.stream.seek(0) metadata.add_nodeAddress(usedPadding[i][2]) metadata.add_size(usedPadding[i][1]) metadata.add_paddingAddress(usedPadding[i][0]) return metadata def read(self,outstream: typ.BinaryIO, metadata: APFSXfieldPaddingMetadata): """ reads previously hidden data and writes it to outstream. :param outstream: typ.BinaryIO, Outstream chosen to display hidden data :param metadata: APFSXfieldPaddingMetadata, a metadata object """ paddingAddresses = metadata.get_paddingAddresses() sizes = metadata.get_sizes() totalsize = 0 i = 0 for s in sizes: totalsize += s for adr in paddingAddresses: # print(str(adr) + "\n") outstream.write(self.readFromPadding(adr, sizes[i])) totalsize -= 4 i += 1 if totalsize <= 0: break def clear(self, metadata: APFSXfieldPaddingMetadata): """ clears hidden data from extended field padding. :param metadata: APFSXfieldPaddingMetadata, a metadata object. """ paddingAddresses = metadata.get_paddingAddresses() nodeAddresses = metadata.get_nodeAddresses() sizes = metadata.get_sizes() totalsize = 0 for s in sizes: totalsize += s for j in range(0, len(paddingAddresses)): clearspace = self.clearPadding(paddingAddresses[j], sizes[j]) totalsize -= clearspace self.stream.seek(nodeAddresses[j] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(nodeAddresses[j]) self.stream.write(chksm) self.stream.seek(0) if totalsize <= 0: break def calcChecksum(self, data): # Copyright (c) 2017 Jonas Plum under GPL3.0 Licensing sum1 = np.uint64(0) sum2 = np.uint64(0) modValue = np.uint64(4294967295) # 2<<31 - 1 for i in range(int(len(data) / 4)): dt = np.dtype(np.uint32) dt = dt.newbyteorder('L') d = np.frombuffer(data[i * 4:(i + 1) * 4], dtype=dt) sum1 = (sum1 + np.uint64(d)) % modValue sum2 = (sum2 + sum1) % modValue check1 = modValue - ((sum1 + sum2) % modValue) check2 = modValue - ((sum1 + check1) % modValue) return (check2 << 32) | check1 def writeToPadding(self, instream, padding): """ subfunction that writes to extended field padding. :param instream: stream containing data that is supposed to be hidden :param padding: tuple list containing padding offsets, sizes and respective node offsets :returns: usedPadding, a tuple list containing only the used padding offsets, sizes and corresponding node offsets """ j = 0 usedPadding = [] total_size = 0 for i in range(0, len(padding)): total_size += padding[i][1] #print(total_size) while instream.peek(): if total_size <= 0: break writeaddress = padding[j][0] # print(writeaddress) writesize = padding[j][1] # print(writesize) nodeaddress = padding[j][2] total_size -= writesize writebuf = instream.read(writesize) usedPadding.append([writeaddress, writesize, nodeaddress]) self.stream.seek(0) self.stream.seek(writeaddress) self.stream.write(writebuf) # print(writebuf) j += 1 if not instream.peek(): break if total_size <= 0 and instream.peek(): raise IOError("Not enough space") return usedPadding # returns "modified" padding tuple list with only the used offset, size and node address def clearPadding(self, adr, size): """ subfunction that clears data from a single extended field padding. :param adr: offset of one extended field padding :param size: size of the extended field padding :returns: l, size of the cleared extended field padding area """ l = size address = adr self.stream.seek(address) self.stream.write(l * b'\x00') return l def readFromPadding(self, address, length): """ subfunction that reads data from a single extended field padding. :param address: offset of the extended field padding :param length: size of the extended field padding :returns: data, the data read from the extended field padding. """ self.stream.seek(0) readAddress = address self.stream.seek(readAddress) l = length data = self.stream.read(l) return data def info(self, metadata: APFSXfieldPaddingMetadata = None) -> None: padding = [] nopadding = [] all = [] for i in range(0, len(self.inodelist)): inodeaddress = self.inodelist[i][0] + self.inodelist[i][1] # get xfield table of contents address & number of extended fields per inode: inodeaddress += 92 self.stream.seek(inodeaddress) xfield_nr = self.stream.read(2) xfield_nr = int.from_bytes(xfield_nr, byteorder='little') #print(self.inodelist[i][0]/4096) #print("\n") # print(xfield_nr) # iterate over xfields, calculate space, use prev size to calculate offset if multiple xfields prev_size = 0 self.stream.seek(0) for j in range(0, xfield_nr): xhdr_size_addr = inodeaddress + (j * 4) + 6 self.stream.seek(xhdr_size_addr) xhdr_size = self.stream.read(2) xhdr_size = int.from_bytes(xhdr_size, byteorder='little') all.append(xhdr_size) # print(xhdr_size) # add xfield size & potential padding size to prev_size; prev_size += xhdr_size self.stream.seek(0) if not (xhdr_size % 8): nopadding.append(xhdr_size) if (xhdr_size % 8): prev_size += (8 - xhdr_size % 8) # calculate slack address & size if j > 0: slack_address = inodeaddress + 4 + (xfield_nr * 4) + xhdr_size + prev_size if j == 0: slack_address = inodeaddress + 4 + (xfield_nr * 4) + xhdr_size #print(slack_address) slack_size = (xhdr_size + (8 - xhdr_size % 8)) - xhdr_size #print(slack_size) #print("\n") padding.append([slack_address, slack_size, self.inodelist[i][0]]) size = 0 for i in range (0, len(padding)): size += padding[i][1] print(str(len(self.inodelist))) print(str(size) + " bytes of usable space.") if metadata != None: uSize = 0 sizes = metadata.get_sizes() for i in range(0, len(sizes)): uSize += sizes[i] print(str(uSize) + " bytes of used space.")
class APFSSuperblockSlack: """ contains methods to write, read and clean hidden data using APFS superblocks and object map structures. """ def __init__(self, stream: typ.BinaryIO): """ :param stream: typ.BinaryIO, filedescriptor of an APFS filesystem """ self.stream = stream self.apfs = APFS(stream) self.blocksize = self.apfs.getBlockSize() def write(self, instream: typ.BinaryIO): """ writes data from instream to superblock and object map structures. :param instream: stream containing the data that is supposed to be hidden :return: an APFSSuperblockSlackMetaData object """ metadata = APFSSuperblockSlackMetaData() checkpoints = Checkpoints(self.stream) al = checkpoints.getCheckpointSuperblocks(self.stream) csb = len(al) cml = checkpoints.getCheckpointCMAP(self.stream, al) csbml = len(cml) vl = checkpoints.getCheckpointVolumes(self.stream, cml) vsb = len(vl) vml = checkpoints.getCheckpointVMAP(self.stream, vl) vsbml = len(vl) opensize = self.calculateHidingSpace(al, cml, vl, vml) filesize = 0 # if (len(instream) > opensize): # raise IOError("Not enough hiding space") while instream.peek(): if opensize == 0: break else: if csb > 0: containerblock = al.pop(0) caddress = containerblock[0] + 1384 tempsize = self.writeToContainerBlock(caddress, instream) opensize -= tempsize filesize += tempsize metadata.add_cblock(caddress) csb -= 1 self.stream.seek(containerblock[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(containerblock[0]) self.stream.write(a) if not instream.peek(): break if csbml > 0: containermap = cml.pop(0) cmaddress = containermap[0] + 88 tempsize = self.writeToContainerMap(cmaddress, instream) opensize -= tempsize filesize += tempsize metadata.add_cmblock(cmaddress) csbml -= 1 self.stream.seek(containermap[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(containermap[0]) self.stream.write(a) if not instream.peek(): break if vsb > 0: volumeblock = vl.pop(0) vaddress = volumeblock[0] + 984 tempsize = self.writeToVolumeBlock(vaddress, instream) opensize -= tempsize filesize += tempsize metadata.add_vblock(vaddress) vsb -= 1 self.stream.seek(volumeblock[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(volumeblock[0]) self.stream.write(a) if not instream.peek(): break if vsbml > 0: volumemap = vml.pop(0) vmaddress = volumemap[0] + 88 tempsize = self.writeToVolumeMap(vmaddress, instream) opensize -= tempsize filesize += tempsize metadata.add_vmblock(vmaddress) vsbml - 1 self.stream.seek(volumemap[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(volumemap[0]) self.stream.write(a) if not instream.peek(): break if instream.peek(): raise IOError("Not enough space") metadata.add_length(filesize) # self.stream.seek(0) return metadata def calcChecksum(self, data): # Copyright (c) 2017 Jonas Plum under GPL3.0 Licensing sum1 = np.uint64(0) sum2 = np.uint64(0) modValue = np.uint64(4294967295) # 2<<31 - 1 for i in range(int(len(data) / 4)): dt = np.dtype(np.uint32) dt = dt.newbyteorder('L') d = np.frombuffer(data[i * 4:(i + 1) * 4], dtype=dt) sum1 = (sum1 + np.uint64(d)) % modValue sum2 = (sum2 + sum1) % modValue check1 = modValue - ((sum1 + sum2) % modValue) check2 = modValue - ((sum1 + check1) % modValue) return (check2 << 32) | check1 def calculateHidingSpace(self, cl, cml, vl, vml): # Not needed as of right now; stays implemented in case info method or other method gets added and needs it cl_space = self.blocksize - 1384 #container superblocks: used biggest possible container superblock : 1448 bytes or 0x5A8 hex cml_space = self.blocksize - 88 vml_space = self.blocksize - 88 #object map largest possible + 56 potentially reserved space: 80 bytes or 0x50 vl_space = self.blocksize - 984 total_cl_space = len(cl) * cl_space total_cml_space = len(cml) * cml_space total_vml_space = len(vml) * vml_space total_vl_space = len(vl) * vl_space #calculate amount of object maps and superblocks times the free space total_space = total_cl_space + total_cml_space + total_vl_space + total_vml_space return total_space def writeToContainerBlock(self, address, instream): """ subfunction to write data to a container superblock. :param address: offset of a container superblock :param instream: stream containing the data that is supposed to be hidden :return: size of the chunk of data that was hidden in this structure """ cl_space = self.blocksize - 1384 buf = instream.read(cl_space) self.stream.seek(address) self.stream.write(buf) self.stream.seek(0) return len(buf) def writeToContainerMap(self, address, instream): """ subfunction to write data to a container object map. :param address: offset of a container object map :param instream: stream containing the data that is supposed to be hidden :return: size of the chunk of data that was hidden in this structure """ cml_space = self.blocksize - 88 buf = instream.read(cml_space) self.stream.seek(address) self.stream.write(buf) self.stream.seek(0) return len(buf) def writeToVolumeBlock(self, address, instream): """ subfunction to write data to a volume superblock. :param address: offset of a volume superblock :param instream: stream containing the data that is supposed to be hidden :return: size of the chunk of data that was hidden in this structure """ vl_space = self.blocksize - 984 buf = instream.read(vl_space) self.stream.seek(address) self.stream.write(buf) self.stream.seek(0) return len(buf) def writeToVolumeMap(self, address, instream): """ subfunction to write data to a volume object map. :param address: offset of a volume object map :param instream: stream containing the data that is supposed to be hidden :return: size of the chunk of data that was hidden in this structure """ vml_space = self.blocksize - 88 buf = instream.read(vml_space) self.stream.seek(address) self.stream.write(buf) self.stream.seek(0) return len(buf) def clear(self, metadata: APFSSuperblockSlackMetaData): """ removes data from APFS superblock and object map structures. :param metadata: an APFSSuperblockSlackMetaData object """ checkpoints = Checkpoints(self.stream) al = checkpoints.getCheckpointSuperblocks(self.stream) csb = len(al) cml = checkpoints.getCheckpointCMAP(self.stream, al) csbml = len(cml) vl = checkpoints.getCheckpointVolumes(self.stream, cml) vsb = len(vl) vml = checkpoints.getCheckpointVMAP(self.stream, vl) vsbml = len(vl) checkpoints = Checkpoints(self.stream) al = checkpoints.getCheckpointSuperblocks(self.stream) cml = checkpoints.getCheckpointCMAP(self.stream, al) vl = checkpoints.getCheckpointVolumes(self.stream, cml) vml = checkpoints.getCheckpointVMAP(self.stream, vl) length = metadata.get_length() i = 0 blocksize = self.blocksize cblocks = metadata.get_cblocks() cmblocks = metadata.get_cmblocks() vblocks = metadata.get_vblocks() vmblocks = metadata.get_vmblocks() if len(cblocks) == 0: raise IOError("Nothing has been hidden") while length >= 0: containerblock = al.pop(0) caddress = cblocks[i] cspace = blocksize - 1384 self.stream.seek(caddress) self.stream.write(cspace * b'\x00') length -= cspace self.stream.seek(containerblock[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(containerblock[0]) self.stream.write(a) if length <= 0: break containermap = cml.pop(0) cmaddress = cmblocks[i] cmspace = blocksize - 88 self.stream.seek(cmaddress) self.stream.write(cmspace * b'\x00') length -= cmspace self.stream.seek(containermap[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(containermap[0]) self.stream.write(a) if length <= 0: break volumeblock = vl.pop(0) vaddress = vblocks[i] vspace = blocksize - 984 self.stream.seek(vaddress) self.stream.write(vspace * b'\x00') length -= vspace self.stream.seek(volumeblock[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(volumeblock[0]) self.stream.write(a) if length <= 0: break volumemap = vml.pop(0) vmaddress = vmblocks[i] vmspace = blocksize - 88 self.stream.seek(vmaddress) self.stream.write(vmspace * b'\x00') length -= vmspace self.stream.seek(volumemap[0] + 8) tocheck = self.stream.read(4088) a = self.calcChecksum(tocheck) self.stream.seek(volumemap[0]) self.stream.write(a) if length <= 0: break i += 1 def read(self, outstream: typ.BinaryIO, metadata: APFSSuperblockSlackMetaData): """ reads data hidden in APFS superblock and object map structures and writes that data to an outstream. :param outstream: chosen outstream to display found hidden data :param metadata: an APFSSuperblockSlackMetaData object """ length = metadata.get_length() foundlength = 0 i = 0 blocksize = self.blocksize cblocks = metadata.get_cblocks() cmblocks = metadata.get_cmblocks() vblocks = metadata.get_vblocks() vmblocks = metadata.get_vmblocks() if len(cblocks) == 0: raise IOError("Nothing has been hidden") while length > 0: cspace = blocksize - 1384 caddress = cblocks[i] self.stream.seek(caddress) if length <= cspace: buf = self.stream.read(length) else: buf = self.stream.read(cspace) outstream.write(buf) length -= cspace if length <= 0: break cmspace = blocksize - 88 cmaddress = cmblocks[i] self.stream.seek(cmaddress) if length <= cmspace: buf = self.stream.read(length) else: buf = self.stream.read(cmspace) outstream.write(buf) length -= cmspace if length <= 0: break vspace = blocksize - 984 vaddress = vblocks[i] if length <= vspace: buf = self.stream.read(length) else: buf = self.stream.read(vspace) outstream.write(buf) length -= vspace if length <= 0: break vmspace = blocksize - 88 vmaddress = vmblocks[i] self.stream.seek(vmaddress) if length <= vmaddress: buf = self.stream.read(length) else: buf = self.stream.read(vmspace) outstream.write(buf) length -= vmspace if length <= 0: break i += 1 def info(self, metadata: APFSSuperblockSlackMetaData = None) -> None: checkpoints = Checkpoints(self.stream) al = checkpoints.getCheckpointSuperblocks(self.stream) csb = len(al) cml = checkpoints.getCheckpointCMAP(self.stream, al) csbml = len(cml) vl = checkpoints.getCheckpointVolumes(self.stream, cml) vsb = len(vl) vml = checkpoints.getCheckpointVMAP(self.stream, vl) vsbml = len(vml) print(str(csb) + " Container Superblocks usable.\n") print(str(csbml) + " Container Object Maps usable.\n") print(str(vsb) + " Volume Superblocks usable.\n") print(str(vsbml) + " Volume Object Maps usable.\n") space = self.calculateHidingSpace(al, cml, vl, vml) print(str(space) + " bytes of space usable.\n") if metadata != None: uSpace = metadata.get_length() print(str(uSpace) + " bytes of space used.")
class APFSInodePadding: """ contains methods to write, read and clean hidden data using the padding fields present in every inode in the APFS filesystem. """ def __init__(self, stream: typ.BinaryIO): """ :param stream: typ.BinaryIO, filedescriptor of an APFS filesystem """ self.stream = stream self.apfs = APFS(stream) self.blocksize = self.apfs.getBlockSize() self.inodetable = InodeTable(stream) self.inodelist = self.inodetable.getAllInodes(self.stream) def write(self, instream: typ.BinaryIO): """ writes from instream to inode padding fields. :param instream: typ.BinaryIO, stream containing data that is supposed to be hidden :return: APFSInodePaddingMetadata """ metadata = APFSInodePaddingMetadata() instream = instream.read() if len(instream) > len(self.inodelist) * 10: raise IOError("Not enough space available") instream_chunks = [ instream[i:i + 10] for i in range(0, len(instream), 10) ] hidden_chunks = 0 i = 0 while hidden_chunks < len(instream_chunks): chunk = instream_chunks[hidden_chunks] writeaddress = self.inodelist[i][0] + self.inodelist[i][1] self.writeToPadding(writeaddress, chunk) metadata.add_inodeAddress(self.inodelist[i][0] + self.inodelist[i][1]) hidden_chunks += 1 # change has-uncompressed-size-flag. TODO mor elegant solution? (flag+0x4 instead of overwrite) self.stream.seek(self.inodelist[i][0] + self.inodelist[i][1] + 50) self.stream.write(b'\x04') self.stream.seek(self.inodelist[i][0] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(self.inodelist[i][0]) metadata.add_nodeAddress(self.inodelist[i][0]) self.stream.write(chksm) self.stream.seek(0) i += 1 return metadata def read(self, outstream: typ.BinaryIO, metadata: APFSInodePaddingMetadata): """ writes previously hidden data into outstream. :param outstream: stream to write to :param metadata: APFSInodePaddingMetadata object """ inode_addresses = metadata.get_inodeAddresses() for adr in inode_addresses: outstream.write(self.readFromPadding(adr)) def clear(self, metadata: APFSInodePaddingMetadata): """ clears previously hidden data into outstream. :param metadata: APFSInodePaddingMetadata object """ inode_addresses = metadata.get_inodeAddresses() node_addresses = metadata.get_nodeAddresses() i = 0 for adr in inode_addresses: self.clearPadding(adr) self.stream.seek(node_addresses[i] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(node_addresses[i]) self.stream.write(chksm) self.stream.seek(0) i += 1 def info(self, metadata: APFSInodePaddingMetadata = None) -> None: print(str(len(self.inodelist)) + " inodes found.\n") print(str(len(self.inodelist) * 8) + " bytes of data usable.") if metadata != None: maxUse = 0 iAddr = metadata.get_inodeAddresses() maxUse = len(iAddr) * 8 print(str(len(iAddr)) + " inodes used.\n") print(str(maxUse) + " is the maximum amount of bytes used.") def calcChecksum(self, data): # Copyright (c) 2017 Jonas Plum under GPL3.0 Licensing sum1 = np.uint64(0) sum2 = np.uint64(0) modValue = np.uint64(4294967295) # 2<<31 - 1 for i in range(int(len(data) / 4)): dt = np.dtype(np.uint32) dt = dt.newbyteorder('L') d = np.frombuffer(data[i * 4:(i + 1) * 4], dtype=dt) sum1 = (sum1 + np.uint64(d)) % modValue sum2 = (sum2 + sum1) % modValue check1 = modValue - ((sum1 + sum2) % modValue) check2 = modValue - ((sum1 + check1) % modValue) return (check2 << 32) | check1 def getTotalOffset(self, a): inodePadAdd = a + 82 # address of inode + offset to 2 padding fields return inodePadAdd def readFromPadding(self, address): """ reads 10 bytes from one inode padding. :param address: offset of a single inode padding :return: data to be written to outstream """ self.stream.seek(0) readAddress = self.getTotalOffset(address) self.stream.seek(readAddress) data = self.stream.read(10) return data def clearPadding(self, address): """ clears 10 bytes from one inode padding. :param address: offset of a single inode padding """ self.stream.seek(0) clearAddress = self.getTotalOffset(address) self.stream.seek(clearAddress) self.stream.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') def writeToPadding(self, address, chunk): """ writes 10 bytes to one inode padding. :param address: offset of a single inode padding :param chunk: 10 byte chunk of data that is supposed to be written to the inode padding referenced by address """ self.stream.seek(0) writeAddress = self.getTotalOffset(address) self.stream.seek(writeAddress) self.stream.seek(writeAddress) self.stream.write(chunk)
class APFSWriteGen: """ contains methods to write, read and clear data using the Write-Gen-Counter present in APFS inodes. """ def __init__(self, stream: typ.BinaryIO): """ :param stream: typ.BinaryIO, filedescriptor of an APFS filesystem """ self.stream = stream self.apfs = APFS(stream) self.blocksize = self.apfs.getBlockSize() self.inodetable = InodeTable(stream) self.inodelist = self.inodetable.getAllInodes(self.stream) def write(self, instream: typ.BinaryIO): """ writes from instream to Write-Gen-Counter field in inodes. :param instream: typ.BinaryIO, stream containing data that is supposed to be hidden :returns: APFSWriteGenMetadata object """ metadata = APFSWriteGenMetadata() instream = instream.read() if len(instream) > len(self.inodelist) * 4: raise IOError("Not enough space available") # Aufteilung des Instreams in Chunks der Größe der einzelnen Verstecke instream_chunks = [ instream[i:i + 4] for i in range(0, len(instream), 4) ] hidden_chunks = 0 i = 0 # Chunks werden an durch InodeTable geschrieben. Hierfür wird die Submethode writeToPadding aufgerufen # Danach wird die Checksumme neu kalkuliert while hidden_chunks < len(instream_chunks): chunk = instream_chunks[hidden_chunks] writeaddress = self.inodelist[i][0] + self.inodelist[i][1] self.writeToPadding(writeaddress, chunk) metadata.add_inodeAddress(self.inodelist[i][0] + self.inodelist[i][1]) hidden_chunks += 1 self.stream.seek(self.inodelist[i][0] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(self.inodelist[i][0]) metadata.add_nodeAddress(self.inodelist[i][0]) self.stream.write(chksm) self.stream.seek(0) i += 1 return metadata def read(self, outstream: typ.BinaryIO, metadata: APFSWriteGenMetadata): """ reads from Write-Gen-Counter fields and writes found data to outstream. :param outstream: chosen stream to display found hidden data :param metadata: an APFSWriteGenMetadata object """ inode_addresses = metadata.get_inodeAddresses() for adr in inode_addresses: outstream.write(self.readFromPadding(adr)) def clear(self, metadata: APFSWriteGenMetadata): """ clears hidden data from Write-Gen-Counter. :param metadata: an APFSWriteGenMetadata object """ inode_addresses = metadata.get_inodeAddresses() node_addresses = metadata.get_nodeAddresses() i = 0 for adr in inode_addresses: self.clearPadding(adr) self.stream.seek(node_addresses[i] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(node_addresses[i]) self.stream.write(chksm) self.stream.seek(0) i += 1 def info(self, metadata: APFSWriteGenMetadata = None) -> None: print(str(len(self.inodelist)) + " inodes found.\n") print(str(len(self.inodelist) * 4) + " bytes of data usable.") if metadata != None: maxUse = 0 iAddr = metadata.get_inodeAddresses() maxUse = len(iAddr) * 4 print(str(len(iAddr)) + " inodes used.\n") print(str(maxUse) + " is the maximum amount of bytes used.") def calcChecksum(self, data): # Copyright (c) 2017 Jonas Plum under GPL3.0 Licensing sum1 = np.uint64(0) sum2 = np.uint64(0) modValue = np.uint64(4294967295) # 2<<31 - 1 for i in range(int(len(data) / 4)): dt = np.dtype(np.uint32) dt = dt.newbyteorder('L') d = np.frombuffer(data[i * 4:(i + 1) * 4], dtype=dt) sum1 = (sum1 + np.uint64(d)) % modValue sum2 = (sum2 + sum1) % modValue check1 = modValue - ((sum1 + sum2) % modValue) check2 = modValue - ((sum1 + check1) % modValue) return (check2 << 32) | check1 def getTotalOffset(self, a): """ calculates offset of the Write-Gen-Counter field within an inode. :param a: an inode offset :returns: exact offset of the Write-Gen-Counter field within the inode """ inodePadAdd = a + 64 return inodePadAdd def readFromPadding(self, address): """ subfunction that reads a single Write-Gen-Counter field :param address: offset of an inode that contains a manipulated Write-Gen-Counter field :returns: data found in Write-Gen-Counter field """ self.stream.seek(0) #standard version readAddress = self.getTotalOffset(address) # ext field test version # readAddress = (9088 * 4096) + 4049 self.stream.seek(readAddress) # data = self.stream.read(10) data = self.stream.read(4) return data def clearPadding(self, address): """ subfunction that clears a single Write-Gen-Counter field :param address: offset of a Write-Gen-Counter field """ self.stream.seek(0) clearAddress = self.getTotalOffset(address) self.stream.seek(clearAddress) self.stream.write(b'\x00\x00\x00\x00') def writeToPadding(self, address, chunk): """ subfunction that writes a 4 byte chunk of data to a single Write-Gen-Counter field :param address: offset of a single Write-Gen-Counter field :param chunk: 4 byte sized part of the data that is supposed to be hidden """ self.stream.seek(0) writeAddress = self.getTotalOffset(address) self.stream.seek(writeAddress) self.stream.seek(writeAddress) self.stream.write(chunk)
class APFSTimestampHiding: """ contains methods to write, read and clear data using the nanosecond timestamp found in APFS inodes. """ def __init__(self, stream: typ.BinaryIO): """ :param stream: typ.BinaryIO, filedescriptor of an APFS filesystem """ self.stream = stream self.apfs = APFS(stream) self.blocksize = self.apfs.getBlockSize() self.inodetable = InodeTable(stream) self.inodelist = self.inodetable.getAllInodes(self.stream) def write(self, instream: typ.BinaryIO): """ writes data from instream to chosen inode timestamps. :param instream: typ.BinaryIO, stream containing data that is supposed to be hidden :return: APFSTimestampHidingMetadata object """ metadata = APFSTimestampHidingMetadata() # instream = instream.read() #if len(instream) > len(self.inodelist)*16: # raise IOError("Not enough space available") # instream_chunks = [instream[i:i+4]for i in range (0, len(instream), 4)] #TODO: 30 BIT CHUNKS -> FILL WITH 30 BIT FROM INSTREAM IN FOR LOOP; PUT INTO LIST INSTREAM_CHUNKS #hidden_chunks = 0 opensize = len(self.inodelist) * 16 filesize = 0 i = 0 j = 0 # Chunks werden an durch InodeTable geschrieben. Hierfür wird die Submethode writeToPadding aufgerufen # Danach wird die Checksumme neu kalkuliert #while hidden_chunks < len(instream_chunks): while instream.peek(): if opensize == 0: break #chunk = instream_chunks[hidden_chunks] writeaddress = self.inodelist[i][0] + self.inodelist[i][1] tempsize = self.writeToPadding(writeaddress, instream, j) opensize -= tempsize filesize += tempsize metadata.add_inodeAddress(self.inodelist[i][0] + self.inodelist[i][1]) print(str(self.inodelist[i][0]) + " " + str(self.inodelist[i][1])) print(str(writeaddress)) #hidden_chunks += 1 self.stream.seek(self.inodelist[i][0] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(self.inodelist[i][0]) metadata.add_nodeAddress(self.inodelist[i][0]) self.stream.write(chksm) self.stream.seek(0) j += 1 if j == 4: j = 0 i += 1 if not instream.peek(): break if instream.peek(): raise IOError("Not enough space") metadata.add_length(filesize) return metadata def read(self, outstream: typ.BinaryIO, metadata: APFSTimestampHidingMetadata): """ reads data from chosen inode timestamps and writes the data to chosen outstream. :param outstream: chosen outstream to display found data :param metadata: an APFSTimestampHidingMetadata object """ inode_addresses = metadata.get_inodeAddresses() length = metadata.get_length() j = 0 for adr in inode_addresses: # print(str(adr) + "\n") outstream.write(self.readFromPadding(adr, length, j)) length -= 4 if length <= 0: break j += 1 if j == 4: j = 0 def clear(self, metadata: APFSTimestampHidingMetadata): """ clears data from chosen inode timestamps. :params metadata: an APFSTimestampHidingMetadata object """ inode_addresses = metadata.get_inodeAddresses() node_addresses = metadata.get_nodeAddresses() length = metadata.get_length() i = 0 j = 0 for adr in inode_addresses: clearspace = self.clearPadding(adr, length, j) length -= clearspace # print(str(adr)) self.stream.seek(node_addresses[i] + 8) data = self.stream.read(4088) chksm = self.calcChecksum(data) self.stream.seek(node_addresses[i]) self.stream.write(chksm) self.stream.seek(0) j += 1 if j == 4: j = 0 i += 1 if length <= 0: break def info(self, metadata: APFSTimestampHidingMetadata = None) -> None: print(str(len(self.inodelist)) + " inodes found.\n") print("Up to " + str(len(self.inodelist) * 16) + " bytes of data usable.") if metadata != None: maxUse = 0 iAddr = metadata.get_inodeAddresses() maxUse = metadata.get_length() print(str(len(iAddr)) + " inodes used.\n") print(str(maxUse) + " is the amount of bytes used.") def calcChecksum(self, data): # Copyright (c) 2017 Jonas Plum under GPL3.0 Licensing sum1 = np.uint64(0) sum2 = np.uint64(0) modValue = np.uint64(4294967295) # 2<<31 - 1 for i in range(int(len(data) / 4)): dt = np.dtype(np.uint32) dt = dt.newbyteorder('L') d = np.frombuffer(data[i * 4:(i + 1) * 4], dtype=dt) sum1 = (sum1 + np.uint64(d)) % modValue sum2 = (sum2 + sum1) % modValue check1 = modValue - ((sum1 + sum2) % modValue) check2 = modValue - ((sum1 + check1) % modValue) return (check2 << 32) | check1 def getTotalOffset(self, a, j): """ calculates offset of hiding space within an inode. :param a: offset of an inode :param j: indicating the chosen timestamp. j can be any valuable from 0 to 3 as there are 4 possible timestamps :return: exact offset of hiding space """ if j == 0: inodePadAdd = a + 16 elif j == 1: inodePadAdd = a + 24 elif j == 2: inodePadAdd = a + 32 elif j == 3: inodePadAdd = a + 40 return inodePadAdd def readFromPadding(self, address, length, j): """ subfunction that reads data from single timestamps. :params address: offset of an inode :params length: size of the hidden data :param j: indicating the chosen timestamp. j can be any valuable from 0 to 3 as there are 4 possible timestamps :return: data found in timestamp """ self.stream.seek(0) readAddress = self.getTotalOffset(address, j) self.stream.seek(readAddress) if length > 4: l = 4 data = self.stream.read(l) if length <= 4: l = length data = self.stream.read(l) return data def clearPadding(self, address, length, j): """ subfunction that clears a single timestamp of previously hidden data. :param address: offset of an inode :param length: size of the hidden data :param j: indicating the chosen timestamp. j can be any valuable from 0 to 3 as there are 4 possible timestamps :return: size of removed data chunk """ self.stream.seek(0) l = 0 if length > 4: l = 4 elif length <= 4: l = length clearAddress = self.getTotalOffset(address, j) self.stream.seek(clearAddress) self.stream.write(l * b'\x00') return l def writeToPadding(self, address, instream, j): """ writes data to a single timestamp. :param address: offset of an inode :param instream: stream containing the data that is supposed to be hidden :param j: indicating the chosen timestamp. j can be any valuable from 0 to 3 as there are 4 possible timestamps :return: size of the hidden data chunk """ buf = instream.read(4) self.stream.seek(0) writeAddress = self.getTotalOffset(address, j) self.stream.seek(writeAddress) self.stream.write(buf) print(str(writeAddress) + " " + str(buf)) return len(buf)