Reading inodes with Python

Once again we turn to Python in order to easily interpret the inode data. To accomplish this we will add a new Inode class to our ever-expanding extfs.py file. The new class and helper functions follow.

“”” This helper function parses the mode bitvector stored in an inode. It accepts a 16-bit mode bitvector and returns a list of strings. “”” 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

“”” This helper function parses the mode bitvector stored in an inode in order to get file type. It accepts a 16-bit mode bitvector and returns a string. “”” 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”

“”” This helper function parses the flags bitvector stored in an inode. It accepts a 32-bit flags bitvector and returns a list of strings. “”” 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 S2ynchronously”) 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

“”” This helper function will convert an inode number to a block group and index with the block group. Usage: [bg, index ] = getInodeLoc(inodeNo, inodesPerGroup) “”” def getInodeLoc(inodeNo, inodesPerGroup):

bg = (int(inodeNo) - 1) / int(inodesPerGroup) index = (int(inodeNo) - 1) % int(inodesPerGroup) return [bg, index ]

“””

Class Inode. This class stores extended filesystem inode information in an orderly manner and provides a helper function for pretty printing. The constructor accepts a packed string containing the inode data and inode size which is defaulted to 128 bytes as used by ext2 and ext3. For inodes > 128 bytes the extra fields are decoded. Usage inode = Inode(dataInPackedString, inodeSize) inode.prettyPrint() “”” class Inode():

def init(self, data, inodeSize=128):

store both the raw mode bitvector and interpretation self.mode = getU16(data)

self.modeList = getInodeModes(self.mode) self.fileType = getInodeFileType(self.mode) self.ownerID = getU16(data, 0x2) self.fileSize = getU32(data, 0x4) # get times in seconds since epoch

note: these will rollover in 2038 without extra # bits stored in the extra inode fields below 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) # store both the raw flags bitvector and interpretation self.flags = getU32(data, 0x20) self.flagList = getInodeFlags(self.flags) # high 32-bits of generation for Linux self.osd1 = getU32(data, 0x24) # store the 15 values from the block array

note: these may not be actual blocks if # certain features like extents are enabled self.block = [] for i in range(0, 15):

self.block.append(getU32(data, 0x28 + i * 4)) self.generation = getU32(data, 0x64)

the most common extened attributes are ACLs # but other EAs are possible 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) # ext4 uses 256 byte inodes on the disk

as of July 2015 the logical size is 156 bytes # the first word is the size of the extra info if inodeSize > 128:

self.inodeSize = 128 + getU16(data, 0x80)

this is the upper word of the checksum if self.inodeSize > 0x82:

self.checksum += getU16(data, 0x82) * pow(2, 16)

these extra bits give nanosecond accuracy of timestamps # the lower 2 bits are used to extend the 32-bit seconds

since the epoch counter to 34 bits # if you are still using this script in 2038 you should # adjust this script accordingly :-) 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()) :

print k+”:”, v

The first three helper functions are straightforward. The line return [bg, index ] in getInodeLoc is the Python way of returning more than one value from a function. We have returned lists (usually of strings) before, but the syntax here is slightly different.

There is something new in the Inode class. When interpreting the extra timestamp bits, the right shift operator (>>) has been used. Writing x >> y causes the bits in x to be shifted y places to the right. By shifting everything two bits to the right we discard the lower two bits which are used to extend the seconds counter (which should not be a problem until 2038) and effectively divide our 32-bit value by four. The shift operators are very fast and efficient. Incidentally, the left shift operator (<<) is used to shift bits the other direction (multiplication).

A script named istat.py has been created. Its functionality is similar to that of the istat utility from The Sleuth Kit. It expects an image file, offset to the start of a filesystem in sectors, and an inode number as inputs. The script follows.

!/usr/bin/python

#

istat.py

#

This is a simple Python script that will # print out metadata in an inode from an ext2/3/4 filesystem inside # of an image file.

#

Developed for PentesterAcademy # by Dr. Phil Polstra (@ppolstra) import extfs import sys import os.path import subprocess import struct import time from math import log def usage():

print(“usage “ + sys.argv[0] + \

<offset> \n”+

“Displays inode information 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 = extfs.ExtMetadata(sys.argv[1], sys.argv[2]) # get inode location

inodeLoc = extfs.getInodeLoc(sys.argv[3], \ emd.superblock.inodesPerGroup) offset = emd.bgdList[inodeLoc[0]].inodeTable \

  • emd.superblock.blockSize + \ inodeLoc[1] * emd.superblock.inodeSize with open(str(sys.argv[1]), ‘rb’) as f:

f.seek(offset + int(sys.argv[2]) * 512) data = str(f.read(emd.superblock.inodeSize)) inode = extfs.Inode(data, emd.superblock.inodeSize) print “Inode %s in Block Group %s at index %s” % (str(sys.argv[3]), \ str(inodeLoc[0]), str(inodeLoc[1])) inode.prettyPrint() if name == “main”:

main()

In this script we have imported the extfs.py file with the line import extfs. Notice that there is no file extension in the import statement. The script is straightforward. We see the typical usage function that is called if insufficient parameters are passed into the script. An extended metadata object is created, then the location of the inode in question is calculated. Once the location is known, the inode data is retrieved from the file and used to create an inode object which is printed to the screen. Notice that “extfs.” must be prepended to the function names now that we are calling code outside of the current file. We could avoid this by changing the import statement to from extfs import *, but I did not do so as I feel having the “extfs.” makes the code clearer.

Results of running this new script against one of the interesting inodes from the PFE subject system image are shown in Figure 7.21. A couple of things about this inode should be noted. First, the flags indicate that this inode uses extents which means the block array has a different interpretation (more about this later in this chapter). Second, this inode contains a creation time. Because this is a new field only available in ext4 filesystems, many tools, including those for altering timestamps, do not change this field. Obviously, this unaltered timestamp is good data for the forensic investigator. Now that we have learned about the generic inodes, we are ready to move on to a discussion of some inode extensions that will be present if the filesystem has certain features enabled.

FIGURE 7.21

Results of running istat.py on an inode associated with a rootkit on the PFE subject system.

results matching ""

    No results matching ""