Ext4 Forensics: Inode Table




An inode (index node) is a fixed-size data structure that holds metadata about a file, directory, symlink, etc., but not the file name or the actual file content/data. Typical metadata includes file type and permissions, owner UID/GID, size (in bytes), timestamps (creation/change/modification/access), link count (for hard links), and pointers to data blocks (in ext4, usually via extents rather than direct/indirect data blocks).  The inode size can be set to any power-of-two larger than 128 bytes size up to the file system block size by using the mke2fs-I [inode size] option at format time. Previous versions of the extended file system used a 128-byte-sized inode, which is already crowded with data and has little space for new fields. In ext4, the default inode structure size is 256 bytes. However, the first 128 bytes are backward compatible with previous versions of ext.


Inodes are numbered, starting with inode number 1, and stored in the inode table of their respective block group. There is no inode 0. Inode numbers are analogous to MFT entry number in NTFS forensics. The ext4 inode table is a contiguous on-disk structure in the ext4 file system that stores all the inodes (index nodes) for a specific block group. It is a linear array of these inode structures—basically one long sequence of 256-byte (or whatever inode size) records. Each block group in an ext4 filesystem has its own inode table. The location (starting block) of the inode table for a block group is recorded in that group's entry in the Group Descriptor Table as follows:


  • bg_inode_table_lo (bytes offset 0x08-0x0B) → Lower 32 bits.
  • bg_inode_table_hi (bytes offset 0x28-0x2B if the 64-bit feature [INCOMPAT_64BIT] is enabled) → Upper 32 bits.


The number of inodes per group is set when the file system is created (controlled by the -i / inode_ratio option; common default is one inode per 16 KB–32 KB of space). The inode bitmap (one bit per inode) tells which inodes in the inode table are used/free. The inode table itself contains no headers or separators — it's purely a flat array of struct ext4_inode. To determine the block group of a respective inode, we use the formula given below:


block_group = (inode_number - 1) / sb.s_inodes_per_group


To calculate the index of the used inode within the block group, we use the following formula:


index = (inode_number - 1) % sb.s_inodes_per_group


Then you go to that group's inode table and read the entry at the calculated offset.


The inodes 1 to 10 are reserved for special functions. For example, inode 2 represents the root directory of the volume and inode 8 represents the file system journal, which was added in Ext3. In Ext4, for the sake of compatibility with prior versions, only a few changes to the inode structure have been implemented. In Ext4, some of the unused space of Ext3 has been used to introduce new attributes. The inode table entry is laid out in struct struct ext4_inode as seen in the table below. Note that the size of the structure is 160 bytes, though the standard inode size in ext4 is 256 bytes.  The extra 96 bytes are used to store extended attributes.


Offset

Size (bytes)

Name

Description

0x00

0x2

i _mode

File mode (type and permissions).

0x02

0x2

i_uid

Lower 16 bits of the owner id (UID).

0x04

0x4

i_size_lo

Lower 32 bits of the file size (in bytes).

0x08

0x4

i_atime

Last access time, in seconds since the epoch.

0x0C

0x4

i_ctime

Last inode change time, in seconds since the epoch.

0x10

0x4

i_mtime

Last data modification time, in seconds since the epoch.

0x14

0x4

i_dtime

Deletion Time, in seconds since the epoch.

0x18

0x2

i_gid

Lower 16 bits of group id (GID).

0x1A

0x2

i_links_count

Number of hard links pointing to this file. With the DIR_NLINK feature enabled, ext4 supports more than 64,998 subdirectories by setting this field to 1 to indicate that the number of hard links is not known.

0x1C

0x4

i_blocks_lo

Lower 32 bits of 512-byte blocks this file uses.

0x20

0x4

i_flags

