Ext4 Forensics: Inode Bitmaps

 



An inode bitmap is a sequence of bits that tracks inode allocation status within a block group/flex group. A single bit is used to represent each inode, with a value of 1 signifying that the inode is in use and a value of zero signifying that the inode is not being used. In the case of the block bitmap, the least significant bit of byte 0 represents block 0 in the block group’s data area. The most significant bit in byte 0 represents block 7 in the data area. In byte 1, the least significant bit represents block 8, and so on. The process is the same in the inode bitmap except that the first inode number is 1, rather than 0. Hence, the least significant bit in byte 0 of the inode bitmap represents the allocation status of inode 1. The most significant bit represents the allocation status of inode 8 and so on.


In flex_bg, inode bitmaps for several groups are co-located in the first group of the flex group. The inode bitmap occupies exactly one block (typically 4096 bytes = 32768 bits), allowing it to track up to 32768 inodes. Its location is stored in the group descriptor as follows:


  • bg_inode_bitmap_lo (bytes offset 0x04 - 0x07)  lower 32 bits
  • bg_inode_bitmap_hi (bytes offset 0x24-0x27; if 64-bit [INCOMPAT_64BIT] feature enabled)  upper 32 bits.


In ext4 (and earlier ext variants), each block group has:


  • A fixed number of blocks per group (blocks_per_group, often matching the block size in bits—e.g., 32,768 for 4 KB blocks).
  • A separate number of inodes per group (inodes_per_group), set at filesystem creation time via mkfs.ext4 (defaults commonly range from 8,192 to 16,384 or similar, depending on the inode ratio, like one inode per 16 KB of space).


The inode bitmap for each block group is always one block in size (e.g., 4,096 bytes on a 4 KB-block file system). It uses 1 bit per inode to track whether that inode is allocated or free. Since inodes_per_group is typically much smaller than blocks_per_group (e.g., 8,192 inodes need only 1,024 bytes of bitmap = 8,192 bits / 8), the inode bitmap rarely fills the entire block. This leaves slack space (unused bytes) at the end of each inode bitmap block. The usable slack space per inode bitmap block is approximately BlockSize−(inode_per_group/8) bytes. (The division by 8 converts inodes to bytes needed.) This slack exists repeatedly across all block groups (there are many block groups on any reasonably sized file system). The total hidden storage capacity is therefore roughly (BlockSize−(inode_per_group/8 ))*BlockGroups bytes. This slack space is not checked or used by the kernel or standard file system tools for inode allocation tracking—it's just padding/unused. Writing arbitrary data there does not break file system consistency in a way that triggers errors during normal operation or most checks. Importantly, e2fsck (the standard ext4 file system checker) does not detect or complain about non-zero data in this inode bitmap slack space. It survives a forced check (e2fsck -f or similar) without flagging inconsistencies, because the tool only validates the actual bits corresponding to the valid inode range—it ignores or tolerates the trailing bytes.


When conducting ext4 file system forensics, one commonly overlooked structure is the bg_flags field of the block group descriptor. The bg_flags field is a 16-bit value in each block group descriptor (at offset 0x12 in struct ext4_group_desc) that contains bitwise flags that describe the initialization and state of the block group's block bitmap, inode bitmap, and inode table. It is part of ext4's lazy initialization features, which optimize file system creation (e.g., via mkfs) by deferring full setup of bitmaps and tables until needed, reducing initial formatting time. Failing to interpret bg_flags correctly can lead to false deleted-file conclusions, misinterpretation of uninitialized inode tables, incorrect timeline reconstruction, and, ultimately, courtroom credibility issues.


