분할된 원시 데이터에서 Ext4 Inode를 식별하는 Python 스크립트

분할된 원시 데이터에서 Ext4 Inode를 식별하는 Python 스크립트

ext4 파티션의 백업 이미지에서 원시 데이터를 읽고 ext4 inode 구조가 표시되는 시기를 식별하여 특정 파일을 복원할 수 있는 Python 스크립트를 작성하려고 합니다.

이 유형의 스크립트의 목적은 슈퍼블록이 손상되어 매직 넘버, 매직 바이트 또는 알려진 확장 유형을 사용하여 파일을 복구할 수 없는 경우, 다른 모든 방법은 실패하는 것입니다.

적절한 파티션을 가리키는 이 명령을 성공적으로 실행하면 분석할 데이터가 생성됩니다.

dd if=/dev/sda of=partition.dd

내가 찾고 있는 대답은 Python 코드입니다. dd.image에서 원시 데이터를 한 번에 한 블록씩 읽고 해당 블록이 ext4 inode 블록인지 확인합니다.

이에 대한 도움을 받으면 inode에 있는 데이터를 사용하여 파일을 완전히 복원할 수 있는 범위 블록을 찾을 수 있습니다. 이와 같은 것을 찾을 수 없는 것 같아서 이것이 절실히 필요한 누락된 도구라고 확신합니다.

나는 이것에 대해 연구해 왔습니다:

https://www.kernel.org/doc/html/latest/filesystems/ext4/about.html

지금까지 알아낸 내용은 아래 답변을 기반으로 업데이트됩니다.

#!/usr/bin/python

import sys

READ_BYTES = 512
SUPERBLOCK_SIZE = 1024
SUPERBLOCK_OFFSETS = [
    0x0, 
    0x4, 
    0x8,
    0xC,
    0x10,
    0x14,
    0x18,
    0x1C,
    0x20,
    0x24,
    0x28,
    0x2C,
    0x30,
    0x34,
    0x36,
    0x38,
    0x3A,
    0x3C,
    0x3E,
    0x40,
    0x44,
    0x48,
    0x4C,
    0x50,
    0x52,
    0x54,
    0x58,
    0x5A,
    0x5C,
    0x60,
    0x64,
    0x68,
    0x78,
    0x88,
    0xC8,
    0xCC,
    0xCD,
    0xCE,
    0xD0,
    0xE0,
    0xE4,
    0xE8,
    0xEC,
    0xFC,
    0xFD,
    0xFE,
    0x100,
    0x104,
    0x108,
    0x10C,
    0x150,
    0x154,
    0x158,
    0x15C,
    0x15E,
    0x160,
    0x164,
    0x166,
    0x168,
    0x170,
    0x174,
    0x175,
    0x176,
    0x178,
    0x180,
    0x184,
    0x188,
    0x190,
    0x194,
    0x198,
    0x19C,
    0x1A0,
    0x1A8,
    0x1C8,
    0x1CC,
    0x1D0,
    0x1D4,
    0x1D8,
    0x1E0,
    0x200,
    0x240,
    0x244,
    0x248,
    0x24C,
    0x254,
    0x258,
    0x268,
    0x26C,
    0x270,
    0x274,
    0x275,
    0x276,
    0x277,
    0x278,
    0x279,
    0x27A,
    0x27C,
    0x27E,
    0x280,
    0x3FC
]
SUPERBLOCK_MAGIC_NUMBER_OFFSET = SUPERBLOCK_OFFSETS[15]

INODE_OFFSETS = [
    0x0, 
    0x2, 
    0x4,
    0x8,
    0xC,
    0x10,
    0x14,
    0x18,
    0x1A,
    0x1C,
    0x20,
    0x24,
    0x28,
    0x64,
    0x68,
    0x6C,
    0x70,
    0x74,
    0x80,
    0x82,
    0x84,
    0x88,
    0x8C,
    0x90,
    0x94,
    0x98,
    0x9C,
]
INODE_MAGIC_NUMBER_OFFSET = SUPERBLOCK_OFFSETS[9]
INODE_BLOCK_SIZE = INODE_OFFSETS[-1] + 32