Inode flags. Valid values include:

  • 0x1 - This file requires secure deletion. (Not implemented).
  • 0x2 - This file should be preserved should un-deletion be desired. (Not implemented).
  • 0x4 - File is compressed. (Not really implemented).
  • 0x8 - All writes to the file must be synchronous.
  • 0x10 - File is immutable.
  • 0x20 - File can only be appended.
  • 0x40 - The dump (1) utility should not dump this file.
  • 0x80 - Do not update access time.
  • 0x100 - Dirty compressed file. (Not used).
  • 0x200 - File has one or more compressed clusters. (Not used).
  • 0x400 - Do not compress file. (Not used).
  • 0x800 - Compression error. (Not used).
  • 0x1000 - Directory has hashed indexes.
  • 0x2000 - AFS magic directory.
  • 0x4000 - File data must always be written through the journal.
  • 0x8000 - File tail should not be merged.
  • 0x10000 - All directory entry data should be written synchronously.
  • 0x20000 - Top of directory hierarchy.
  • 0x40000 - This is a huge file.
  • 0x80000 - Inode uses extents.
  • 0x200000 - Inode used for a large extended attribute.
  • 0x400000 - This file has blocks allocated past EOF.
  • 0x80000000 - Reserved for ext4 library.

Aggregate Flags:

  • 0x4BDFFF - User-visible flags.
  • 0x4B80FF - User-modifiable flags.

0x24

0x4

i_osd1

OS descriptor 1. Values include:

 

Tag

Contents

Offset

Size

Name

Description

linux1

0x0

0x4

l_i_version

Version

hurd1

0x0

0x4

h_i_translator

??

masix1

0x0

0x4

m_i_reserved

??

 

0x28

0x3C

i_block[EXT4_N_BLOCKS]

Block map or Extent tree.

0x64

0x4

i_generation

File version for NFS.

0x68

0x4

i_file_acl_lo

Lower 32-bit address of extended attribute block.

0x6C

0x4

i_size_high

Higher 32-bit address of file size

0x70

0x4

i_obso_faddr

(Obsolete) fragment address

0x74

0xC

i_osd2

OS descriptor 2.

 

Tag

Contents

Offset

Size

Name

Description

linux2

0x0

 

 

 0x2

 

 

 

 

 

 0x4

 

 

 0x6

 

 0x8

0x2

 

 

 0x2

 

 

 

 

 

 0x2

 

 

 0x2

 

 0x4

l_i_blocks_hi

 

 

 l_i_file_acl_high

 

 

 

 

 

 l_i_uid_high

 

 

 l_i_gid_high

 

 l_i_reserved2       

Upper 16-bits of the block count.

 

Upper 16-bits of the extended attribute block (historically, the file ACL location).

 

Upper 16-bits of the Owner UID.

 

Upper 16-bits of the GID.

??

hurd2

0x0

 

0x2

 

 

 

0x4

 

 

 

0x6

 

 

 

0x8

0x2

 

0x2

 

 

 

0x2

 

 

 

0x2

 

 

 

0x4

h_i_reserved1

 

h_i_mode_high

 

 

 

h_i_uid_high

 

 

 

h_i_gid_high

 

 

 

h_i_author

??

 

Upper 16-bits of the file mode.

 


Upper 16-bits of the Owner UID.

 


Upper 16-bits of the GID.

 


Author code?

masix2

0x0

 

0x2

 

 

 

 

 

 0x4

0x2

 

0x2

 

 

 

 

 

 0x4

m_i_reserved1

 

m_i_file_acl_high

 

 

 

 

 

 m_i_reserved2[2]

??

 

Upper 16-bits of the extended attribute block (historically, the file ACL location).

 

??


0x80

0x2

i_extra_isize

Size of the used area of inode - 128.

0x82

0x2

i_checksum_hi

Upper 16 bits of the inode checksum.

0x84

0x4

i_ctime_extra

Extra change time bits. This provides sub-second precision.

0x88

0x4

i_mtime_extra

Extra modification time bits. This provides sub-second precision.

0x8C

0x4

i_atime_extra

Extra access time bits. This provides sub-second precision.

0x90

0x4

i_crtime

File creation time, in seconds since the Unix Epoch.

0x94

0x4

i_crtime_extra

Extra file creation time bits. This provides sub-second precision.

0x98

0x4

i_version_hi

Upper 32 bits for version number.

0x9C

0x4

i_projid

Project ID.


Note: Every field with an offset greater than or equal to 0x80 is an extended field, meaning it was introduced in ext4 and is not backward compatible with ext2/3.


