diff --git a/AUTHORS b/AUTHORS new file mode 100755 index 0000000..b09a825 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +fatlister is written and maintained by Patrick Neumann . diff --git a/CHANGES b/CHANGES new file mode 100755 index 0000000..00b00ad --- /dev/null +++ b/CHANGES @@ -0,0 +1,21 @@ +Release 1.0 (in development) +============================ + + +(Incompatible) changes +---------------------- + +* none + + +Features added +-------------- + +* none + + +Bugs fixed +---------- + +* none + diff --git a/cf64mbwin.dd b/cf64mbwin.dd new file mode 100755 index 0000000..e09e7a3 Binary files /dev/null and b/cf64mbwin.dd differ diff --git a/cf64mbwin.info b/cf64mbwin.info new file mode 100755 index 0000000..88c5928 --- /dev/null +++ b/cf64mbwin.info @@ -0,0 +1,114 @@ + +GUYMAGER ACQUISITION INFO FILE +============================== + +Guymager +======== + +Version : 0.8.4-1 +Compilation timestamp: 2017-02-02-16.19.12 +Compiled with : gcc 4.9.2 +libewf version : 20140608 +libguytools version : 2.1.0beta5 +Host name : host10 +Domain name : (none) +System : Linux host10 3.16.0-4-amd64 #1 SMP Debian 3.16.39-1+deb8u2 (2017-03-07) x86_64 + + +Device information +================== +Command executed: bash -c "search="`basename /dev/sdc`: H..t P.......d A..a de.....d" && dmesg | grep -A3 "$search" || echo "No kernel HPA messages for /dev/sdc"" +Information returned: +---------------------------------------------------------------------------------------------------- + No kernel HPA messages for /dev/sdc + +Command executed: bash -c "smartctl -s on /dev/sdc ; smartctl -a /dev/sdc" +Information returned: +---------------------------------------------------------------------------------------------------- + smartctl 6.4 2014-10-07 r4002 [x86_64-linux-3.16.0-4-amd64] (local build) + Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org + + /dev/sdc: Unknown USB bridge [0x0bda:0x0184 (0x8413)] + Please specify device type with the -d option. + + Use smartctl -h to get a usage summary + + smartctl 6.4 2014-10-07 r4002 [x86_64-linux-3.16.0-4-amd64] (local build) + Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org + + /dev/sdc: Unknown USB bridge [0x0bda:0x0184 (0x8413)] + Please specify device type with the -d option. + + Use smartctl -h to get a usage summary + +Command executed: bash -c "hdparm -I /dev/sdc" +Information returned: +---------------------------------------------------------------------------------------------------- + SG_IO: bad/missing sense data, sb[]: 70 00 05 00 00 00 00 0a 00 00 00 00 24 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + /dev/sdc: + + ATA device, with non-removable media + Standards: + Likely used: 1 + Configuration: + Logical max current + cylinders 0 0 + heads 0 0 + sectors/track 0 0 + -- + Logical/Physical Sector size: 512 bytes + device size with M = 1024*1024: 0 MBytes + device size with M = 1000*1000: 0 MBytes + cache/buffer size = unknown + Capabilities: + IORDY not likely + Cannot perform double-word IO + R/W multiple sector transfer: not supported + DMA: not supported + PIO: pio0 + +Hidden areas: unknown + + +Acquisition +=========== + +Linux device : /dev/sdc +Device size : 65536000 (65,5MB) +Format : Linux split dd raw image - file extension is .xxx +Image path and file name: /home/user10/Dokumente/cf64mbwin.xxx +Info path and file name: /home/user10/Dokumente/cf64mbwin.info +Hash calculation : MD5 and SHA-1 +Source verification : on +Image verification : on + +No bad sectors encountered during acquisition. +No bad sectors encountered during verification. +State: Finished successfully + +MD5 hash : 9073705f04e50e70790cddf48a1d0413 +MD5 hash verified source : 9073705f04e50e70790cddf48a1d0413 +MD5 hash verified image : 9073705f04e50e70790cddf48a1d0413 +SHA1 hash : 08165d91b9f2d84581ecc087abf36855fb721da3 +SHA1 hash verified source : 08165d91b9f2d84581ecc087abf36855fb721da3 +SHA1 hash verified image : 08165d91b9f2d84581ecc087abf36855fb721da3 +SHA256 hash : -- +SHA256 hash verified source: -- +SHA256 hash verified image : -- +Source verification OK. The device delivered the same data during acquisition and verification. +Image verification OK. The image contains exactly the data that was written. + +Acquisition started : 2017-05-08 15:10:06 (ISO format YYYY-MM-DD HH:MM:SS) +Verification started: 2017-05-08 15:10:24 +Ended : 2017-05-08 15:10:43 (0 hours, 0 minutes and 37 seconds) +Acquisition speed : 3.47 MByte/s (0 hours, 0 minutes and 18 seconds) +Verification speed : 3.47 MByte/s (0 hours, 0 minutes and 18 seconds) + + +Generated image files and their MD5 hashes +========================================== + +No MD5 hashes available (configuration parameter CalcImageFileMD5 is off) +MD5 Image file +n/a cf64mbwin.000 diff --git a/doc/fatlister.pdf b/doc/fatlister.pdf new file mode 100755 index 0000000..3382fb2 Binary files /dev/null and b/doc/fatlister.pdf differ diff --git a/fatlister.py b/fatlister.py new file mode 100755 index 0000000..66ff525 --- /dev/null +++ b/fatlister.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""List file and directory names in a FAT16 file system (disk image).""" + +#=============================================================================== +# +# FILE: +# ./fatlister.py +# +# BASIC USAGE: +# $ ./fatlister.py [-h|--help] [-o|--offset OFFSET] [-f|--file FILENAME] IMAGE +# OR +# $ python3 fatlister.py [-h|--help] [-o|--offset OFFSET] [-f|--file FILENAME] IMAGE +# +# OPTIONS: +# -h, +# --help show help message and exit +# -o OFFSET, +# --offset OFFSET offset in sectors (default=0) +# -f FILENAME, write output to file (default=unset) +# --file FILENAME if unset output goes to stdout +# IMAGE raw image file (esp. dd) +# +# EXIT STATES: +# 0 = success +# 1 = Python version not tested/supported +# 2 = states thrown by argparse: +# - IMAGE not passed to fatlister +# - unknown argument passed to fatlister +# - OFFSET is not an integer +# 3 = image file does not exist +# 4 = file is not readable +# 5 = offset is not a positive integer +# 6 = output file already exist +# 7 = output directory is not writable +# 8 = empty image file +# 9 = image file smaller than offset +# 10 = invalid vbr signatures +# +# REQUIREMENTS: +# python3.4+ +# +# NOTES: +# Tested on: +# - ArchLinux (64-Bit) + python 3.6.1 +# - Raspbian GNU/Linux 8.0 (32-Bit) + python 3.4.2 +# - macOS (10.12.4) + python 3.6.1 (from Homebrew) +# - Bash on Ubuntu on Windows 10 "1703" (64-Bit) + python 3.5.2 +# with: +# - raw images +# - filesystem(s) in partition(s) <--- TODO! +# - filesystem without a partition +# - no partition/no filesystem (empty file) +# - invalid filesystem +# - FAT16 +# +# 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 os +import os.path +import re +import struct +import sys +import time + +#=== 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.1 +@status: Development +""" + +#=== CHECKS ==================================================================== + +"""Only tested with Python versions 3.4.x - 3.6.x!""" + +if sys.version.startswith("2"): + sys.stderr.write("Python version 2.x is not supportet!\n") + sys.exit(1) + +if sys.version_info < (3, 4, 0): + sys.stderr.write("Sorry, your Python version was not tested but may work?\n") + sys.exit(1) + +#=== CLASS ===================================================================== + +class Fatlister: + def __init__(self, image, offset): + self._checkimage(image) + self.image = image + self._checkoffset(offset) + self.offset = offset + self._checkvbr() + self.output = ['# This RFC4180 compliant file was created with fatlister.py at ' + time.strftime("%Y-%m-%dT%H:%M:%S") + '.', + '"id", "path", "name", "LFN", "Allocated", "Size", "Accessed", "Modified", "Created", "Starting Cluster"'] + self._processvbr() + self._readfiles() + +#------------------------------------------------------------------------------- + + # This is a private method for usage in this class only. + # Unfortunately, the underscore does not prevent other developers from using + # it directly from outside this class. :-( + # sphinx-apidoc does ignore it. :-) + def _checkimage(self, _file): + """Check image file. + + Check if the image is a existing and readable file. + + Parameters + ---------- + _file : str + path to the image file. + + Raises + ------ + Exits if the image does not exist or is not readable. + """ + + if not os.path.isfile(_file): + sys.stderr.write("Error: image file does not exist!\n") + sys.exit(3) + + size = os.stat(_file)[6] + if size == 0: + sys.stderr.write("Error: the image file is an empty file!\n") + sys.exit(8) + + # Check if image is readable the first time if we need to read it! + +#------------------------------------------------------------------------------- + + # This is a private method for usage in this class only. + # Unfortunately, the underscore does not prevent other developers from using + # it directly from outside this class. :-( + # sphinx-apidoc does ignore it. :-) + def _checkoffset(self, offset): + """Check offset. + + Check if the offset is an integer and is equal or greater than zero. + + Parameters + ---------- + offset : int + offset in bytes in the image file. + + Raises + ------ + Exits if the offset is not equal or greater than zero. + """ + + if not offset >= 0: + sys.stderr.write("Error: offset is not equal or greater zero!\n") + sys.exit(5) + + size = os.stat(self.image)[6] + if size < offset + 512: + sys.stderr.write("Error: size of the image file is smaller than offset (+ 512 byte)!\n") + sys.exit(9) + +#------------------------------------------------------------------------------- + + # This is a private method for usage in this class only. + # Unfortunately, the underscore does not prevent other developers from using + # it directly from outside this class. :-( + # sphinx-apidoc does ignore it. :-) + def _checkvbr(self): + """Check for FAT VBR. + + Check for the existence of a valid FAT12/16 VBR. + + Parameters + ---------- + self.image : str + path to the image file. + + self.offset : int + offset in bytes in the image file. + + Raises + ------ + Exits if the VBR can not be verified as a FAT12/16 VBR. + """ + + try: + with open(self.image, "rb") as isreadable: + isreadable.seek(self.offset + 38) + extended_signature = isreadable.read(1) + isreadable.seek(15, 1) + file_system_type = isreadable.read(8) + isreadable.seek(448, 1) + vbr_signature = isreadable.read(2) + except IOError: + sys.stderr.write("Error: image file is not readable!\n") + sys.exit(4) + + condition1 = extended_signature == b"\x29" + condition2 = re.search(r'FAT1(2|6) ', file_system_type.decode("ascii")) + condition3 = vbr_signature == b"\x55\xaa" + + if not (condition1 and condition2 and condition3): + sys.stderr.write("Error: no valid FAT12/FAT16 VBR found!\n") + sys.exit(10) + +#------------------------------------------------------------------------------- + + def printoutput(self): + """Print to stdout. + + Prints every item of self.output to stdout. + + Parameters + ---------- + self.output : list + List with every line of output as a item. + """ + + for line in self.output: + print(line) + +#------------------------------------------------------------------------------- + + def writeoutput(self): + """Write to file. + + Writes every item of self.output to a file. + + Parameters + ---------- + self.output : list + List with every line of output as a item. + + Raises + ------ + Exits if output directory is not writable. + """ + + try: + with open(self.file, "w") as iswritable: + for line in self.output: + iswritable.write(line + "\n") + except IOError: + sys.stderr.write("Error: output directory is not writable!\n") + sys.exit(7) + +#------------------------------------------------------------------------------- + + # This is a private method for usage in this class only. + # Unfortunately, the underscore does not prevent other developers from using + # it directly from outside this class. :-( + # sphinx-apidoc does ignore it. :-) + def _processvbr(self): + """Process FAT12/FAT16 VBR. + + Read, translate and calculate needed information out of the vbr and + write it into single attributes. + """ + + # We have already checked if we are able to read the image. + with open(self.image, "rb") as image: + image.seek(self.offset + 11) + raw = image.read(2 + 1 + 2 + 1 + 2) + parts = struct.unpack_from("> 9) + month = "{:02}".format((0b0000000111100000 & unsignedShort) >> 5) + day = "{:02}".format(0b0000000000011111 & unsignedShort) + return (year, month, day) + +#------------------------------------------------------------------------------- + + def _calculatetime(unsignedShort): + # TODO: + # candidate for external use? + # add test for unignedShort + # add comment + hours = "{:02}".format((0b1111100000000000 & unsignedShort) >> 11) + minutes = "{:02}".format((0b0000011111100000 & unsignedShort) >> 5) + seconds = "{:02}".format((0b0000000000011111 & unsignedShort) * 2) + return (hours, minutes, seconds) + +#------------------------------------------------------------------------------- + + # This is a private method for usage in this class only. + # Unfortunately, the underscore does not prevent other developers from using + # it directly from outside this class. :-( + # sphinx-apidoc does ignore it. :-) + def _readfiles(self): + """Loop over directories and files. + + Loop over all directories and add each file with SFN and optionally + LFN to the list (self.outout). + + TODO + ---- + 1. Move subloops to separate methods!? + """ + + directoryOffsets = [self.offset + self.firstRootDirectoryEntryLocation] + directories = {self.offset + self.firstRootDirectoryEntryLocation: "/"} + _id = 0 + longFileName = "" + + # We have already checked if we are able to read the image. + with open(self.image, "rb") as image: + for directoryOffset in directoryOffsets: + fileOffset = directoryOffset + for entry in range(int(self.bytesPerCluster / 32)): + image.seek(fileOffset) + filename = image.read(8) + # skip . or ..: + if re.search(b"^\x2e\x20{7}|\x2e{2}\x20{6}", filename): + fileOffset = fileOffset + 32 + continue + # if unused: + elif re.search(b"^\x00", filename): + break + # if deleted: + elif re.search(b"^\xe5", filename): + filename = b"\x3f" + filename[1:] # deleted (sigma) to ? + allocated = "deleted" + else: + # if the filename starts with "sigma": + if re.search(b"^\x05", filename): + filename = b"\xe5" + filename[1:] + allocated = "allocated" + filenameExtension = image.read(3) + fileAttributes = struct.unpack_from("H", struct.unpack_from("H", struct.unpack_from("H", struct.unpack_from(" 65526: + break + fileOffset = fileOffset + 32 + continue + fileSize = parts[3] + self.output.append('"' + str(_id) + '", ' \ + + '"' + directories[directoryOffset] + '", ' \ + + '"' + filename + '", ' \ + + '"' + longFileName + '", ' \ + + '"' + allocated + '", ' \ + + '"' + str(fileSize) + '", ' \ + + '"' + str(accessedYear) + '-' + str(accessedMonth) + '-' + str(accessedDay) + '", ' \ + + '"' + str(modifiedYear) + '-' + str(modifiedMonth) + '-' + str(modifiedDay) + 'T' + str(modifiedHours) + ':' + str(modifiedMinutes) + ':' + str(modifiedSeconds) + '", ' \ + + '"' + str(createdYear) + '-' + str(createdMonth) + '-' + str(createdDay) + 'T' + str(createdHours) + ':' + str(createdMinutes) + ':' + str(createdSeconds) + '", ' \ + + '"' + str(startingCluster) + '"') + # if we don't have a LFN: + if not fileAttributes == 15: + longFileName = "" + _id = _id + 1 + fileOffset = fileOffset + 32 + +#=== 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 _getargs(): + """Get command line arguments. + + Parses the command line arguments and return them. + (Don't forget to check them in the next steps?) + + Returns + ------- + tuple + A tuple containing the to the image file, + the offset in bytes in the image file calculated from sectors + and the output file. + + The default offset is 0. + + For example: + ( "image.dd", 2048, "output.csv" ) + + Raises + ------ + Exits if argparse detects an error. + """ + + 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)") + parser.add_argument("-f", + "--file", + type=str, + default="", + help='output file (default="")') + args = parser.parse_args() + + return (args.image, args.offset * 512, args.file) + +#------------------------------------------------------------------------------- + +# 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 class. :-( +# sphinx-apidoc does ignore it. :-) +def _checkfile(_file): + """Check output file. + + Check if the output file is not existing and the target directory is writable. + + Parameters + ---------- + _file : str + path to the output file. + + Raises + ------ + Exits if the image does not exist or is not readable. + """ + + if os.path.isfile(_file): + sys.stderr.write("Error: output file already exist!\n") + sys.exit(6) + + # Check write permissions if we try to write to the file the first time! + +#=== MAIN ====================================================================== + +if __name__ == '__main__': + (IMAGE, OFFSET, FILE) = _getargs() + OBJECT = Fatlister(IMAGE, OFFSET) + if not FILE: + OBJECT.printoutput() + else: + _checkfile(FILE) + OBJECT.file = FILE + OBJECT.writeoutput() + sys.exit(0) + diff --git a/fatlister_wrapper.py b/fatlister_wrapper.py new file mode 100755 index 0000000..f408f0e --- /dev/null +++ b/fatlister_wrapper.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3.6 +# -*- coding: utf-8 -*- + +"""List file and directory names in a disk image.""" + +#=============================================================================== +# +# FILE: +# ./fatlister_wrapper.py +# +# BASIC USAGE: +# $ ./fatlister_wrapper.py [-h|--help] [-o|--offset OFFSET] [-f|--file FILENAME] image +# OR +# $ python fatlister_wrapper.py [-h|--help] [-o|--offset OFFSET] [-f|--file FILENAME] image +# +# OPTIONS: +# -h, +# --help show help message and exit +# -o OFFSET, +# --offset OFFSET offset in sectors (default=0) +# -f FILENAME, write output to file (default=unset) +# --file FILENAME if unset output goes to stdout +# image raw image file (esp. dd) +# +# EXIT STATES: +# 0 = success +# 1 = Python version not tested/supported +# 2 = image file does not exist +# 33 = file is not readable +# 3 = offset is not a positive integer +# 22 = output file already exist +# 23 = output directory is not writable +# 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 sys +import fatlister + +#=== INFO ====================================================================== + +"""@author: Patrick Neumann +@contact: patrick@neumannsland.de +@copyright: Copyright (C) 2017, Patrick Neumann +@license: GNU General Public License 3.0 +@date: 2017-05-08 +@version: 1.0.0 +@status: Development +""" + +#=== CHECKS ==================================================================== + +"""Only tested with Python versions 3.6.x!""" + +if sys.version_info < (3, 6, 0): + sys.stderr.write("Sorry, your Python version was not tested but may work?\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 _getargs(): + """Get command line arguments. + + Parses the command line arguments and return them. + (Don't forget to check them in the next steps?) + + Returns + ------- + tuple + A tuple containing the to the image file, + the offset in bytes in the image file calculated from sectors + and the output file. + + The default offset is 0. + + For example: + ( "image.dd", 1048576, "/dev/stdout" ) + """ + + 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)") + parser.add_argument("-f", + "--file", + type=str, + default="", + help="output file (default=/dev/stdout)") + args = parser.parse_args() + + return (args.image, args.offset * 512, args.file) + +#=== MAIN ====================================================================== + +if __name__ == '__main__': + (IMAGE, OFFSET, FILE) = _getargs() + OBJECT = fatlister.Fatlister(IMAGE, OFFSET, FILE) + sys.exit(0) diff --git a/images/cf8mbwin.dmg b/images/cf8mbwin.dmg new file mode 100755 index 0000000..7dec0d0 Binary files /dev/null and b/images/cf8mbwin.dmg differ diff --git a/images/empty.dd b/images/empty.dd new file mode 100755 index 0000000..e69de29 diff --git a/images/fat16.dd b/images/fat16.dd new file mode 100755 index 0000000..0e98dae Binary files /dev/null and b/images/fat16.dd differ diff --git a/images/generate_fat16_image b/images/generate_fat16_image new file mode 100755 index 0000000..9156044 --- /dev/null +++ b/images/generate_fat16_image @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +fallocate -l 32M fat16.dd +mkfs.vfat -F 16 -s 2 -n "IMG_4_A2" fat16.dd + +mkdir fat16.loop +mount -o loop fat16.dd fat16.loop + +cd fat16.loop + +echo "Short file name." > SFN.TXT +echo "Short file name with a leading sigma." > X_SIGMA.TXT +echo "Short file name that will be deleted soon." > DELSFN.TXT + +echo "Long file name." > Long_file_name.txt +echo "Long file name with spaces." > "LFN with spaces.txt" +echo "Long file name that will be deleted soon." > Del_long_file_name.txt + +dd if=/dev/urandom of=BIGFILE.BIN bs=1024 count=1024 + +mkdir SDN +cd SDN + +mkdir SSUBDN +cd SSUBDN + +echo "Another short file name." > SFN2.TXT + +cd ../.. + +mkdir Long_directory_name +cd Long_directory_name + +mkdir Long_subdirectory_name +cd Long_subdirectory_name + +echo "Another long file name." > Long_file_name2.txt + +cd ../.. + +mkdir BIGDIR +cd BIGDIR + +for id in {01..30} ; do + echo "I am in the first cluster of the directory." > FILE${id}.TXT +done +echo "I am in the second cluster of the directory." > FILE31.TXT + +cd .. + +rm DELSFN.TXT +rm Del_long_file_name.txt + +cd .. + +umount fat16.loop +rmdir fat16.loop + +chmod 444 fat16.dd + +echo +echo "Do not forget to change the X in X_SIGMA.TXT to \x05 with a hex editor!" +echo + +exit 0 diff --git a/images/invalid.dd b/images/invalid.dd new file mode 100755 index 0000000..06d7405 Binary files /dev/null and b/images/invalid.dd differ