#!/usr/bin/env python2.7 # -*- coding: utf-8 -*- """Display general details of a FAT file system.""" #=============================================================================== # # FILE: # ./fatmapper.py # # BASIC USAGE: # $ ./fatmapper.py [-h|--help] [-o|--offset OFFSET] image # OR # $ python fatmapper.py [-h|--help] [-o|--offset OFFSET] image # # OPTIONS: # -h, # --help show help message and exit # -o OFFSET, # --offset OFFSET offset in sectors (default=0) # image raw image file (esp. dd) # # EXIT STATES: # 0 = success # 1 = Python version not tested/supported # 2 = image file does not exist # 3 = offset is not a positive integer # 4 = wrong or missing data in dictionary # 5 = empty image file # 6 = image file smaller than offset # 7 = invalid vbr signatures # # REQUIREMENTS: # python2.7 # # NOTES: # Tested on: # - ArchLinux (64-Bit) + python 2.7.13 # - Raspbian GNU/Linux 8.0 (32-Bit) + python 2.7.9 # - macOS (10.12.3) + python 2.7.10 # - Bash on Ubuntu on Windows 10 (64-Bit) + python 2.7.6 # with: # - raw images # - filesystem(s) in partition(s) # - filesystem without a partition # - no partition/no filesystem (empty file) # - invalid filesystem # - FAT12, FAT16 and FAT32 # # WARRANTY: # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # See the LICENSE file for more details. # # HISTORY: # See the CHANGES file for more details. # #=============================================================================== #=== MODULES =================================================================== import argparse import hashlib import os import os.path import re import struct import sys #=== INFO ====================================================================== """@author: Patrick Neumann @contact: patrick@neumannsland.de @copyright: Copyright (C) 2017, Patrick Neumann @license: GNU General Public License 3.0 @date: 2017-03-26 @version: 1.0.0 @status: Development """ #=== CHECKS ==================================================================== """Only tested with Python versions 2.7.9, 2.7.10 and 2.7.13! Python 3.x is not supported! """ if sys.version_info < (2, 7, 0): sys.stderr.write("Sorry, your Python version was not tested but may work?\n") sys.exit(1) if sys.version_info >= (3, 0, 0): sys.stderr.write("Error: Python 3.x is not supported!\n") sys.exit(1) #=== FUNCTIONS ================================================================= # This is a private module function only for the other module functions. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this module. :-( # sphinx-apidoc does ignore it. :-) def _checkimage(_file): """Check image file. Check if the image is a existing file. Parameters ---------- _file : str path to the image file. Raises ------ Exits if the image does not exist. """ if not os.path.isfile(_file): sys.stderr.write("Error: image file does not exist!\n") sys.exit(2) #------------------------------------------------------------------------------- # This is a private module function only for the other module functions. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this module. :-( # sphinx-apidoc does ignore it. :-) def _checkoffset(loffset): """Check offset. Check if the offset is an integer and is equal or greater than zero. Parameters ---------- loffset : int offset in bytes in the image file. Raises ------ Exits if the offset is not equal or greater than zero. """ if not isinstance(loffset, int): sys.stderr.write("Error: offset is not an integer!\n") sys.exit(3) if not loffset >= 0: sys.stderr.write("Error: offset is not equal or greater zero!\n") sys.exit(3) #------------------------------------------------------------------------------- # This is a private module function only for the other module functions. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this module. :-( # sphinx-apidoc does ignore it. :-) def _checkdict(check, _dict): """Check dictionary. Check if the dictionary contains all needed data and that the data has the right type. Parameters ---------- check : dict) dictionary with necessary keys and types of the data dictionary. _dict : dict dictionary with mapping of parts of a fat VBR to a human readable index. Raises ------ Exits if the data dictionary does not contain the necessary keys or the type of data is incorrect. """ for key, var in check.items(): if not key in _dict: sys.stderr.write("Error: necessary data is missing in dictionary!\n") sys.exit(4) # attention: on 32-Bit systems there is more often used long # where on 64-Bit systems is int big enough: if not isinstance(_dict[key], var): sys.stderr.write("Error: invalid type of data in dictionary!\n") sys.exit(4) #------------------------------------------------------------------------------- # This is a private module function only for the other module functions. # Unfortunately, the underscore does not prevent other developers from using # it directly from outside this module. :-( # sphinx-apidoc does ignore it. :-) def _getargs(): """Get command line arguments. Parses the command line arguments, checks and return them. Returns ------- tuple A tuple containing the checked path to the image file and the offset in bytes in the image file calculated from sectors. The default offset is 0. For example: ( "fat12_in_partition.dd", 1048576 ) """ parser = argparse.ArgumentParser() parser.add_argument("image", type=str, help="raw image file (esp.: image.dd)") parser.add_argument("-o", "--offset", type=int, default=0, help="offset in sectors (default=0)") args = parser.parse_args() _checkimage(args.image) limage = args.image _checkoffset(args.offset) loffset = args.offset * 512 return (limage, loffset) #------------------------------------------------------------------------------- def getsize(_file, loffset): """Get image file size. Get size of the image file. Parameters ---------- _file : str path to the image file. loffset : int offset in bytes in the image file. Returns ------- int size of image file in bytes. Raises ------ Exits if image file is empty or smaller than offset. """ _checkimage(_file) _checkoffset(loffset) lsize = os.stat(_file)[6] if lsize == 0: sys.stderr.write("Error: the image file is an empty file!\n") sys.exit(5) if lsize < loffset + 512: sys.stderr.write("Error: size of the image file is smaller than offset (+ 512 byte)!\n") sys.exit(6) return lsize #------------------------------------------------------------------------------- def getmd5(_file): """Get MD5 of image file. Get MD5 digest in hex for the content of the image file. Parameters ---------- _file : str path to the image file. Returns ------- str MD5 digest in uppercase hex. """ _checkimage(_file) lfile = open(_file, "rb") lmd5 = hashlib.md5(lfile.read()).hexdigest().upper() lfile.close() return lmd5 #------------------------------------------------------------------------------- def checkvbr(_file, loffset): """Check for FAT VBR. Check for the existence of a valid FAT12/16/32 VBR. Parameters ---------- _file : str path to the image file. loffset : int offset in bytes in the image file. Returns ------- str file_system_type (FAT1X or FAT32) Raises ------ Exits if the VBR can not be verified as a FAT12/16/32 VBR. """ _checkimage(_file) _checkoffset(loffset) lfile = open(_file, "rb") lfile.seek(loffset + 38) extended_signature = lfile.read(1) if extended_signature == "\x29": lfile.seek(15, 1) file_system_type = lfile.read(8) lfile.seek(448, 1) vbr_signature = lfile.read(2) file_system_type_switch = "FAT1X" else: lfile.seek(loffset + 66) extended_signature = lfile.read(1) lfile.seek(15, 1) file_system_type = lfile.read(8) lfile.seek(420, 1) vbr_signature = lfile.read(2) file_system_type_switch = "FAT32" lfile.close() condition1 = extended_signature == "\x29" condition2 = re.search(r'FAT(1(2|6)|32) ', file_system_type) condition3 = vbr_signature == "\x55\xaa" if not (condition1 and condition2 and condition3): sys.stderr.write("Error: no valid FAT12/FAT16/FAT32 VBR found!\n") sys.exit(7) #print "fileSystemTypeSwitch:", fileSystemTypeSwitch return file_system_type_switch #------------------------------------------------------------------------------- def getraw(_file, loffset): """Get global data. Read raw data from the VBR into a python dictionary. Parameters ---------- _file : str path to the image file. loffset : int offset in bytes in the image file. Returns ------- dict dictionary with mapping of parts of a fat VBR to a human readable index. For example: {"bytesPerSector": 512, "sectorsPerCluster": 64, "reservedSectors": 1, "fatCopies": 2, "rootDirectoryEntries": 1024, "sectorsInPartition": 0, "rawMediaDescriptor": 248, "sectorsPerFat": 64, "sectorsPerTrack": 32, "numberOfHeads": 64, "hiddenSectors": 0, "largerSectorsInPartition": 260096} """ _checkimage(_file) _checkoffset(loffset) keys = ["bytesPerSector", "sectorsPerCluster", "reservedSectors", "fatCopies", "rootDirectoryEntries", "sectorsInPartition", "rawMediaDescriptor", "sectorsPerFat", "sectorsPerTrack", "numberOfHeads", "hiddenSectors", "largerSectorsInPartition"] lfile = open(_file, "rb") lfile.seek(loffset + 3) # just skip the jumpcode oem_name_version = lfile.read(8) raw = lfile.read(2 + 1 + 2 + 1 + 2 + 2 + 1 + 2 + 2 + 2 + 4 + 4) values = struct.unpack_from("= 2 and cluster <= 268435446: if cluster > _max: _max = cluster if cluster < _min: _min = cluster counter = counter + 1 _next = loffset + (_dict["reservedSectors"] * _dict["bytesPerSector"]) + (cluster * 4) if not lfile.tell() == _next: lfile.seek(_next - lfile.tell(), 1) raw = lfile.read(4) cluster = struct.unpack_from("