A C++ tool that reads raw EXT2 filesystem images and performs forensic analysis — reconstructing the full directory hierarchy (including deleted/ghost entries) and recovering the complete filesystem action history from inode metadata.
- Raw EXT2 parsing — Directly reads superblock, block group descriptors, inode tables, and directory entries from disk images. No mounting, no external libraries, no kernel code.
- Directory tree reconstruction — Builds and displays the complete file hierarchy using depth-first traversal, including proper handling of indirect blocks (single, double, triple).
- Ghost entry detection — Recovers "deleted" directory entries that still exist in slack space after being absorbed by neighboring entries via length expansion. These represent files/directories that were moved or removed.
- Action history recovery — Correlates inode timestamps (
access_time,change_time,modification_time,deletion_time) with directory entry metadata to reconstruct the sequence of filesystem operations:touch— File creationmkdir— Directory creationrm/rmdir— File/directory deletionmv— File/directory moves and renames (including chain moves detected via multiple ghost entries)
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ EXT2 Image │────▶│ Parse Structures│────▶│ Build Directory │
│ (raw bytes) │ │ (SB, BGD, Inodes│ │ Tree + Ghosts │
└───────────────┘ └──────────────────┘ └──────────────────┘
│
┌──────────────────┐ │
│ Correlate │◀─────────────┘
│ Timestamps │
│ + Ghost Entries │
└──────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ State │ │ History │ │ Move │
│ Output │ │ Output │ │ Chains │
│ (tree) │ │ (events)│ │ (A→B→C) │
└──────────┘ └──────────┘ └──────────┘
The analyzer works in two passes:
- State pass: DFS traversal from root (inode 2), printing the directory hierarchy. Ghost entries are detected by scanning slack space in directory blocks and displayed in parentheses.
- History pass: Collects all unique timestamps from every discovered inode, then classifies each timestamp as a creation, deletion, or move event based on which inode fields match.
make allRequires a C++17 compatible compiler (g++ or clang++).
./histext2fs <image_path> <state_output> <history_output>| Argument | Description |
|---|---|
image_path |
Path to the raw EXT2 filesystem image |
state_output |
Output file for the current directory hierarchy |
history_output |
Output file for the reconstructed action history |
./histext2fs disk.img state.txt history.txt- 2:root/
-- 11:lost+found/
-- 13:home/
--- 15:user1/
---- 22:file1.txt
---- 20:file2.txt
---- (19:file1.txt)
--- 21:user2/
---- 19:file4.txt
--- (16:user3/)
-- 12:etc/
--- 14:config.txt
- Dashes indicate depth level (root = 1 dash)
- Directories end with
/ - Ghost (deleted/moved) entries are wrapped in
(parentheses) - Ghost directories show only the entry, not their contents
1746963260 mkdir [/home] [2] [13]
1746963353 touch [/home/user1/file1.txt] [15] [19]
1746963412 mv [/home/user1/file1.txt /home/user2/file4.txt] [15 21] [19]
1746963455 rm [/home/user3/file5.txt] [16] [17]
Each line: TIMESTAMP ACTION [ARGS] [AFFECTED_DIRS] [AFFECTED_INODES]
- Timestamps are Unix timestamps (seconds since epoch)
?is used when information cannot be recovered (e.g., intermediate move timestamps)
| Structure | Location |
|---|---|
| Superblock | Byte offset 1024 |
| Block Group Descriptor Table | First block after superblock |
| Inode Table | From BGDT inode_table field |
| Directory Entries | Variable-length linked list in blocks |
When a file is moved or deleted, its directory entry isn't erased — the neighboring entry's length field is expanded to absorb the space. The ghost entry's content remains intact in the "slack space." The analyzer scans this slack space by:
- Computing the minimum record length for each live entry
- Scanning the gap between that minimum and the actual
lengthfield - Validating potential ghost entries by checking inode number range and reading the inode from disk
When a file is moved multiple times (A → B → C), it leaves ghost entries in each intermediate directory. The analyzer:
- Identifies chain moves by finding inodes with 2+ ghost entries and 1 live entry
- Outputs intermediate moves with
?timestamps (since the on-disk timestamps are overwritten by subsequent operations) - Detects the final move using the inode's
change_time
├── histext2fs.cpp # Main implementation (~700 lines)
├── ext2fs.h # EXT2 structure definitions
├── Makefile # Build configuration
└── README.md
MIT