class Partition:
    def __init__(self, path):
        self.path = path
        self.superblockMagicNumber = '\x53\xEF'
        self.superblocks = []
        self.inodes = []

        self.inodeMagicNumber = "\x0A\xF3"
        # self.inodeMagicNumber = '\x00\x00\x02\xEA'

    def findSuperblock(self):
        byteCount = 0

        filePointer = open(self.path, 'rb')
        data = filePointer.read(READ_BYTES)
        byteCount += len(data)

        while len(data):

            if self.superblockMagicNumber in data:

                # print info when magic number found
                print 'found magic number in read block:', byteCount, " (" + str(READ_BYTES) + " bytes per block)"
                magicNumberPosition = data.find(self.superblockMagicNumber)
                print 'position in data block:', magicNumberPosition
                print 'hex offset:', hex(magicNumberPosition)
                print " ".join([d.encode('hex') for d in data])

                # reset the file pointer to the begining of the superblock
                currentPosition = filePointer.tell()
                position = currentPosition - len(data) + magicNumberPosition - SUPERBLOCK_MAGIC_NUMBER_OFFSET
                filePointer.seek(position)
                superblockData = filePointer.read(SUPERBLOCK_SIZE)
                print "superblock data:"
                print " ".join([d.encode('hex') for d in superblockData])

                # # use the offsets to gather and set the superblock values
                superblockArgs = []
                for i in range(len(SUPERBLOCK_OFFSETS)-1) :
                    arg = superblockData[SUPERBLOCK_OFFSETS[i] : SUPERBLOCK_OFFSETS[i+1]]
                    superblockArgs.append(arg)
                arg = superblockData[SUPERBLOCK_OFFSETS[i+1] : SUPERBLOCK_SIZE]
                superblockArgs.append(arg)

                sb = Superblock(superblockArgs)
                for key, value in sb.__dict__.items():
                    values = []
                    for b in value:
                        values.append(b.encode('hex'))
                    print key, ":", " ".join(values)
                self.superblocks.append(sb)

            # reset the file pointer to end of data already read
            data = filePointer.read(READ_BYTES)
            byteCount += len(data)

        filePointer.close()

    def findInodes(self):

        byteCount = 0

        filePointer = open(self.path, 'rb')
        data = filePointer.read(READ_BYTES)
        byteCount += len(data)

        while data != None and len(data):

            if self.inodeMagicNumber in data:

                # print info when magic number found
                print 'found magic number in read block:', byteCount, " (" + str(READ_BYTES) + " bytes per block)"
                magicNumberPosition = data.find(self.inodeMagicNumber)
                print 'position in data block:', magicNumberPosition
                print 'hex offset:', hex(magicNumberPosition)
                print " ".join([d.encode('hex') for d in data])


                # reset the file pointer to the begining of the inode
                currentPosition = filePointer.tell()
                position = currentPosition - len(data) + magicNumberPosition - INODE_MAGIC_NUMBER_OFFSET
                filePointer.seek(position)
                indodeData = filePointer.read(INODE_BLOCK_SIZE)
                print "inode data:"
                print " ".join([d.encode('hex') for d in indodeData])

                # # use the offsets to gather and set the inode values
                indodeArgs = []
                for i in range(len(INODE_OFFSETS)-1) :
                    arg = indodeData[INODE_OFFSETS[i] : INODE_OFFSETS[i+1]]
                    indodeArgs.append(arg)
                arg = indodeData[INODE_OFFSETS[i+1] : INODE_BLOCK_SIZE]
                indodeArgs.append(arg)

                sb = Inode(indodeArgs)
                for key, value in sb.__dict__.items():
                    values = []
                    for b in value:
                        values.append(b.encode('hex'))
                    print key, ":", " ".join(values)
                self.inodes.append(sb)

            # reset the file pointer to end of data already read
            data = filePointer.read(READ_BYTES)
            byteCount += len(data)
            magicNumber = None

        filePointer.close()

