Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

Building a FUSE Filesystem for WAD Archives: A Stealth Data Exfiltration Tutorial

Learn how to implement a FUSE-based userspace filesystem to read and write WAD files, the format used by classic games like DOOM. This tutorial covers WAD parsing, directory structure, and FUSE integration with a spy-themed narrative.

FUSE filesystem tutorial WAD file format DOOM modding C++ filesystem programming userspace filesystem stealth data exfiltration FUSE API C++ WAD parser game file steganography retro gaming programming FUSE mkdir mknod WAD descriptor lump namespace markers map markers FUSE readdir getattr covert data hiding COP4600 project

Introduction: The Mission Behind the Filesystem

In the world of covert operations, hiding data in plain sight is a time-honored tradition. In this tutorial, you'll build a userspace filesystem using the FUSE (Filesystem in UserSpacE) API to access data in the WAD (Where's All the Data) format—the same format used by classic PC games like DOOM and Hexen. Imagine you're a resistance fighter in a lizard-invasion scenario: you need to exfiltrate encrypted bits embedded in game mods. This project gives you the skills to implement read and write access to files and directories within WAD files, all while learning about file systems, C++ programming, and low-level data structures.

By the end of this guide, you'll understand how to parse WAD headers, descriptors, and lumps, and how to expose them as a mountable filesystem. This is not just a programming exercise—it's a lesson in how data can be hidden and retrieved using game files, a technique that resonates with modern trends like steganography in AI-generated images or secure messaging apps.

Understanding the WAD File Format

WAD files are binary containers that store game assets: textures, maps, sounds, and more. They consist of three sections:

  • Header: Contains the magic string (e.g., "IWAD" or "PWAD"), the number of descriptors, and the offset to the descriptor list.
  • Descriptors: An array of entries, each describing a lump (data block) with its file offset, size, and name.
  • Lumps: The actual data blocks, stored sequentially in the file.

All integers are in little-endian format. Since modern x86 processors are also little-endian, you can read them directly without byte swapping.

Parsing the Header

The header is 12 bytes long:

struct WadHeader {
    char magic[4];    // e.g., "IWAD"
    uint32_t numDescriptors;
    uint32_t descriptorOffset;
};

Load the WAD file into memory, read the header, and verify the magic ends with "WAD". Then seek to the descriptor offset to read the descriptor array.

Descriptors and Lumps

Each descriptor is 16 bytes:

struct WadDescriptor {
    uint32_t lumpOffset;
    uint32_t lumpSize;
    char name[8];     // null-padded
};

The name field is exactly 8 bytes; if the name is shorter, it's null-padded. For example, "E1M1" becomes "E1M1\0\0\0\0".

Lumps are the actual data. You can read a lump by seeking to its offset and reading lumpSize bytes.

Building the Wad Class

Your library should provide a Wad class that encapsulates the WAD data. Key methods include:

  • static Wad* loadWad(const string &path) – Loads a WAD file and returns a pointer to a new Wad object. The caller must delete it.
  • string getMagic() – Returns the magic string.
  • bool isContent(const string &path) – Returns true if the path points to a lump (file).
  • bool isDirectory(const string &path) – Returns true if the path is a directory (marker).
  • int getSize(const string &path) – Returns the size of a lump, or -1 if it's a directory.
  • int getContents(const string &path, char *buffer, int length, int offset) – Copies lump data into buffer, starting at offset. Returns bytes copied.

