fatmapper/fatmapper.py
2018-06-15 21:16:53 +02:00

859 lines
26 KiB
Python
Executable File

#!/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("<HBHBHHBHHHII", raw)
_dict = dict(zip(keys, values))
_dict["oemNameVersion"] = oem_name_version
lfile.close()
#for k, v in _dict.items():
# print k, ":", v
return _dict
#-------------------------------------------------------------------------------
def getraw1x(_file, loffset):
"""Get FAT12/16 spezial 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:
{"logicalDriveNumber": 128,
"RESERVED": 0,
"extendedSignature": 41,
"rawVolumeSerialNumber": 549101972,
"oemNameVersion": "mkfs.fat",
"volumeLabel": "NO NAME ",
"rawFileSystemType": "FAT12 ",
"vbrSignature": "\x55\xaa"}
"""
_checkimage(_file)
_checkoffset(loffset)
keys = ["logicalDriveNumber", "RESERVED", "extendedSignature",
"rawVolumeSerialNumber"]
lfile = open(_file, "rb")
lfile.seek(loffset + 36) # just skip the global area
raw = lfile.read(1 + 1 + 1 + 4)
values = struct.unpack_from("<BBBI", raw)
_dict = dict(zip(keys, values))
_dict["volumeLabel"] = lfile.read(11)
_dict["rawFileSystemType"] = lfile.read(8)
lfile.seek(448, 1) # absolute offset 510 - actual offset 62 = relative offset 448
_dict["vbrSignature"] = lfile.read(2)
lfile.close()
#for k, v in _dict.items():
# print k, ":", v
return _dict
#-------------------------------------------------------------------------------
def getraw32(_file, loffset):
"""Get FAT32 spezial 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:
{"sectorsPerFat": 947,
"mirrorFlags": 0,
"fileSystemVersion": 0,
"firstClusterOfRootDirectory": 2,
"fsinfoSector": 1,
"backupBootSector": 6,
"logicalDriveNumber": 128,
"RESERVED": 0,
"extendedSignature": 41,
"rawVolumeSerialNumber": 774266948,
"volumeLabel": "NO NAME ",
"rawFileSystemType": "FAT32 ",
"vbrSignature": "\x55\xaa"}
"""
_checkimage(_file)
_checkoffset(loffset)
keys1 = ["sectorsPerFat", "mirrorFlags", "fileSystemVersion",
"firstClusterOfRootDirectory", "fsinfoSector", "backupBootSector"]
keys2 = ["logicalDriveNumber", "RESERVED", "extendedSignature",
"rawVolumeSerialNumber"]
lfile = open(_file, "rb")
lfile.seek(loffset + 36) # just skip the global area
raw1 = lfile.read(4 + 2 + 2 + 4 + 2 + 2)
values1 = struct.unpack_from("<IHHIHH", raw1)
_dict = dict(zip(keys1, values1))
lfile.seek(12, 1)
raw2 = lfile.read(1 + 1 + 1 + 4)
values2 = struct.unpack_from("<BBBI", raw2)
_dict.update(dict(zip(keys2, values2)))
_dict["volumeLabel"] = lfile.read(11)
_dict["rawFileSystemType"] = lfile.read(8)
lfile.seek(420, 1) # absolute offset 510 - actual offset 90 = relative offset 420
_dict["vbrSignature"] = lfile.read(2)
lfile.close()
#for k, v in _dict.items():
# print k, ":", v
return _dict
#-------------------------------------------------------------------------------
def transform(_dict):
"""Transform data.
Do some modifications to the raw data.
Parameters
----------
_dict : dict
dictionary with mapping of parts of a fat VBR to a human readable index.
Returns
-------
dict
dictionary with mapping of transformed parts of a fat VBR to a human
readable index.
"""
check = {"bytesPerSector": (int, long),
"sectorsPerCluster": (int, long),
"rawMediaDescriptor": (int, long),
"sectorsInPartition": (int, long),
"largerSectorsInPartition": (int, long),
"rawFileSystemType": str,
"rawVolumeSerialNumber": (int, long)}
_checkdict(check, _dict)
_dict["bytesPerCluster"] = _dict["bytesPerSector"] * _dict["sectorsPerCluster"]
if _dict["rawMediaDescriptor"] == 240:
_dict["mediaDescriptor"] = "floppy disk"
elif _dict["rawMediaDescriptor"] == 248:
_dict["mediaDescriptor"] = "hard disk"
else:
sys.stderr.write("Error: no valid media descriptor found!\n")
sys.exit(2)
if _dict["sectorsInPartition"] == 0:
_dict["sectorsInPartition"] = _dict["largerSectorsInPartition"]
_dict["fileSystemType"] = _dict["rawFileSystemType"][:3] + " " + _dict["rawFileSystemType"][3:]
_dict["volumeSerialNumber"] = "0x" + '{:08X}'.format(_dict["rawVolumeSerialNumber"])
#for k, v in _dict.items():
# print k, ":", v
return _dict
#-------------------------------------------------------------------------------
def getrootdirsize32(_file, loffset, _dict):
"""Get FAT32 root directory informations.
Read raw data about the root directory from the FAT32 VBR into a python dictionary.
Parameters
----------
loffset : int
offset in bytes in the image file.
_dict :dict
dictionary with mapping of parts of a fat VBR to a human readable index.
Returns
-------
dict
dictionary with start, end and size of the root directory.
"""
_checkoffset(loffset)
check = {"reservedSectors": (int, long),
"bytesPerSector": (int, long),
"firstClusterOfRootDirectory": (int, long),
"sectorsPerFat": (int, long),
"sectorsPerCluster": (int, long)}
_checkdict(check, _dict)
lfile = open(_file, "rb")
lfile.seek(loffset +
(_dict["reservedSectors"] * _dict["bytesPerSector"]) +
(_dict["firstClusterOfRootDirectory"] * 4))
raw = lfile.read(4)
cluster = struct.unpack_from("<I", raw)[0]
_min = _dict["firstClusterOfRootDirectory"]
_max = _min
counter = 1
while cluster >= 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("<I", raw)[0]
lfile.close()
# In the FAT the root directory starts in the entry 2 (0 = label and 1 = reserved)
# but in the data area it starts in cluster in the first (= 0)!
# To find the root directory in the FAT you have to use the entry in the VBR.
# To find the root directory in the data area you have to use the entry in the VBR - 2!
root_directory = {"rootDirectoryStartSector": _dict["reservedSectors"] +
2 * _dict["sectorsPerFat"] + _min - 2,
"rootDirectoryEndSector": _dict["reservedSectors"] +
2 * _dict["sectorsPerFat"] +
_max -
2 +
_dict["sectorsPerCluster"] -
1,
"rootDirectorySize": counter * _dict["sectorsPerCluster"]}
#for k, v in rootDirectory.items():
# print k, ":", v
return root_directory
#-------------------------------------------------------------------------------
def calculate(_file, loffset, _dict):
"""Calcuate data.
Do some calculations with the raw/modified data.
Parameters
----------
_dict : dict
dictionary with mapping of transformed parts of a fat VBR to
a human readable index.
Returns
-------
dict
dictionary with mapping of transformed parts and added calculations of
a fat VBR to a human readable index.
"""
_checkoffset(loffset)
check = {"reservedSectors": (int, long),
"sectorsPerFat": (int, long),
"sectorsInPartition": (int, long),
"rootDirectoryEntries": (int, long),
"bytesPerSector": (int, long)}
_checkdict(check, _dict)
_dict["FAT0StartSector"] = _dict["reservedSectors"]
_dict["FAT0EndSector"] = _dict["reservedSectors"] + _dict["sectorsPerFat"] - 1
_dict["FAT1StartSector"] = _dict["FAT0EndSector"] + 1
_dict["FAT1EndSector"] = _dict["FAT1StartSector"] + _dict["sectorsPerFat"] - 1
_dict["DataAreaStartSector"] = _dict["FAT1EndSector"] + 1
_dict["DataAreaEndSector"] = _dict["sectorsInPartition"] - 1
_dict["DataAreaSize"] = _dict["sectorsInPartition"] - _dict["DataAreaStartSector"]
if _dict["rootDirectoryEntries"] != 0:
_dict["rootDirectoryStartSector"] = _dict["DataAreaStartSector"]
_dict["rootDirectorySize"] = _dict["rootDirectoryEntries"] * 32 / _dict["bytesPerSector"]
_dict["rootDirectoryEndSector"] = (_dict["DataAreaStartSector"] +
_dict["rootDirectorySize"] - 1)
else:
_dict.update(getrootdirsize32(_file, loffset, _dict))
#for k, v in _dict.items():
# print k, ":", v
return _dict
#-------------------------------------------------------------------------------
def getlabel2(_file, loffset, _dict):
"""Get FAT32 root directory data.
Read raw data about the label from the FAT32 root directory into a python dictionary.
Parameters
----------
loffset : int
offset in bytes in the image file.
_dict : dict
dictionary with mapping of parts of a fat VBR to a human readable index.
Returns
-------
str
volume label from root directory
"""
_checkoffset(loffset)
check = {"rootDirectoryStartSector": (int, long),
"bytesPerSector": (int, long)}
_checkdict(check, _dict)
lfile = open(_file, "rb")
lfile.seek(loffset + _dict["rootDirectoryStartSector"] * _dict["bytesPerSector"])
volume_label2 = lfile.read(11)
lfile.close()
#print "volumeLabel2:", volumeLabel2
return volume_label2
#-------------------------------------------------------------------------------
def main(_file, loffset, _dict):
"""Print data.
Insert the data into the template and print it on stdout.
Parameters
----------
_file : str
path to the image file.
loffset : int
offset in bytes in the image file.
_dict : dict
dictionary with mapping of transformed parts and added
calculations of a fat VBR to a human readable index.
"""
_checkimage(_file)
_checkoffset(loffset)
check = {"size": (int, long),
"md5": str,
"volumeLabel": str,
"oemNameVersion": str,
"fileSystemType": str,
"volumeSerialNumber": str,
"bytesPerSector": (int, long),
"bytesPerCluster": (int, long),
"sectorsPerCluster": (int, long),
"sectorsInPartition": (int, long),
"fatCopies": (int, long),
"reservedSectors": (int, long),
"FAT0StartSector": (int, long),
"FAT0EndSector": (int, long),
"sectorsPerFat": (int, long),
"FAT1StartSector": (int, long),
"FAT1EndSector": (int, long),
"DataAreaStartSector": (int, long),
"DataAreaEndSector": (int, long),
"DataAreaSize": (int, long),
"rootDirectoryStartSector": (int, long),
"rootDirectoryEndSector": (int, long),
"rootDirectorySize": (int, long)}
_checkdict(check, _dict)
tpl24_1 = "{:<24}{}"
tpl24_1b = "{:<24}{} bytes"
tpl24_2 = "{:<24}{} bytes ({} Sectors)"
tpl24_3 = "{:<24}{} - {} ({} Sectors)"
tpl21 = "{:<21}{} - {} ({} Sectors)"
print "File Information"
print tpl24_1.format("-- Name:",
_file)
print tpl24_1.format("-- File Size:",
_dict["size"])
print tpl24_1.format("-- MD5:",
_dict["md5"])
print
print "FS Information"
print tpl24_1.format("-- Volume Label:",
_dict["volumeLabel"])
if _dict["fileSystemType"] == "FAT 32 ":
print tpl24_1.format("-- Volume Label #2:",
getlabel2(_file, loffset, _dict))
print tpl24_1.format("-- OEM Name:",
_dict["oemNameVersion"])
print tpl24_1.format("-- File System:",
_dict["fileSystemType"])
print tpl24_1.format("-- Volume ID:",
_dict["volumeSerialNumber"])
print
print tpl24_1b.format("-- Sector Size:",
_dict["bytesPerSector"])
print tpl24_2.format("-- Cluster Size:",
_dict["bytesPerCluster"],
_dict["sectorsPerCluster"])
print tpl24_1.format("-- Number of Sectors:",
_dict["sectorsInPartition"])
print tpl24_1.format("-- Number of FATs:",
_dict["fatCopies"])
print tpl24_3.format("-- Total Range:",
0,
int(_dict["sectorsInPartition"]) - 1,
_dict["sectorsInPartition"])
print
print "FS Layout"
print tpl21.format("-- Reserved:",
0,
int(_dict["reservedSectors"]) - 1,
_dict["reservedSectors"])
print tpl21.format("---- VBR:", 0, 0, 1)
print tpl21.format("-- FAT 0:",
_dict["FAT0StartSector"],
_dict["FAT0EndSector"],
_dict["sectorsPerFat"])
print tpl21.format("-- FAT 1:",
_dict["FAT1StartSector"],
_dict["FAT1EndSector"],
_dict["sectorsPerFat"])
print tpl21.format("-- Data Area:",
_dict["DataAreaStartSector"],
_dict["DataAreaEndSector"],
_dict["DataAreaSize"])
print tpl21.format("---- Root Dir:",
_dict["rootDirectoryStartSector"],
_dict["rootDirectoryEndSector"],
_dict["rootDirectorySize"])
#=== MAIN ======================================================================
if __name__ == '__main__':
(IMAGE, OFFSET) = _getargs()
SIZE = getsize(IMAGE, OFFSET) # has to be done after _getargs()!
SWITCH = checkvbr(IMAGE, OFFSET)
DATA = getraw(IMAGE, OFFSET)
if SWITCH == "FAT1X":
DATA.update(getraw1x(IMAGE, OFFSET))
else:
DATA.update(getraw32(IMAGE, OFFSET))
DATA["size"] = SIZE
DATA["md5"] = getmd5(IMAGE)
DATA = transform(DATA)
DATA = calculate(IMAGE, OFFSET, DATA)
main(IMAGE, OFFSET, DATA)
sys.exit(0)