class Superblock:
    def __init__(self, args=[]):

        if len(args):
            self.inodeCount = args[0]
            self.blockCount = args[1] 
            self.reservedBlockCount = args[2]
            self.freeBlockCount = args[3]
            self.freeInodeCount = args[4]
            self.firstDataBlock = args[5]
            self.logBlockSize = args[6]
            self.logClusterSize = args[7]
            self.blocksPerGroup = args[8]
            self.clustersPerGroup = args[9]
            self.inodesPerGroup = args[10]
            self.mountTime = args[11]
            self.writeTime = args[12]
            self.mountCount = args[13]
            self.maxMountCount = args[14]
            self.magic = args[15]
            self.state = args[16]
            self.errors = args[17]
            self.minorRevisionLevel = args[18]
            self.lastCheck = args[19]
            self.checkInterveal = args[20]
            self.creatorOS = args[21]
            self.revisionLevel = args[22]
            self.reservedBlocksUID = args[23]
            self.reservedBlocksDefaultGID = args[24]
            self.firstNonReservedInode = args[25]
            self.inodeSize = args[26]
            self.blockGroup = args[27]
            self.compatibleFeatures = args[28]
            self.incompatibleFeatures = args[29]
            self.readOnlyCompatibleFeatures = args[30]
            self.uuid = args[31]
            self.label = args[32]
            self.lastMounted = args[33]
            self.compression = args[34]
            self.preallocatedFileBlocks = args[35]
            self.preallocatedDirectoryBlocks = args[36]
            self.reservedGDTBlocks = args[37]
            self.journalUUID = args[38]
            self.journalInodeNumber = args[39]
            self.journalFileDeviceNumber = args[40]
            self.lastOrphan = args[41]
            self.hashSeed = args[42]
            self.hashVersion = args[43]
            self.journalBackupType = args[44]
            self.groupDescriptorSize = args[45]
            self.mountOptionsDefault = args[46]
            self.firstMetablockBlockGroup = args[47]
            self.makeFileSystemTime = args[48]
            self.journalInodesBackup = args[49]
            self.blockCountHigh = args[50]
            self.reserverdBlockCountHigh = args[51]
            self.freeBlockCountHigh = args[52]
            self.minimumInodeSize = args[53]
            self.newInodeReservationSize = args[54]
            self.miscFlags = args[55]
            self.raidStride = args[56]
            self.multiMountPreventionInterval = args[57]
            self.multiMountPreventionData = args[58]
            self.raidStripeWidth = args[59]
            self.flexibleBlockGroupSize = args[60]
            self.metadataChecksumAlgorithmType = args[61]
            self.reservedPad = args[62]
            self.kilobytesWritten = args[63]
            self.snapshotInodeNumber = args[64]
            self.snapshotID = args[65]
            self.snapshotReservedBlockCount = args[66]
            self.snapshotList = args[67]
            self.errorCount = args[68]
            self.firstErrorTime = args[69]
            self.firstErrorInode = args[70]
            self.firstErrorBlock = args[71]
            self.firstErrorFunction = args[72]
            self.firstErrorLine = args[73]
            self.lastErrorTime = args[74]
            self.lastErrorInode = args[75]
            self.lastErrorLine = args[76]
            self.lastErrorBlock = args[77]
            self.lastErrorFunction = args[78]
            self.mountOptions = args[79]
            self.inodeOfUserQuotaFile = args[80]
            self.infodeOfGroupQuotaFile = args[81]
            self.overheadBlocks = args[82]
            self.superblockBackups = args[83]
            self.encryptionAlgorithms = args[84]
            self.encryptionSalt = args[85]
            self.inodeLostAndFound = args[86]
            self.inodeProjectQuota = args[87]
            self.checksumSeed = args[88]
            self.wtimeHigh = args[89]
            self.mtimeHigh = args[90]
            self.makeFileSystemTimeHigh = args[91]
            self.lastCheckHigh = args[92]
            self.firstErrorTimeHigh = args[93]
            self.lastErrorTimeHigh = args[94]
            self.zeroPadding = args[95]
            self.encoding = args[96]
            self.encodingFlags = args[97]
            self.reservedPadding = args[98]
            self.checksum = args[99]

        else:
            self.inodeCount = None
            self.blockCount = None
            self.reservedBlockCount = None
            self.freeBlockCount = None
            self.freeInodeCount = None
            self.firstDataBlock = None
            self.logBlockSize = None
            self.logClusterSize = None
            self.blocksPerGroup = None
            self.clustersPerGroup = None
            self.inodesPerGroup = None
            self.mountTime = None
            self.writeTime = None
            self.mountCount = None
            self.maxMountCount = None
            self.magic = None
            self.state = None
            self.errors = None
            self.minorRevisionLevel = None
            self.lastCheck = None
            self.checkInterveal = None
            self.creatorOS = None
            self.revisionLevel = None
            self.reservedBlocksUID = None
            self.reservedBlocksDefaultGID = None
            self.firstNonReservedInode = None
            self.inodeSize = None
            self.blockGroup = None
            self.compatibleFeatures = None
            self.incompatibleFeatures = None
            self.readOnlyCompatibleFeatures = None
            self.uuid = None
            self.label = None
            self.lastMounted = None
            self.compression = None
            self.preallocatedFileBlocks = None
            self.preallocatedDirectoryBlocks = None
            self.reservedGDTBlocks = None
            self.journalUUID = None
            self.journalInodeNumber = None
            self.journalFileDeviceNumber = None
            self.lastOrphan = None
            self.hashSeed = None
            self.hashVersion = None
            self.journalBackupType = None
            self.groupDescriptorSize = None
            self.mountOptionsDefault = None
            self.firstMetablockBlockGroup = None
            self.makeFileSystemTime = None
            self.journalInodesBackup = None
            self.blockCountHigh = None
            self.reserverdBlockCountHigh = None
            self.freeBlockCountHigh = None
            self.minimumInodeSize = None
            self.newInodeReservationSize = None
            self.miscFlags = None
            self.raidStride = None
            self.multiMountPreventionInterval = None
            self.multiMountPreventionData = None
            self.raidStripeWidth = None
            self.flexibleBlockGroupSize = None
            self.metadataChecksumAlgorithmType = None
            self.reservedPad = None
            self.kilobytesWritten = None
            self.snapshotInodeNumber = None
            self.snapshotID = None
            self.snapshotReservedBlockCount = None
            self.snapshotList = None
            self.errorCount = None
            self.firstErrorTime = None
            self.firstErrorInode = None
            self.firstErrorBlock = None
            self.firstErrorFunction = None
            self.firstErrorLine = None
            self.lastErrorTime = None
            self.lastErrorInode = None
            self.lastErrorLine = None
            self.lastErrorBlock = None
            self.lastErrorFunction = None
            self.mountOptions = None
            self.inodeOfUserQuotaFile = None
            self.infodeOfGroupQuotaFile = None
            self.overheadBlocks = None
            self.superblockBackups = None
            self.encryptionAlgorithms = None
            self.encryptionSalt = None
            self.inodeLostAndFound = None
            self.inodeProjectQuota = None
            self.checksumSeed = None
            self.wtimeHigh = None
            self.mtimeHigh = None
            self.makeFileSystemTimeHigh = None
            self.lastCheckHigh = None
            self.firstErrorTimeHigh = None
            self.lastErrorTimeHigh = None
            self.zeroPadding = None
            self.encoding = None
            self.encodingFlags = None
            self.reservedPadding = None
            self.checksum = None

