#!/usr/bin/python # This is a simple Python script that will # get metadata from an ext2/3/4 filesystem inside # of an image file. # # Developed for PentesterAcademy by Dr. Phil Polstra (@ppolstra) import sys import os.path import subprocess import struct import time, calendar from math import log # these are simple functions to make conversions easier def getU32(data, offset=0): return struct.unpack('= self.firstMetaBlockGroup: mbgSize = self.blockSize / 32 retVal = (bgNo % mbgSize == 0) or ((bgNo + 1) % mbgSize == 0) or ((bgNo + 2) % mbgSize == 0) else: # if we got this far we must have default with every bg having sb and gdt retVal = True return retVal class GroupDescriptor(): def __init__(self, data, wide=False): self.wide = wide self.blockBitmapLo=getU32(data) #/* Blocks bitmap block */ self.inodeBitmapLo=getU32(data, 4) #/* Inodes bitmap block */ self.inodeTableLo=getU32(data, 8) #/* Inodes table block */ self.freeBlocksCountLo=getU16(data, 0xc)#/* Free blocks count */ self.freeInodesCountLo=getU16(data, 0xe)#/* Free inodes count */ self.usedDirsCountLo=getU16(data, 0x10) #/* Directories count */ self.flags=getU16(data, 0x12) #/* EXT4_BG_flags (INODE_UNINIT, etc) */ self.flagList = self.printFlagList() self.excludeBitmapLo=getU32(data, 0x14) #/* Exclude bitmap for snapshots */ self.blockBitmapCsumLo=getU16(data, 0x18) #/* crc32c(s_uuid+grp_num+bbitmap) LE */ self.inodeBitmapCsumLo=getU16(data, 0x1a) #/* crc32c(s_uuid+grp_num+ibitmap) LE */ self.itableUnusedLo=getU16(data, 0x1c) #/* Unused inodes count */ self.checksum=getU16(data, 0x1e) #/* crc16(sb_uuid+group+desc) */ if wide==True: self.blockBitmapHi=getU32(data, 0x20) #/* Blocks bitmap block MSB */ self.inodeBitmapHi=getU32(data, 0x24) #/* Inodes bitmap block MSB */ self.inodeTableHi=getU32(data, 0x28) #/* Inodes table block MSB */ self.freeBlocksCountHi=getU16(data, 0x2c) #/* Free blocks count MSB */ self.freeInodesCountHi=getU16(data, 0x2e) #/* Free inodes count MSB */ self.usedDirsCountHi=getU16(data, 0x30) #/* Directories count MSB */ self.itableUnusedHi=getU16(data, 0x32) #/* Unused inodes count MSB */ self.excludeBitmapHi=getU32(data, 0x34) #/* Exclude bitmap block MSB */ self.blockBitmapCsumHi=getU16(data, 0x38)#/* crc32c(s_uuid+grp_num+bbitmap) BE */ self.inodeBitmapCsumHi=getU16(data, 0x3a)#/* crc32c(s_uuid+grp_num+ibitmap) BE */ self.reserved=getU32(data, 0x3c) def printFlagList(self): flagList = [] if self.flags & 0x1: #inode table and bitmap are not initialized (EXT4_BG_INODE_UNINIT). flagList.append('Inode Uninitialized') if self.flags & 0x2: #block bitmap is not initialized (EXT4_BG_BLOCK_UNINIT). flagList.append('Block Uninitialized') if self.flags & 0x4: #inode table is zeroed (EXT4_BG_INODE_ZEROED). flagList.append('Inode Zeroed') return flagList def prettyPrint(self): for k, v in sorted(self.__dict__.iteritems()) : print k+":", v # This class combines informaton from the block group descriptor # and the superblock to more fully describe the block group class ExtendedGroupDescriptor(): def __init__(self, bgd, sb, bgNo): self.blockGroup = bgNo self.startBlock = sb.groupStartBlock(bgNo) self.endBlock = sb.groupEndBlock(bgNo) self.startInode = sb.groupStartInode(bgNo) self.endInode = sb.groupEndInode(bgNo) self.flags = bgd.printFlagList() self.freeInodes = bgd.freeInodesCountLo if bgd.wide: self.freeInodes += bgd.freeInodesCountHi * pow(2, 16) self.freeBlocks = bgd.freeBlocksCountLo if bgd.wide: self.freeBlocks += bgd.freeBlocksCountHi * pow(2, 16) self.directories = bgd.usedDirsCountLo if bgd.wide: self.directories += bgd.usedDirsCountHi * pow(2, 16) self.checksum = bgd.checksum self.blockBitmapChecksum = bgd.blockBitmapCsumLo if bgd.wide: self.blockBitmapChecksum += bgd.blockBitmapCsumHi * pow(2, 16) self.inodeBitmapChecksum = bgd.inodeBitmapCsumLo if bgd.wide: self.inodeBitmapChecksum += bgd.inodeBitmapCsumHi * pow(2, 16) # now figure out the layout and store it in a list (with lists inside) self.layout = [] self.nonDataBlocks = 0 # for flexible block groups must make an adjustment fbgAdj = 1 if 'Flexible Block Groups' in sb.incompatibleFeaturesList: if bgNo % sb.groupsPerFlex == 0: # only first group in flex block affected fbgAdj = sb.groupsPerFlex if sb.groupHasSuperblock(bgNo): self.layout.append(['Superblock', self.startBlock, self.startBlock]) gdSize = sb.groupDescriptorSize() * sb.blockGroups() / sb.blockSize self.layout.append(['Group Descriptor Table', self.startBlock + 1, self.startBlock + gdSize]) self.nonDataBlocks += gdSize + 1 if sb.reservedGdtBlocks > 0: self.layout.append(['Reserved GDT Blocks', self.startBlock + gdSize + 1, \ self.startBlock + gdSize + sb.reservedGdtBlocks]) self.nonDataBlocks += sb.reservedGdtBlocks bbm = bgd.blockBitmapLo if bgd.wide: bbm += bgd.blockBitmapHi * pow(2, 32) self.layout.append(['Data Block Bitmap', bbm, bbm]) # is block bitmap in this group (not flex block group, etc) if sb.groupFromBlock(bbm) == bgNo: self.nonDataBlocks += fbgAdj ibm = bgd.inodeBitmapLo if bgd.wide: ibm += bgd.inodeBitmapHi * pow(2, 32) self.layout.append(['Inode Bitmap', ibm, ibm]) # is inode bitmap in this group? if sb.groupFromBlock(ibm) == bgNo: self.nonDataBlocks += fbgAdj it = bgd.inodeTableLo if bgd.wide: it += bgd.inodeTableHi * pow(2, 32) self.inodeTable = it itBlocks = (sb.inodesPerGroup * sb.inodeSize) / sb.blockSize self.layout.append(['Inode Table', it, it + itBlocks - 1]) # is inode table in this group? if sb.groupFromBlock(it) == bgNo: self.nonDataBlocks += itBlocks * fbgAdj self.layout.append(['Data Blocks', self.startBlock + self.nonDataBlocks, self.endBlock]) def prettyPrint(self): print "" print 'Block Group: ' + str(self.blockGroup) print 'Flags: %r ' % self.flags print 'Blocks: %s - %s ' % (self.startBlock, self.endBlock) print 'Inodes: %s - %s ' % (self.startInode, self.endInode) print 'Layout:' for item in self.layout: print ' %s %s - %s' % (item[0], item[1], item[2]) print 'Free Inodes: %u ' % self.freeInodes print 'Free Blocks: %u ' % self.freeBlocks print 'Directories: %u ' % self.directories print 'Checksum: 0x%x ' % self.checksum print 'Block Bitmap Checksum: 0x%x ' % self.blockBitmapChecksum print 'Inode Bitmap Checksum: 0x%x ' % self.inodeBitmapChecksum class ExtMetadata(): def __init__(self, filename, offset): # read first sector if not os.path.isfile(sys.argv[1]): print("File " + str(filename) + " cannot be openned for reading") exit(1) with open(str(filename), 'rb') as f: f.seek(1024 + int(offset) * 512) sbRaw = str(f.read(1024)) self.superblock = Superblock(sbRaw) # read block group descriptors self.blockGroups = self.superblock.blockGroups() if self.superblock.descriptorSize != 0: self.wideBlockGroups = True self.blockGroupDescriptorSize = 64 else: self.wideBlockGroups = False self.blockGroupDescriptorSize = 32 # read in group descriptors starting in block 1 with open(str(filename), 'rb') as f: f.seek(int(offset) * 512 + self.superblock.blockSize) bgdRaw = str(f.read(self.blockGroups * self.blockGroupDescriptorSize)) self.bgdList = [] for i in range(0, self.blockGroups): bgd = GroupDescriptor(bgdRaw[i * self.blockGroupDescriptorSize:], self.wideBlockGroups) ebgd = ExtendedGroupDescriptor(bgd, self.superblock, i) self.bgdList.append(ebgd) def prettyPrint(self): self.superblock.prettyPrint() for bgd in self.bgdList: bgd.prettyPrint() def getInodeModes(mode): retVal = [] if mode & 0x1: retVal.append("Others Exec") if mode & 0x2: retVal.append("Others Write") if mode & 0x4: retVal.append("Others Read") if mode & 0x8: retVal.append("Group Exec") if mode & 0x10: retVal.append("Group Write") if mode & 0x20: retVal.append("Group Read") if mode & 0x40: retVal.append("Owner Exec") if mode & 0x80: retVal.append("Owner Write") if mode & 0x100: retVal.append("Owner Read") if mode & 0x200: retVal.append("Sticky Bit") if mode & 0x400: retVal.append("Set GID") if mode & 0x800: retVal.append("Set UID") return retVal def getInodeFileType(mode): fType = (mode & 0xf000) >> 12 if fType == 0x1: return "FIFO" elif fType == 0x2: return "Char Device" elif fType == 0x4: return "Directory" elif fType == 0x6: return "Block Device" elif fType == 0x8: return "Regular File" elif fType == 0xA: return "Symbolic Link" elif fType == 0xc: return "Socket" else: return "Unknown Filetype" def getInodeFlags(flags): retVal = [] if flags & 0x1: retVal.append("Secure Deletion") if flags & 0x2: retVal.append("Preserve for Undelete") if flags & 0x4: retVal.append("Compressed File") if flags & 0x8: retVal.append("Synchronous Writes") if flags & 0x10: retVal.append("Immutable File") if flags & 0x20: retVal.append("Append Only") if flags & 0x40: retVal.append("Do Not Dump") if flags & 0x80: retVal.append("Do Not Update Access Time") if flags & 0x100: retVal.append("Dirty Compressed File") if flags & 0x200: retVal.append("Compressed Clusters") if flags & 0x400: retVal.append("Do Not Compress") if flags & 0x800: retVal.append("Encrypted Inode") if flags & 0x1000: retVal.append("Directory Hash Indexes") if flags & 0x2000: retVal.append("AFS Magic Directory") if flags & 0x4000: retVal.append("Must Be Written Through Journal") if flags & 0x8000: retVal.append("Do Not Merge File Tail") if flags & 0x10000: retVal.append("Directory Entries Written Synchronously") if flags & 0x20000: retVal.append("Top of Directory Hierarchy") if flags & 0x40000: retVal.append("Huge File") if flags & 0x80000: retVal.append("Inode uses Extents") if flags & 0x200000: retVal.append("Large Extended Attribute in Inode") if flags & 0x400000: retVal.append("Blocks Past EOF") if flags & 0x1000000: retVal.append("Inode is Snapshot") if flags & 0x4000000: retVal.append("Snapshot is being Deleted") if flags & 0x8000000: retVal.append("Snapshot Shrink Completed") if flags & 0x10000000: retVal.append("Inline Data") if flags & 0x80000000: retVal.append("Reserved for Ext4 Library") if flags & 0x4bdfff: retVal.append("User-visible Flags") if flags & 0x4b80ff: retVal.append("User-modifiable Flags") return retVal def getInodeLoc(inodeNo, inodesPerGroup): bg = (int(inodeNo) - 1) / int(inodesPerGroup) index = (int(inodeNo) - 1) % int(inodesPerGroup) return [bg, index ] class ExtentHeader(): def __init__(self, data): self.magic = getU16(data) self.entries = getU16(data, 0x2) self.max = getU16(data, 0x4) self.depth = getU16(data, 0x6) self.generation = getU32(data, 0x8) def prettyPrint(self): print("Extent depth: %s entries: %s max-entries: %s generation: %s" \ % (self.depth, self.entries, self.max, self.generation)) class ExtentIndex(): def __init__(self, data): self.block = getU32(data) self.leafLo = getU32(data, 0x4) self.leafHi = getU16(data, 0x8) def prettyPrint(self): print("Index block: %s leaf: %s" \ % (self.block, self.leafHi * pow(2, 32) + self.leafLo)) class Extent(): def __init__(self, data): self.block = getU32(data) self.len = getU16(data, 0x4) self.startHi = getU16(data, 0x6) self.startLo = getU32(data, 0x8) def prettyPrint(self): print("Extent block: %s data blocks: %s - %s" \ % (self.block, self.startHi * pow(2, 32) + self.startLo, \ self.len + self.startHi * pow(2, 32) + self.startLo - 1)) def getExtentTree(data): # first entry must be a header retVal = [] retVal.append(ExtentHeader(data)) if retVal[0].depth == 0: # leaf node for i in range(0, retVal[0].entries): retVal.append(Extent(data[(i + 1) * 12 : ])) else: # index nodes for i in range(0, retVal[0].entries): retVal.append(ExtentIndex(data[(i + 1) * 12 : ])) return retVal class Inode(): def __init__(self, data, inodeSize=128): self.mode = getU16(data) self.modeList = getInodeModes(self.mode) self.fileType = getInodeFileType(self.mode) self.ownerID = getU16(data, 0x2) self.fileSize = getU32(data, 0x4) self.accessTime = time.gmtime(getU32(data, 0x8)) self.changeTime = time.gmtime(getU32(data, 0xC)) self.modifyTime = time.gmtime(getU32(data, 0x10)) self.deleteTime = time.gmtime(getU32(data, 0x14)) self.groupID = getU16(data, 0x18) self.links = getU16(data, 0x1a) self.blocks = getU32(data, 0x1c) self.flags = getU32(data, 0x20) self.flagList = getInodeFlags(self.flags) self.osd1 = getU32(data, 0x24) # high 32-bits of generation for Linux self.block = [] self.extents = [] if self.flags & 0x80000: self.extents = getExtentTree(data[0x28 : ]) else: for i in range(0, 15): self.block.append(getU32(data, 0x28 + i * 4)) self.generation = getU32(data, 0x64) self.extendAttribs = getU32(data, 0x68) self.fileSize += pow(2, 32) * getU32(data, 0x6c) # these are technically only correct for Linux ext4 filesystems # should probably verify that that is the case self.blocks += getU16(data, 0x74) * pow(2, 32) self.extendAttribs += getU16(data, 0x76) * pow(2, 32) self.ownerID += getU16(data, 0x78) * pow(2, 32) self.groupID += getU16(data, 0x7a) * pow(2, 32) self.checksum = getU16(data, 0x7c) if inodeSize > 128: self.inodeSize = 128 + getU16(data, 0x80) if self.inodeSize > 0x82: self.checksum += getU16(data, 0x82) * pow(2, 16) if self.inodeSize > 0x84: self.changeTimeNanosecs = getU32(data, 0x84) >> 2 if self.inodeSize > 0x88: self.modifyTimeNanosecs = getU32(data, 0x88) >> 2 if self.inodeSize > 0x8c: self.accessTimeNanosecs = getU32(data, 0x8c) >> 2 if self.inodeSize > 0x90: self.createTime = time.gmtime(getU32(data, 0x90)) self.createTimeNanosecs = getU32(data, 0x94) >> 2 else: self.createTime = time.gmtime(0) def prettyPrint(self): for k, v in sorted(self.__dict__.iteritems()) : if k == 'extents' and self.extents: v[0].prettyPrint() # print header for i in range(1, v[0].entries + 1): v[i].prettyPrint() elif k == 'changeTime' or k == 'modifyTime' or k == 'accessTime' or k == 'createTime': print k+":", time.asctime(v) elif k == 'deleteTime': if calendar.timegm(v) == 0: print 'Deleted: no' else: print k+":", time.asctime(v) else: print k+":", v # get a datablock from an image def getDataBlock(imageFilename, offset, blockNo, blockSize=4096): with open(str(imageFilename), 'rb') as f: f.seek(blockSize * blockNo + offset * 512) data = str(f.read(blockSize)) return data # This function will return a list of data blocks # if extents are being used this should be simple assuming # there is a single level to the tree. # For extents with multiple levels and for indirect blocks # additional "disk access" is required. def getBlockList(inode, imageFilename, offset, blockSize=4096): # now get the data blocks and output them datablocks = [] if inode.extents: # great we are using extents # extent zero has the header # check for depth of zero which is most common if inode.extents[0].depth == 0: for i in range(1, inode.extents[0].entries + 1): sb = inode.extents[i].startHi * pow(2, 32) + inode.extents[i].startLo eb = sb + inode.extents[i].len # really ends in this minus 1 for j in range(sb, eb): datablocks.append(j) else: # load this level of the tree currentLevel = inode.extents leafNode = [] while currentLevel[0].depth != 0: # read the current level nextLevel = [] for i in range(1, currentLevel[0].entries + 1): blockNo = currentLevel[i].leafLo + currentLevel[i].leafHi * pow(2, 32) currnode = getExtentTree(getDataBlock(imageFilename, offset, blockNo, blockSize)) nextLevel.append(currnode) if currnode[0].depth == 0: leafNode.append(currnode[1: ]) # if there are leaves add them to the end currentLevel = nextLevel # now sort the list by logical block number leafNode.sort(key=lambda x: x.block) for leaf in leafNode: sb = leaf.startHi * pow(2, 32) + leaf.startLo eb = sb + leaf.len for j in range(sb, eb): datablocks.append(j) else: # we have the old school blocks blocks = inode.fileSize / blockSize # get the direct blocks for i in range(0, 12): datablocks.append(inode.block[i]) if i >= blocks: break # now do indirect blocks if blocks > 12: iddata = getDataBlock(imageFilename, offset, inode.block[12], blockSize) for i in range(0, blockSize / 4): idblock = getU32(iddata, i * 4) if idblock == 0: break else: datablocks.append(idblock) # now double indirect blocks if blocks > (12 + blockSize / 4): diddata = getDataBlock(imageFilename, offset, inode.block[13], blockSize) for i in range(0, blockSize / 4): didblock = getU32(diddata, i * 4) if didblock == 0: break else: iddata = getDataBlock(imageFilename, offset, didblock, blockSize) for j in range(0, blockSize / 4): idblock = getU32(iddata, j * 4) if idblock == 0: break else: datablocks.append(idblock) # now triple indirect blocks if blocks > (12 + blockSize / 4 + blockSize * blockSize / 16): tiddata = getDataBlock(imageFilename, offset, inode.block[14], blockSize) for i in range(0, blockSize / 4): tidblock = getU32(tiddata, i * 4) if tidblock == 0: break else: diddata = getDataBlock(imageFilename, offset, tidblock, blockSize) for j in range(0, blockSize / 4): didblock = getU32(diddata, j * 4) if didblock == 0: break else: iddata = getDataBlock(imageFilename, offset, didblock, blockSize) for k in range(0, blockSize / 4): idblock = getU32(iddata, k * 4) if idblock == 0: break else: datablocks.append(idblock) return datablocks class DirectoryEntry(): def __init__(self, data): self.inode = getU32(data) self.recordLen = getU16(data, 0x4) self.nameLen = getU8(data, 0x6) self.fileType = getU8(data, 0x7) self.filename = data[0x8, 0x8 + self.nameLen] # parses directory entries in a data block that is passed in def getDirectory(data): done = False retVal = [] i = 0 while not done: de = DirectoryEntry(data[i: ]) if de.inode == 0: done = True else: retVal.append(de) i += de.recordLen return retVal def usage(): print("usage " + sys.argv[0] + " \nReads superblock from an image file") exit(1) def main(): if len(sys.argv) < 3: usage() # read first sector if not os.path.isfile(sys.argv[1]): print("File " + sys.argv[1] + " cannot be openned for reading") exit(1) emd = ExtMetadata(sys.argv[1], sys.argv[2]) emd.prettyPrint() if __name__ == "__main__": main()