Skip to main content
2 of 8
deleted 15 characters in body

I was working on this this weekend. Turns out it's pretty straight-forward. Simple list of contiguous files. Just copies files when they change, and marks the allocation-table entries as dead, for later collection.

The following is incomplete, but it should let you tweak and special-case your way through a specific MFS partition. I've tried to document in comments where I've made large assumptions.

import sys #litte-endian integer readers def read_leuint8(file): data = file.read(1) return data[0] def read_leuint16(file): data = file.read(2) return data[0] | (data[1] << 8) def read_leuint24(file): data = file.read(3) return data[0] | (data[1] << 8) | (data[2] << 16) def read_leuint32(file): data = file.read(4) return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24) class MEFileSystemDataHeader: """ Data Page Header: (Top of a 0x4000-byte data page in the MFS) 0 1 2 3 4 5 6 7 8 9 a b c d e f +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 00 | pgno| pgflags | 0x00| 0x00| 0x00| 0x00| mystery bits | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 10 | freed_flags... | +-----------------------------------------------------------------------------------------------+ ... | ... | +-----------------------------------------------------------------------------------------------+ 80 | ... | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 90 | block indirection table... | +-----------------------------------------------------------------------------------------------+ ... | ... | +-----------------------------------------------------------------------------------------------+ c0 | ... | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ d0+| (file data follows) | +-----------------------------------------------------------------------------------------------+ pgno -- page identifier within the MFS. All MFS page headers have this field, be they AT or Data. pgflags -- have seen 0x78 0xfe 0xff for AT header. Data pages have had this or 0x78 0xfc 0xff. (zeros) -- a useless value for flash...must be part of the signature of the header? Or reserved region for later version of the format. myst_bits -- TBD freed_flags-- each bit marks whether a written 0x10-byte chunk is no longer required. Bits exist for the header chunks, as well as the chunks for file data. lsb of the first byte corresponds to the first chunk of the header. 0=freed. blk_itab -- translates ATFileEntry's okey value into a block offset within the data page. offset = blk_itab[okey] * 0x100. This is the location from which to begin the search. Note that an offset of 0 must still respect that the page header consumes bytes [0x00, 0xd0) for the search. (filedata) -- Files are padded with 0xff at the end. Note files seem to have at least 2 bytes more data than the specified length. Larger files have more. """ def __init__(self, page_no, flags, bytes_00000000, myst_bits, freed_flags, blk_itab): self.page_no = page_no self.flags = flags self.zeros_good = bytes_00000000 == b'\x00\x00\x00\x00' self.myst_bits = myst_bits self.freed_flags = freed_flags self.blk_itab = blk_itab def debug_print(self, log = sys.stdout): log.write("Debug Print of MEFileSystemDataHeader %x:\n" % id(self)) if self.page_no != 0xff: log.write(" page no: 0x%x\n" % self.page_no) log.write(" flags: 0x%06x\n" % self.flags) log.write(" zeros good: %s\n" % str(self.zeros_good)) log.write(" mystery bits: %s\n" % " ".join("%02x"%x for x in self.myst_bits)) log.write(" freed_flags: %s\n" % "".join("%02x"%x for x in self.freed_flags)) log.write(" block ind. tab.: [%s]\n" % " ".join("%02x"%x for x in self.blk_itab)) else: log.write(" (empty)\n") class MEFileSystemATEntry: """ Allocation Table Entry: 0 1 2 3 4 5 6 7 8 9 a +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 00 |state| flgs| identifier | type| filelen |pgidx| okey| skip| +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ state -- status of the entry: 0xdc=present; 0xc8=overwritten(i.e., there will be another entry below) flags -- has value 0xf0 in every example...so hard to tell. identifier -- 3-byte identifier...seem to be a preference for ASCII, but there are counterexamples type -- no idea...maybe permissions? Observed values: 0x00, 0x0a, 0x0c, 0x0d, 0x0e, 0x18, 0x1a filelen -- 16-bit, little endian. actual content seems to be a few bytes more...might just be pollution of the structures used to write the file. pgidx -- the pgno for the MEFileSystemDataHeader that holds the data. The ATHeader is numbered 0, each data page seems to get sequentially numbered in my examples, though that could change with more use. 0xff indicates "not yet used / nonumber assigned" --- a fact that hints page numbers aren't guaranteed to be sequentially okey -- key for indexing the block_indirection table of the MEFileSystemDataHeader holding this file's content, to find the right 0x100 byte block from which to start the file search according to the 'skip' field. skip -- perhaps the most amusing part of this format: starting from the block-offset determined from okey, gives how many file end-padding sequences (sequences of 0xff) to skip to find the target file. Effectively, "file count within the block". Note that 0 is a special case: when the file starts exactly on the block bounary. """ def __init__(self, state, flags, identifier, type, filelen, pgidx, okey, skip): self.state = state self.flags = flags self.identifier = identifier self.type = type self.filelen = filelen self.pgidx = pgidx self.okey = okey self.skip = skip def debug_print(self, log = sys.stdout): log.write("%15s len=0x%04x [pg=0x%02x k=0x%02x s=0x%02x] ty=0x%02x st=0x%02x, fg=0x%02x" % (str(self.identifier), self.filelen, self.pgidx, self.okey, self.skip, self.type, self.state, self.flags)) class MEFileSystemATHeader: """ Allocation Table Header: 0 1 2 3 4 5 6 7 8 9 a b c d e f +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 00 | pgno| flags | 0x00| 0x00| 0x00| 0x00|"MFS\0" |bitfields? | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 10 | bitfields? | +-----+-----+-----+-----+ pgno -- page identifier within the MFS. All MFS page headers have this field, be they AT or Data. flags -- have seen 0x78 0xfe 0xff for AT header. Data pages have had this or 0x78 0xfc 0xff. (zeros) -- a useless value for flash...must be part of the signature of the header? Or reserved region for later version of the format. "MFS\0" -- ASCIIZ signature for the MFS AT Header bitfields -- 64 bits of apparent bitfields """ max_files = int((0x4000 - 0x14) / 11) #page size, less the header, divided by file entry size def __init__(self, page_no, flags, bytes_00000000, sig, bitfields): self.page_no = page_no self.flags = flags self.zeros_good = bytes_00000000 == b'\x00\x00\x00\x00' self.sig_good = sig == b'MFS\x00' self.bitfields = bitfields def debug_print(self, log = sys.stdout): log.write("Debug Print of MEFileSystemATHeader %x:\n" % id(self)) log.write(" page no: 0x%x\n" % self.page_no) log.write(" flags: 0x%06x\n" % self.flags) log.write(" zeros good: %s\n" % str(self.zeros_good)) log.write(" sig good: %s\n" % str(self.sig_good)) log.write(" bitfields: %s\n" % " ".join("%02x"%x for x in self.bitfields)) class MEFileSystemAT: """ Allocation Table (a 0x4000-byte page in the MFS): 0 1 2 3 4 5 6 7 8 9 a b c d e f +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 00 | Allocation Table Header... | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 10 |... | File Entry |FE...| +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ ...|... | +-----------------------------------------------------------------------------------+-----+-----+ 3fe0|... |File Ent...| +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 3ff0|... |Padding | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ """ def __init__(self, header, entry_list): self.header = header self.entry_list = entry_list def debug_print(self, log = sys.stdout): log.write("Debug Print of MEFileSystemAT %x:\n" % id(self)) self.header.debug_print(log) log.write("File entries:\n") for ent in self.entry_list: if(ent): log.write(" ") ent.debug_print(log) log.write("\n") class MEFileSystem: """ MFS Partition: (Making the assumption the AT is the first page) +------------------------------------+ 0000 | Allocation Table | +------------------------------------+ 4000 | Data Page | +------------------------------------+ 8000 | Data Page | +------------------------------------+ c000 | Data Page | +------------------------------------+ ... |... | +------------------------------------+ """ def __init__(self, allocation_table, data_page_list): self.allocation_table = allocation_table self.data_page_list = data_page_list def debug_print(self, log = sys.stdout): log.write("Debug Print of MEFileSystem %x:\n" % id(self)) self.allocation_table.debug_print(log) for page in self.data_page_list: if(page): page.debug_print(log) else: log.write("(Not parsed)\n") def parse_me_fs_at_entry(me_file, log_file = sys.stdout): return MEFileSystemATEntry( state = read_leuint8(me_file), flags = read_leuint8(me_file), identifier = me_file.read(3), type = read_leuint8(me_file), filelen = read_leuint16(me_file), pgidx = read_leuint8(me_file), okey = read_leuint8(me_file), skip = read_leuint8(me_file)) def parse_me_fs_at_header(me_file, log_file = sys.stdout): return MEFileSystemATHeader( page_no = read_leuint8(me_file), flags = read_leuint24(me_file), bytes_00000000 = me_file.read(4), sig = me_file.read(4), bitfields = me_file.read(8)) def parse_me_fs_at(me_file, log_file = sys.stdout): hdr = parse_me_fs_at_header(me_file, log_file) entry_list = [None] * MEFileSystemATHeader.max_files for i in range(MEFileSystemATHeader.max_files): ent = parse_me_fs_at_entry(me_file, log_file) if ent.state == 0xff: break entry_list[i] = ent return MEFileSystemAT(hdr, entry_list) def parse_me_fs_data_header(me_file, log_file = sys.stdout): return MEFileSystemDataHeader( page_no = read_leuint8(me_file), flags = read_leuint24(me_file), bytes_00000000 = me_file.read(4), myst_bits = me_file.read(0x8), freed_flags = me_file.read(0x80), blk_itab = me_file.read(0x40)) def parse_me_fs(length, me_file, log_file = sys.stdout): """ ARGS: length -- length in bytes of the ME partition holding the MFS/MFSB data me_file -- a file handle whose present position is the start of the MFS(B) partition log_file -- if there is diagnostic output, put it here RETURNS: an MEFileSystem instance populated with the allocation table and data-page headers """ total_pages = int(length/0x4000) start_offset = me_file.tell() #NOTE: I'm presuming the allocation table is the first page... #that might not be reliable... at = parse_me_fs_at(me_file, log_file) data_pages = [None] * (total_pages-1) for page in range(0, total_pages-1): me_file.seek(start_offset + 0x4000 * (page+1)) data_pages[page] = parse_me_fs_data_header(me_file, log_file) return MEFileSystem( allocation_table=at, data_page_list = data_pages) def get_mfs_file(mefs, mfs_data, id, log = sys.stdout): """ Example of how to use the MEFileSystem structures to retrieve MFS and MFSB files. ARGS: mefs -- a MEFileSystem instance parsed from mfs_data mfs_data -- the entire MFS partition, as a byte_array id -- a 3-byte byte array with the file identifier log -- if there is diagnostic output, put it here RETURNS: an array containing [state, The data from the corresponding file]. else None if the file identifier does not exist within the data. Example driver, given the known offset and size for a MFS partition: MFS_OFFSET = 0x64000 #typical location MFS_LENGTH = 0x40000 #typical size spi_image_file.seek(MFS_OFFSET) mefs = parse_me_fs(MFS_LENGTH, spi_image_file) spi_image_file.seek(MFS_OFFSET) mfs_data = spi_image_file.read(MFS_LENGTH) result_tuple = get_mfs_file(mefs, mfs_data, b'UKS') if result_tuple: print("State: %x, data: %s\n" % tuple(result_tuple)) """ #Find the file identifer in the Allocation Table state = None filelen = None pgidx = None okey = None skip = None data = None for ent in mefs.allocation_table.entry_list: if ent and ent.identifier == id: state = ent.state filelen = ent.filelen pgidx = ent.pgidx okey = ent.okey skip = ent.skip if state == 0xdc: break; # got a current entry log.write("Error: found an item w/ state %02x...continuing\n" % state) #if found, lookup which data page matches the entry's pgidx value if state: page_found = False for list_idx in range(len(mefs.data_page_list)): page = mefs.data_page_list[list_idx] if page.page_no == pgidx: page_found = True #we found the right data page, so start the file search search_start = page.blk_itab[okey] * 0x100 #In the following lines: # The value d0 is to skip over the datapage header if we're in the first block # # The multiple of 0x4000 selects the data offset that goes with list_idx # since the parsed table list is in the same order as found in the file. # # Because mefs.data_page_list doesn't include the allocation table page, we +1 # to the index before multiplying. The result is a set of offsets into the MFS data # bounding the file search ## search_off = 0x4000 * (list_idx+1) + (0xd0 if search_start == 0 else search_start) search_end = 0x4000 * (list_idx+1) + search_start + 0x100 skipped = 0 while search_off < search_end: #have we eaten enough files yet? if skipped == skip: return [state, mfs_data[search_off:(search_off+filelen)]] #else consume until we eat another end-marker while mfs_data[search_off] != 0xff: search_off = search_off + 1 while mfs_data[search_off] == 0xff: search_off = search_off + 1 skipped = skipped + 1 log.write("Error: there weren't enough files to skip in the target block\n"); if not page_found: log.write("Error: corresponding data page doesn't exist (is this an new MFSB?)\n") else: log.write("Error: no file record was found matching the identifier\n") return None if __name__ == "__main__": with open("image.bin", "rb") as spi_image_file: MFS_OFFSET = 0x64000 #typical location MFS_LENGTH = 0x40000 #typical size spi_image_file.seek(MFS_OFFSET) mefs = parse_me_fs(MFS_LENGTH, spi_image_file) spi_image_file.seek(MFS_OFFSET) mfs_data = spi_image_file.read(MFS_LENGTH) result_tuple = get_mfs_file(mefs, mfs_data, b'UKS') if result_tuple: print("State: %x, data: %s\n" % tuple(result_tuple))