The 16-bit file mode value is broken up into three sections. In the lower 9 bits are the permissions flags, and each bit corresponds to a permission. The permissions use the notion of user, group, and world where user is the user ID in the inode, group is the group ID in the inode, and world is all other users. Each of these collections of users can have read, write, or execute permissions. The flags for each of these permissions are given below:


  • 0x1 → S_IXOTH (Others may execute)
  • 0x2 → S_IWOTH (Others may write)
  • 0x4 → S_IROTH (Others may read)
  • 0x8 → S_IXGRP (Group members may execute)
  • 0x10 → S_IWGRP (Group members may write)
  • 0x20 → S_IRGRP (Group members may read)
  • 0x40 → S_IXUSR (Owner may execute)
  • 0x80 → S_IWUSR (Owner may write)
  • 0x100 → S_IRUSR (Owner may read)


The next three bits are for executable files and directories. If any of these are set, an executable will behave differently when it is run, or files in a directory will have special properties. These flags are listed below:


  • 0x200 → S_ISVTX (Sticky bit):  If it is set, it means that all files within this directory can only be modified by the owner.
  • 0x400 → S_ISGID (Set GID): When set on executables, it runs with the file's group ID. When set on directories, new files created inside inherit the directory's group ID.
  • 0x800 → S_ISUID (Set UID):  Set-UID makes sure an executable uses the owner as the user executing the file and not the actual user executing it.


Lastly, bits 12 to 15 identify the type of the file that the inode is for. Knowing the type of the file tells the investigator what kind of inode is under investigation. This can give insight into if an inode describes a communication socket (two-ways communication) or FIFO (one-way communication), or if it is just a pointer (symbolic link) to another inode, or if the inode is used to access a storage device (for instance a sd_card). These are values and not flags, so only one should be set. They are listed below:


  • 0x1000  S_IFIFO (FIFO or named pipe)
  • 0x2000 → S_IFCHR (Character device)
  • 0x4000 → S_IFDIR (Directory)
  • 0x6000 → S_IFBLK (Block device): Require all read/write operations to work on an entire block at a time.
  • 0x8000 → S_IFREG (Regular file)
  • 0xA000 → S_IFLNK (Symbolic link): Contents of the file are the path to the file pointed to. Path is stored in inode if the target string is less than 60 bytes. Otherwise, either extents or block maps will be used to allocate data blocks to store the link target.
  • 0xC000 → S_IFSOCK (Socket)


S_IFDIR and S_IFREG are the only 2 types that allocate data blocks in the file system (except symbolic links, sometimes). When an inode represents a device, it will be either a block device or a character device. A block device stores data in predefined blocks that may be randomly accessed. A character device can be read from and written to, and accessed as a sequential stream of bytes.

The time attributes allow an investigator to develop a timeline of an incident. For backward compatibility, these are located at byte offset 0x08 from the start of the inode. For each file or directory, the following timestamps are provided by ext4:


  • The last modification time (mtime) - changed by modifying a file’s content.
  • The last access time (atime) - changed by reading a file or running a program.
  • The last metadata change time (ctime) - keeps track of when the meta-information about the file was changed (e.g., owner, group, file permission, or access privilege settings).
  • The deletion time (dtime)


Each of the four original timestamps is stored as a 32-bit integer representing the number of seconds elapsed since the Unix epoch (January 1, 1970). With the introduction of ext4, a fifth timestamp—creation time (crtime)—was added to the inode structure. Additionally, ext4 increased the inode size to 256 bytes, providing extra space to enhance timestamp precision. To improve time representation, four of the five timestamps (excluding the deletion timestamp) were expanded to 64 bits through the addition of corresponding 32-bit fields named i_[c|m|a|cr]time_extra. In these extra fields, the lowest two bits are used to extend the original 32-bit seconds counter to 34 bits as depicted in the Figure below. This modification mitigates the Year 2038 overflow problem, pushing the new overflow date to May 10, 2446. The remaining upper 30 bits of each extra field store nanosecond-level precision. Since 30 bits are sufficient to represent nanosecond resolution, ext4 can achieve highly granular timestamps. This level of precision is supported because the Linux system clock itself provides nanosecond accuracy. We can only trust the timestamps, however, if there is no malware installed on the system or any other tools to manipulate the inode metadata.


Figure 1: Calculating ext4 timestamps


The four additional timestamp extension fields provide a potential capacity of 16 bytes per inode entry that could be misused to store hidden data. However, manipulating the lower two epoch-extension bits for steganographic purposes can result in timestamps that extend past the year 2038. Such anomalous dates may appear irregular and could alert a forensic examiner to the presence of concealed information and the existence of a covert data channel.


