import os import errno import struct import binwalk.core.common import binwalk.core.compat import binwalk.core.plugin class PFSCommon(object): def _make_short(self, data, endianness): """Returns a 2 byte integer.""" data = binwalk.core.compat.str2bytes(data) return struct.unpack('%sH' % endianness, data)[0] def _make_int(self, data, endianness): """Returns a 4 byte integer.""" data = binwalk.core.compat.str2bytes(data) return struct.unpack('%sI' % endianness, data)[0] class PFS(PFSCommon): """Class for accessing PFS meta-data.""" HEADER_SIZE = 16 def __init__(self, fname, endianness='<'): self.endianness = endianness self.meta = binwalk.core.common.BlockFile(fname, 'rb') header = self.meta.read(self.HEADER_SIZE) self.file_list_start = self.meta.tell() self.num_files = self._make_short(header[-2:], endianness) self.node_size = self._get_fname_len() + 12 def _get_fname_len(self, bufflen=128): """Returns the number of bytes designated for the filename.""" buff = self.meta.peek(bufflen) strlen = buff.find('\0') for i, b in enumerate(buff[strlen:]): if b != '\0': return strlen+i return bufflen def _get_node(self): """Reads a chunk of meta data from file and returns a PFSNode.""" data = self.meta.read(self.node_size) return PFSNode(data, self.endianness) def get_end_of_meta_data(self): """Returns integer indicating the end of the file system meta data.""" return self.HEADER_SIZE + self.node_size * self.num_files def entries(self): """Returns file meta-data entries one by one.""" self.meta.seek(self.file_list_start) for i in range(0, self.num_files): yield self._get_node() def __enter__(self): return self def __exit__(self, type, value, traceback): self.meta.close() class PFSNode(PFSCommon): """A node in the PFS Filesystem containing meta-data about a single file.""" def __init__(self, data, endianness): self.fname, data = data[:-12], data[-12:] self._decode_fname() self.inode_no = self._make_int(data[:4], endianness) self.foffset = self._make_int(data[4:8], endianness) self.fsize = self._make_int(data[8:], endianness) def _decode_fname(self): """Extracts the actual string from the available bytes.""" self.fname = self.fname[:self.fname.find('\0')] self.fname = self.fname.replace('\\', '/') class PFSExtractor(binwalk.core.plugin.Plugin): """ Extractor for known PFS/0.9 File System Formats. """ MODULES = ['Signature'] def init(self): if self.module.extractor.enabled: self.module.extractor.add_rule(regex='^pfs filesystem', extension='pfs', cmd=self.extractor) def _create_dir_from_fname(self, fname): try: os.makedirs(os.path.dirname(fname)) except OSError as e: if e.errno != errno.EEXIST: raise e def extractor(self, fname): fname = os.path.abspath(fname) out_dir = binwalk.core.common.unique_file_name(os.path.join(os.path.dirname(fname), "pfs-root")) try: with PFS(fname) as fs: # The end of PFS meta data is the start of the actual data data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): outfile_path = os.path.join(out_dir, entry.fname) if not outfile_path.startswith(out_dir): binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) else: self._create_dir_from_fname(outfile_path) outfile = binwalk.core.common.BlockFile(outfile_path, 'wb') outfile.write(data.read(entry.fsize)) outfile.close() data.close() except KeyboardInterrupt as e: raise e except Exception as e: return False return True