The inode bitmap determines allocation status, where the value 0 denotes a free inode, and the value 1 denotes an allocated inode. But this allocation status is only valid if the group descriptor says the inode table and bitmap are initialized (i.e., EXT4_BG_INODE_UNINIT is NOT set). That is where the bg_flags becomes critical. These flags directly determine whether allocation metadata is forensically trustworthy. The bg_flags field can have any combination of the following values.


  • EXT4_BG_INODE_UNINIT (0x0001) → This is the most important flag for inode bitmap forensics. This implies that the inode bitmap and inode table are not initialized. The kernel treats the inode bitmap as all zeros (all inodes free), even if metadata might reside in the group. It has the following forensic implication:
    • The inode bitmap does NOT contain valid allocation data.
    • A '0' bit does NOT mean the inode was deleted. You might find historical data that predates the file system creation—but it's not evidence of file deletion.
    • Allocation status cannot be reconstructed reliably. You cannot trust any inode structures, extents, timestamps, etc., found there for allocation status or file recovery.
    • Thus, if EXT4_BG_INODE_UNINIT is set, their bitmaps or inode tables have not yet been fully initialized. In such cases, the kernel treats the group as logically empty until it is first used. While allocation bitmaps reflect the logical file system state, the underlying inode table blocks may still contain residual data until background zeroing occurs. Forensic tools that rely solely on bitmap interpretation for quick scanning may overlook structures present in uninitialized tables. Therefore, investigators should directly parse inode tables and examine journal data (JBD2) to verify file system contents. In newly created or recently resized large file systems, deferred initialization may leave remnants of prior data in uninitialized groups, creating potential recovery opportunities.
    • If this flag is not set, the inode table is fully initialized and trustworthy. The inode bitmap can be read to identify free (or potentially deleted) inodes. The inode table is valid and can be parsed for i_dtime, extents, etc.
  • EXT4_BG_BLOCK_UNINIT (0x0002)  This is irrelevant to inodes. It implies that the data block bitmap is not initialized. The kernel assumes all data blocks in the group are free. It was covered in a previous post.
  • EXT4_BG_INODE_ZEROED (0x0004)  This indicates that the inode table for that block group has been zeroed out (fully initialized with zeros on disk). This flag works in conjunction with related flags like EXT4_BG_INODE_UNINIT (0x0001), which signals that the inode bitmap and table were not fully set up during mkfs (a "lazy" or deferred initialization feature introduced in ext4 to speed up filesystem creation on large volumes). When EXT4_BG_INODE_ZEROED is set, and EXT4_BG_INODE_UNINIT is not set, it confirms that background initialization (or explicit zeroing) has completed for the inode table in that block group. The kernel's background thread (ext4lazyinit) or foreground processes zero out the inode table entries to ensure no old/garbage data is mistaken for valid inodes. However, even when the flag indicates the table is "zeroed," this only means the unused portions of the inode table were initialized to zero during creation/mount. Allocated (in-use) inodes naturally contain valid metadata (permissions, timestamps, pointers, etc.). Previously allocated (but now freed) inodes may retain residual data until they are reused and re-zeroed or overwritten. The file system does not proactively wipe freed inode slots beyond the initial zeroing phase. In practice, for unallocated inode slots in a fully initialized block group, you should see zeros if nothing suspicious has happened. Non-zero data in those regions can be a red flag in forensics. It might indicate:
    • Interrupted or partial background initialization (e.g., system crash/power loss during lazy init).

    • File system inconsistency or corruption (e.g., e2fsck would complain).

    • Deliberate metadata manipulation (an anti-forensics technique, such as writing hidden data into slack/unused inode space before forcing the flag or interrupting init).


Uninitialized block groups—marked primarily by the EXT4_BG_INODE_UNINIT (0x0001) and/or EXT4_BG_BLOCK_UNINIT (0x0002) flags in the group descriptor—are prime candidates for data hiding in ext4 forensics. The lazy initialization feature (enabled by default during mkfs.ext4 for performance on large volumes) defers zeroing of inode tables, inode bitmaps, and block bitmaps until allocation is needed or the kernel's background thread completes it. This leaves those on-disk structures untouched (potentially containing old/garbage data or adversary-written content) until explicitly initialized. Adversaries can exploit this by writing arbitrary data directly to the uninitialized inode tables, bitmaps, or related slack space (using low-level tools like dd or debugfs in raw mode), as normal filesystem operations won't trigger initialization or overwrite the area if no allocation occurs in that group. Such hidden data can persist indefinitely if higher-numbered groups (initialized later) are targeted and no heavy filesystem activity forces early zeroing.


Tools like debugfs may not fully account for or display these uninitialized regions correctly (assuming calculated free status or skipping them), potentially missing anomalies, while hex/carving examination of the raw device can reveal non-zero patterns, steganographic payloads, or manipulated metadata.

This technique is explicitly documented in ext4 anti-forensics literature (e.g., Göbel & Baier's 2018 papers on ext4 data hiding and Fairbanks' 2012 low-level ext4 analysis) as offering high capacity (entire bitmap/table blocks usable, not just slack) with advanced detection difficulty—many standard forensic tools overlook it, and e2fsck won't flag it unless checksums (if enabled via metadata_csum) mismatch against the superblock UUID or other integrity fields. This underscores why thorough ext4 analysis demands parsing group descriptors at a low level, examining bg_flags, and manually inspecting uninitialized structures rather than relying solely on high-level tools.


To detect the INODE_UNINIT feature quickly in the system under investigation, you can use the dumpe2fs utility as shown below.



When we analyzed the contents of the group descriptor table in our example image, we saw that the inode bitmap for group 0 started in block 1051. It is perhaps important to state here that the bg_flags value for block group 0 in our image is 0x0004 (EXT4_BG_INODE_ZEROED). We can extract the content of that block with blkcat or dd, as shown below:




To determine the correct byte, we subtract 1 to account for the first inode of the group and divide by 8.


inode 24 = (24 - 1)/8 = 2 remainder 7


The bit for inode 24 is the most significant bit (index position 7) in byte 2 (0x7f), which is a 0, so it is not allocated. We can get all the inode details from the istat command, and they are listed here for the inode that we just examined:



Post a Comment

Previous Post Next Post