The total count of 512-byte blocks (sectors) allocated to a file is stored in the i_blocks_lo field of the inode. However, when the EXT4_HUGE_FILE_FL flag is set in the inode’s i_flags, and the filesystem superblock has the huge file feature enabled, the value in i_blocks_hi must also be incorporated. In such cases, the complete block count is calculated by combining both fields according to the appropriate formula.


(i_blocks_lo + i_blocks_hi ≪ 32)


If the i_flags has the EXT4_HUGE_FILE_FL inode, but the file system does not have the huge file feature, then the field i_blocks_hi needs to be added using this formula.


i_blocks_lo + (i_blocks_hi ≪ 32)


The i_links_count field records how many directory entries reference a particular inode. Each directory entry contains the inode number it is associated with. When more than one directory entry references the same inode, this represents the presence of hard links. Once the final directory entry linked to an inode is removed, the inode is marked as free in the inode bitmap. Symbolic (soft) links behave differently. Creating a symbolic link does not increase the link count of the target inode. Instead, it generates a separate inode designated for the symbolic link itself. This new inode stores a file path that refers to a directory entry rather than directly referencing another inode.


Let us look at inode entry 23 from our image. The first step is to identify the group of which it is a part. Bytes 0x28-0x2B of the Superblock show that there are 8,192 inodes per group. From the formula to determine the block group of a respective inode given earlier in this post, we can conclude that inode number 23 resides in block group 0. The starting address of the inode table is given in the group descriptor, and we previously saw that it starts in block 1067, and each block is 4,096 bytes. To extract the data, we will use dd with a block size of 4096 to skip ahead to the inode table and then use dd with a block size of 256 to skip ahead to inode 23. The first inode is number 1, so we need to subtract 1 from our skip value.


Figure 2: hexdump of inode number 23


Bytes 0 to 1 show the mode, which is 0xA1FF (1010 000 111 111 111b). These bits show us that others can read this file (0x004), others can write (0x002), and others can execute (0x001); the group can read (0x020), the group can write (0x010), and the group can execute (0x008); the user can read (0x100), the user can write (0x080), and the user can execute (0x040). The 3 bits marked red (bits 9 - 12)  are special privileges. Here they are 000, and it means the Set-UID, Set-GID, and the Sticky bit are not set. The four most significant bits identify the type of file, and it shows that it is a symbolic link (0xA000).


Bytes 4 to 7 show that the size of the file is 30 bytes (0x0000001E).  The access time and the access time (extra) fields are recorded in bytes 0x08-0x0B and 0x8C-0x8F and have the values 0x5D747AB0 (0101 1101 0111 0100 0111 1010 1011 0000b) and 0x50B369EC (0101 0000 1011 0011 0110 1001 1110 1100b) respectively. To calculate the last access time, we consider the two least significant bits of the access time’s extra field. These have the values 00b. These lowest two bits are used to extend the original 32-bit seconds counter to 34 bits as depicted in Figure 1. This becomes 0001011101011101000111101010110000b1567914672d. This decodes to 2019-09-08 03:51:12.0000000 UTC. The change time and the change time (extra) fields are recorded in bytes 0x0C-0x0F and 0x84-0x87, and have the values 0x5D72DCB0 and 0x87EAE2A8, respectively. Repeating the same process decodes to 2019-09-06 22:24:48.0000000 UTC. The modification time and the modification time (extra) fields are recorded in bytes 0x10-0x13 and 0x88-0x8B and have the values 0x5D72DCB0 and 0x86F6BEA8, respectively. Repeating the same process decodes to 2019-09-06 22:24:48.0000000 UTCThe creation time and the creation time (extra) fields are recorded in bytes 0x90-0x93 and 0x94-0x97 and have the values 0x5D72DCB0 and 0x86F6BEA8, respectively. Repeating the same process decodes to 2019-09-06 22:24:48.0000000 UTC.


Bytes 0x1A-0x1B show the link count is 1, which means that there is a file name pointing to it. The total number of blocks allocated to the inode, as revealed by offsets 0x1C-0x1F, is 0x00000000 (0 allocated 512-byte sectors), typical of symlinks whose link target is short (<= 60 bytes).


For Linux forensic investigations, mastering inode table analysis is as fundamental as mastering the MFT in NTFS investigations.

Post a Comment

Previous Post Next Post