Bringing unallocated data back: the FAT12/16/32 case

Modern operating systems provide a way to increase the size of a given file without writing to it.

In Unix-like operating systems, this is achieved through the truncate() and ftruncate() system calls. These calls allow programs to decrease or increase the file size.

If the file size is decreased, data beyond the new end-of-file position is discarded (it can survive as deleted data, but there is no way to bring these bytes back by restoring the original file size).

If the file size is increased, extra data (data after the old end-of-file position) is filled with null bytes. Internally, many modern file systems don’t write those extra null bytes to the drive, they make a sparse data range instead (so, if a program increases the file size by several gigabytes, there is no need to actually write that amount of null bytes to the drive).

Some file systems may reserve sectors to hold that amount of extra data and attach those sectors to the file, but their contents are left intact. This is useful to quickly preallocate some space to avoid fragmentation: a file system driver reserves the requested amount of sectors, but it doesn’t clear them (and an attempt to read from that extra space returns null bytes, despite the fact that these extra sectors may contain something different).

Treating the extra space as containing null bytes is essential for security, there is a POSIX requirement for that:

If the file size is increased, the extended area shall appear as if it were zero-filled.

https://pubs.opengroup.org/onlinepubs/9699919799/functions/ftruncate.html

It’s a vulnerability if deleted (or uninitialized) data is returned when reading that extra area (for example, see CVE-2021-4155).

Surprisingly, such vulnerabilities are still discovered in file system drivers.

In the FAT12/16/32 driver of the FreeBSD operating system (which is called msdosfs), it’s possible to extend the file size using the truncate() and ftruncate() system calls.

There is an interesting case: extending the file size within the boundary of the file’s last cluster. In this case, remnant (deleted) data existing in the slack space of that cluster is exposed as file data.

Slack space means remnant (deleted) data after the old (before the file size extension) end-of-file position. Typically, it contains deleted data or data which wasn’t overwritten during the format operation (e.g., data from a previous file system existed on that drive).

If the mount point of a FAT12/16/32 volume is exposed to (accessible by) unprivileged users, this bug allows them to read some amount of deleted data from the underlying drive. So, it’s a vulnerability.

Here is a screenshot:

The “SECRET DATA” string was used to fill the entire drive before the format operation. After the format operation, this string doesn’t belong to valid data of any allocated file. Because of the bug, this string now appears as valid data of the “/test.txt” file.

The bug won’t be triggered if the new file size requires the driver to allocate a cluster. The new file size must be within the length of a cluster chain already assigned to that file, so no allocation is required to serve the requested data extension.

(If the cluster size is 4096 bytes and the current file size is 1 byte, the vulnerability can be triggered by extending the file size, with the final file size up to 4096 bytes.)

The same vulnerability also exists in the OpenBSD operating system.

This vulnerability has some forensic implications — occasionally, deleted data can become a part of “valid” file data. Previously, this affected some system files. But now this also includes user-generated files.

Timeline

One thought on “Bringing unallocated data back: the FAT12/16/32 case

Leave a comment