To handle directories, you need to interpret marker descriptors. Map markers have names like "E1M1" (format E#M#). They are followed by exactly 10 map element descriptors. Namespace markers come in pairs: "F1_START" and "F1_END". Everything between them belongs to a directory named "F1".

For example, given descriptors in order: "F1_START", "LUMP1", "LUMP2", "F1_END", the filesystem should show /F1/LUMP1 and /F1/LUMP2. Map markers create directories like /E1M1/ with its 10 children.

Implementing the FUSE Daemon

FUSE lets you implement a filesystem in userspace. You'll create a daemon that mounts a virtual directory tree based on the WAD structure. The FUSE API provides callbacks for operations like getattr, readdir, open, read, mkdir, mknod, write, etc.

Setting Up FUSE

Include fuse.h (or fuse3/fuse.h) and define a struct to hold your Wad object. In main(), call fuse_main() with your callback operations.

struct fuse_operations wad_ops = {
    .getattr = wad_getattr,
    .readdir = wad_readdir,
    .open = wad_open,
    .read = wad_read,
    .mkdir = wad_mkdir,
    .mknod = wad_mknod,
    .write = wad_write,
    // ...
};

Implementing Callbacks

getattr: Return file attributes. For directories, set st_mode to S_IFDIR | 0755. For files, set S_IFREG | 0644 and st_size to the lump size. Use isContent and isDirectory to determine the type.

readdir: Fill the directory listing. For the root "/", list all top-level directories (namespaces and map markers) and any orphan lumps. For subdirectories, list their children. Use filler() to add entries.

open: For files, simply return 0 (success). You don't need to maintain file handles; just verify the path is content.

read: Use getContents() to copy data into the buffer. The size and offset parameters come from FUSE.

mkdir: Create a new namespace directory. This involves adding a new descriptor pair (e.g., "NEW_START" and "NEW_END") to the WAD file. You'll need to update the header and rewrite the descriptor list and lumps.

mknod: Create a new empty file (lump) inside a namespace directory. Add a descriptor with offset and size 0. The lump data will be written later.

write: Write data to an existing lump (only for empty files you created). Update the lump data and descriptor size.

Example: Navigating the Mounted WAD

Suppose you have a WAD file game.wad with the following structure:

  • Header: magic="PWAD", numDescriptors=5, descriptorOffset=12
  • Descriptors:
    1. "F1_START" (offset=0, size=0)
    2. "HELLO" (offset=100, size=5, data="Hello")
    3. "WORLD" (offset=105, size=5, data="World")
    4. "F1_END" (offset=0, size=0)
    5. "E1M1" (offset=0, size=0) – map marker, followed by 10 map lumps (not shown)

After mounting with FUSE, you'll see:

$ ls /mnt/wad/
F1  E1M1
$ ls /mnt/wad/F1/
HELLO  WORLD
$ cat /mnt/wad/F1/HELLO
Hello

You can also create new directories and files:

$ mkdir /mnt/wad/MYSTUFF
$ touch /mnt/wad/MYSTUFF/secret.txt
$ echo "Top secret" > /mnt/wad/MYSTUFF/secret.txt
$ cat /mnt/wad/MYSTUFF/secret.txt
Top secret

Behind the scenes, the daemon adds a "MYSTUFF_START" and "MYSTUFF_END" pair, and a descriptor for "secret.txt" with the written data.

Testing and Debugging

Use fusermount -u to unmount. Test with sample WAD files from the assignment. Verify that map marker directories are read-only and cannot have new files added. Check edge cases: paths with multiple levels, names longer than 8 characters (truncate or error), and writing to existing lumps (should be forbidden).

To debug, add logging to stderr (FUSE captures it). Use gdb or print statements. Remember that FUSE callbacks run in a multithreaded context; protect shared data with mutexes.

Connecting to Modern Trends

This project mirrors real-world steganography used in AI-generated images (e.g., hiding watermarks in Stable Diffusion outputs) or in secure messaging apps like Signal. The concept of hiding data in game files is also reminiscent of how some malware uses innocent-looking files to exfiltrate data. By mastering WAD files and FUSE, you're learning skills applicable to cybersecurity, digital forensics, and systems programming.

Moreover, the trend of retro gaming has exploded—DOOM is still being ported to everything from calculators to car dashboards. Understanding its file format gives you a backstage pass to modding culture. Imagine creating a mod that secretly transmits intelligence; your FUSE daemon is the first step.

Conclusion

You've built a functional userspace filesystem for WAD archives, complete with directory parsing, file read/write, and FUSE integration. This project not only fulfills the assignment requirements but also arms you with knowledge of low-level file formats and systems programming. The resistance—and your grade—will thank you.