class Inode:
    def __init__(self, args=[]):

        if len(args):
            self.fileMode = args[0]
            self.uidLow = args[1] 
            self.sizeLow = args[2]
            self.accessTime = args[3]
            self.changeTime = args[4]
            self.modificationTime = args[5]
            self.deletionTime = args[6]
            self.gidLow = args[7]
            self.linkCount = args[8]
            self.blockCountLow = args[9]
            self.flags = args[10]
            self.osd1 = args[11]
            self.blockMap = args[12]
            self.fileVersion = args[13]
            self.extendedAttributeBlockLow = args[14]
            self.fileDirectorySizeHigh = args[15]
            self.fragmentAddress = args[16]
            self.osd2 = args[17]
            self.extraSize = args[18]
            self.checksumHigh = args[19]
            self.extraChangeTime = args[20]
            self.extraModificationTime = args[21]
            self.extraAccessTime = args[22]
            self.creationTime = args[23]
            self.versionHigh = args[24]
            self.projectID = args[25]

        else:
            self.fileMode = None
            self.uidLow = None
            self.sizeLow = None
            self.accessTime = None
            self.changeTime = None
            self.modificationTime = None
            self.deletionTime = None
            self.gidLow = None
            self.linkCount = None
            self.blockCountLow = None
            self.flags = None
            self.osd1 = None
            self.blockMap = None
            self.fileVersion = None
            self.extendedAttributeBlockLow = None
            self.fileDirectorySizeHigh = Non
            self.fragmentAddress = None
            self.osd2 = None
            self.extraSize = None
            self.checksumHigh = None
            self.extraChangeTime = None
            self.extraModificationTime = None
            self.extraAccessTime = None
            self.creationTime = None
            self.versionHigh = None
            self.projectID = None

    def printSize(self):



p = Partition(sys.argv[1])
#p.findSuperblock()
p.findInodes()

답변1

아니요진짜ext4 inode 형식에 대한 매직 넘버(나중에 자세히 설명)이므로 요청하는 것이 완전히 가능하지는 않습니다. 거기e2fsprogs 코드의 "findsuper" 및 ext3grep과 같은 도구는 장치를 스캔하고 슈퍼블록 매직 넘버(백업일 수도 있음)를 찾은 다음 e2fsck를 사용하여 복구를 시도할 수 있습니다.

ext4는 상대적으로 고정된 디스크 형식을 가지므로 파일을 검색할 필요가 상대적으로 적습니다. 슈퍼블록을 찾은 다음 그룹 설명자(또는 해당 백업)의 위치를 ​​계산하고 모든 inode를 직접 읽는 것이 더 쉽습니다. 해당 위치는 포맷팅 시 고정되기 때문입니다.

하지만, 이 글을 쓰면서 깨달았다.정말로 이 방법을 사용하고 싶다면 몇 가지 마법의 숫자가 inode 본체에 삽입되어 있을 뿐만 아니라 사용할 수 있는 다른 기능도 있습니다.

inode가 충분히 크고(256바이트 이상, 모든 최신 파일 시스템의 기본값이어야 함) xattr이 들어갈 만큼 작은 경우(SELinux의 경우), 빠른 xattr 함수는 xattr을 아이노드.

EXT4_XATTR_MAGIC  0xEA020000

i_extra_bytes이는 추가 inode 필드 뒤의 inode의 두 번째 128바이트에 저장됩니다. 최소한 이는 4바이트 경계에서 적절하게 정렬되며, 아마도 inode가 시작된 후 약 128+32바이트에 정렬됩니다(inode가 작성된 커널 버전에 따라 다름).

i_blocks둘째, 범위 형식(최신 파일 시스템에서도 일반적임)을 사용하는 inode는 이 기능의 사용을 나타내기 위해 필드 시작 부분에 범위 매직 필드를 저장합니다 .

EXT4_EXT_MAGIC    0xf30a

inode의 적절한 오프셋에서 이러한 값 중 하나 또는 둘 다를 찾으면 inode가 있을 가능성이 높습니다.

관련 정보