This section details the design and implementation of several sample security file systems we wrote based on Wrapfs. The examples range from simple to complex. We attempted to cover a range of security features: intrusion detection, intrusion avoidance, analysis, access control, and encryption.
It should be noted that these examples are merely experimental file systems intended to illustrate the kinds of file systems that can be written based on Wrapfs. We do not consider them to be complete solutions. Whenever possible, we illustrate potential enhancements to our examples. We hope this would serve to convince the readers of the flexibility and ease of writing new file systems using Wrapfs. Our primary intent is to demonstrate that useful file systems can be written with a small amount of code and rapidly prototyped. Moreover, several such file systems can be stacked together, to form a union of their functionality.
User files in their home directories are often considered private and personal. Normally, these files can be read by their owner or by the root user (for example during backups). Other sanctioned file access includes files shared via a common Unix group. Any other access trial can be considered a break-in attempt. For example, a manager might want to know if a subordinate tried to cd to the manager's ~/private directory, or an instructor might wish to be informed when anyone tries to read files containing solutions to homeworks or exams.
The one place in a file system where files are initially searched is the vnode lookup routine. In order to detect access problems, we first have to perform the lookup on the lower file system, and then check the resulting status. If the status was one of the error codes ``permission denied'' or ``file not found'', we know that someone was trying to read a file they do not have access to, or they were trying to guess the names of files. If we detect one of these two error codes, we also check if the process accessing it belongs to the super-user or the file's owner (by checking user credentials). If it was a root user or the owner, we do nothing. Otherwise, we print a warning using the in-kernel syslog(3) facility. The warning contains the file or directory name to which access was denied, and the user ID of the process that tried to access it.
We made one additional design change and allowed for selective directory snooping. That is, we allow for this simple intrusion detection algorithm to be turned on only for directories that their owner wants to. We decided to use a seldom used mode bit for directories--the setuid bit--to flag a directory for snooping. This bit can be easily turned on or off by the chmod(3) utility.
The implementation of Snoopfs was very simple. In less than one hour we were able to implement it on all three operating systems we have Wrapfs ported to. The sum total of code added to Wrapfs was less than 10 lines of C code. Without Wrapfs, writing Snoopfs would have required several thousand lines of code
Snoopfs makes the point that even good technology can be abused. A malicious user with source access to Snoopfs and a root ID can modify Snoopfs to silently pry into other users' files.
Snoopfs can serve as a prototype for a more elaborate intrusion detection file system. Such a file system can prohibit or limit the creation or execution of setuid and setgid programs; it can deny an attacker from unlinking opened files (so as to continue using them while they are hidden from others). It can also disallow overwriting certain executables that rarely change (such as /bin/login) to prevent an attacker from replacing them with trojan versions.
When users export a file system for remote (NFS) mounting, the whole file system is either given write permission or not. Unix directory modes can be used to deny write access to selected directories, but a remote client with compromised root access can easily change these modes by becoming the owner of the directory. Rofs can be used to deny modification of any files in selected directories.
Rofs is first mounted on a file system to be exported, and then Rofs's mount point is exported for NFS mounts. In that way Rofs controls file access on the NFS server side where it is safer. If anyone tries to change the mode of read-only directory, Rofs denies that request and returns ``permission denied.'' Attempts to modify such directories or files within are refused with an error code ``read-only file system.'' The only way write access can be given to such read-only directories is from the host that exported the file system, and going through the disk-based file system directly, not through Rofs. (Only the Rofs mount point is exported, and it is imperative that the actual lower-level file system is not.)
The implementation of Rofs was easy and took less than 20 lines of C code. For each operation that wants to modify state (such as unlink or write) we first check the modes on the parent vnode. If the modes do not allow writing, we return an error code instead of performing the given vnode operation.
Attackers will often attempt to remove or truncate files that may reveal their tracks, such as logs and tools used to attack. Wofs is a file system that allows only appending to files and the creation of new files. That way attackers cannot remove existing files, nor can they modify existing data within. However, system log utilities such as syslog(3) can still append new information to files. No user process is allowed to read back existing files through Wofs. That way attackers could not find out what information might already be known about them.
The implementation of Wofs also took less than 10 lines of C code. The decode_data routine always returns an error code such as ``permission denied''. The encode_data routine allows the write to proceed only if the write offset requested was past the end of the file.
Allowing the creation of new files and disallowing their deletion has an added benefit which can be used to deny the installation of trojan programs. A simple enhancement to Wofs can achieve the following: if someone tried to overwrite a root owned file (such as /usr/ucb/telnetd), Wofs could implicitly rename the new file to, say /usr/ucb/.telnetd.trj. Since this new file now exists, attackers who discovered this would be unable to remove the file. A more clever addition to Wofs will also use decode_filename to skip listing files ending with .trj. This way such files would not be visible to casual listing through Wofs.
Since Wofs (and Unrmfs, Section 4.6) perform multiple atomic operations, such as renaming files, some guarantees are needed to ensure that multi-layer file system operations work reliably. Wrapfs behaves like a virtual file system (VFS) to the lower file system on which Wrapfs mounts on. Wrapfs takes care of locking file system objects such as vnodes, as mandated by the VFS which called Wrapfs. As such, Wrapfs may be invoked with locked objects (e.g., files, pages, etc.) and will have to lock the underlying, corresponding objects before calling the lower level file system. This implicit ordering in acquiring locks avoids deadlocking, but requires that the lower level file system not be directly accessible: the stackable file system must be overlay mounted.
Aclfs is used to provide very simple additional access controls. Specifically, it allows for an additional user and Unix group to share access to a directory as if they were the owner and group of the directory. When looking up a file in a directory, Aclfs first performs the normal access checks (in the vnode lookup routine). If access to the file was denied but the file exists, Aclfs looks for an additional file named .acl in that directory. It then repeats the lookup operation on the originally looked-up file, but using the ownership and group credentials of the .acl file. The .acl file itself is not modifiable via Aclfs, but only through the lower level file system using normal Unix file creation and access controls. Aclfs assumes that only authorized users would create the .acl files before Aclfs is overlay-mounted. Aclfs's implementation used no more than 10 lines of C code over Wrapfs.
There are several possible extensions to this trivial implementation of Aclfs. Instead of using the modes of the .acl file, it can contain an arbitrarily long list of user and group IDs to allow access to. The .acl file may also include sets of permissions to deny access from, perhaps using negative integers to distinguish them from access permissions. The granularity of Aclfs can be made on a per-file basis; for each file F, access permissions are read from the file .F.acl, if one exists. Also, ACLs can be used to completely replace regular Unix access control. Finally, new ioctls can be added to ease ACL administration.
Certain files in a Unix system are very important and hardly change, such as the kernel image or system daemons like /usr/ucb/rshd. Attackers often try to replace those with trojan versions. Moreover, a careless user or administrator may inadvertently overwrite those by attempting to install pre-packaged software or one built from source. FreeBSD's FFS is able to set an immutable flag on some files so they cannot be overwritten by even the root user, until the immutable flag is turned off. Imfs provides such functionality, but also adds simple key authentication.
For every read-only file F, for which the key file .F.imkey exists, Imfs will not allow modification to that file unless the key was given. A user-level tool prompts the user for a passphrase, and passes an MD5 hash of it to the kernel via an ioctl on that file. If the key matches the key stored in the .imkey file, the process is allowed to modify the file.
Most of the implementation work for Imfs concentrated in the vnode ioctl routine. One ioctl is used to set a key initially, while another is used to get a key from a user and compare it to an existing key. Of the 60 lines of C code for Imfs, most were used for general key management.
This initial design does not allow changing or removing keys through Imfs. They have to be removed by removing the .imkey file from the lower-level file system. This requires an unmounting of Imfs, as it is implemented as an overlay-only file system (see Section 2.2.1); casual access to the key files by root users is not possible. Clearly, better key management schemes can be used for Imfs, but such discussion is beyond the scope of this paper.
Unrmfs can alleviate some of the problems associated with accidental or intentional removal of files, by allowing the reversion of such deletion. Its implementation is simple. When the vnode function unlink is called to remove a file F, it instead translates it into a call to rename the file to F.unrm. An ioctl added to Unrmfs allows the user to un-remove a file by renaming it back. This way a user can avoid accidental removal of files. The code for this file system took 20 lines.
Using a special flag, Unrmfs can be mounted in a more secure mode, intended to help track down attackers. In this mode, Unrmfs will neither list nor allow the removal of the .unrm files. If the original file is re-created and removed again, the older .unrm file will not be overwritten, so as to keep the old backup file around.
One obvious extension to Unrmfs is to allow versioning. Each time a file F is removed, a new F.unrm.N is created, where N is a monotonically increasing integer. Another enhancement may put a limit on the maximum number of versions, while keeping only recent versions. This limits the possibility of filling up a file system with repeated file removals, which could be a form of denial-of-service attack.
Consfs allows root owned binaries to be modified or executed only by a process whose controlling terminal is /dev/console. It is intended so that attackers who often come from the outside could not replace vital binaries with trojans, nor could they execute them. This way only administrators with physical access to a machine are able to perform such work.
We wrote Consfs also to illustrate an aspect of Wrapfs that is both a source of functional flexibility and the cause of portability problems. Consfs has to determine--inside the kernel--if the current process has a controlling terminal and that one is the console. Different operating systems use different methods to find this information; different data structures and fields have to be followed. This makes it harder to modify one set of Wrapfs's API calls (Section 2.1.1) in a way that would compile and work on different operating systems. Not surprisingly, Consfs's total code size was about 100 lines of C; about one third of it was unportable code.
Before we embarked on a cryptographically strong file system, we wrote one that uses a simple encryption algorithm that works on single bytes--rot13. Rot13fs encrypts only file data, not names. We developed Rot13fs at the same time we worked on Wrapfs. Rot13fs was useful in finding out which features of a stackable file system were generally useful for the Wrapfs template, and which were not.
The encryption file system Cryptfs is the most involved file system we have designed and implemented based on Wrapfs. This section summarizes the design and implementation of Cryptfs. More detailed information is available in a separate report.
For an encryption algorithm we picked Blowfish, a 64 bit block cipher that was designed to be fast, compact, and simple. Blowfish is suitable in applications where the keys do not change often such as in automatic file decryptors. It can use variable length keys as long as 448 bits. We kept the default 128 bit long keys. In relation to the algorithm, we picked the Cipher Block Chaining (CBC) encryption mode because it allows us to encrypt byte sequences of any length, which is suitable for encrypting file names. However, we decided to use CBC only within each block encrypted by Cryptfs. This way ciphertext blocks (of 4-8KB) would not depend on previous ones, allowing us to decrypt each block independently. Moreover, since Wrapfs naturally lets us manipulate file data in aligned units of even page size multiples, encrypting them promised to be simple.
Next, we decided to encrypt file names as well, to provide stronger security. We realized that we should not encrypt the ``.'' and ``..'' directory names so that the lower level Unix file system remains intact. Furthermore, since encrypting file names may result in characters that are illegal in file names (such as nulls and forward slashes ``/''), we settled on further uuencoding the resulting encrypted strings. This eliminated the unwanted characters and guaranteed that all file names consisted of printable characters that are valid in file names.
We decided that Cryptfs mounts will be regular. This facilitates faster and more secure backups of (ciphertext) user files directly from the lower level file system.
The next important design issue for Cryptfs was key management. We decided that only the root user would be allowed to mount an instance of Cryptfs, but could not automatically encrypt or decrypt files. To thwart an attacker who gains access to a user's account or to root privileges, Cryptfs maintains keys in an in-memory data structure that associates keys not with UIDs alone but with the combination of UID and session ID. To succeed in acquiring or changing a user's key, attackers would not only have to break into an account, but also arrange for their processes to have the same session ID as the process that originally received the user's passphrase. This is a more difficult attack, requiring session and terminal hijacking or kernel-memory manipulations.
Using session IDs to further restrict key access does not burden users during authentication. Login shells and daemons use setsid(2) to set their session ID and detach from the controlling terminal. Forked processes inherit the session ID from their parent. Therefore, users would normally have to authorize themselves only once in a shell. From this shell they could run most other programs that would work transparently and safely with the same encryption key. Note that Cryptfs can be modified easily to use only one key per mount instance. This provides a separation of mechanism, since a separate file system is instantiated for each user. Wrapfs templates, in general, do not impose a restriction on how many times they can be instantiated.
We designed a user tool that prompts users for passphrases that are at least 16 characters long. The tool hashes passphrases using MD5 and passes them to Cryptfs using a special ioctl(2). The tool can also instruct Cryptfs to delete or reset keys.
Our design decouples key possession from file ownership. For example, a group of users who wish to edit a single file would normally do so by having the file group-owned by one Unix group and add each user to that group. However, Unix systems often limit the number of groups a user can be a member of to 8 or 16. Worse, there are often many subsets of users who are all members of one group and wish to share certain files, but are unable to guarantee the security of their shared files because there are other users who are members of the same group; e.g., many sites put all of their staff members in a group called ``staff'', students in the ``student'' group, guests in another, etc. With our design, users can further restrict access to shared files only to those users who were given the key.
One disadvantage of this design is reduced scalability with respect to the number of files being encrypted and shared. Users who have many files encrypted with different keys will have to switch their effective key before attempting to access files that were encrypted with a different one. We did not perceive this to be a serious problem for two reasons. First, the amount of Unix file sharing of restricted files has always been limited. Most shared files are generally world readable and thus do not require encryption. Secondly, with the proliferation of windowing systems, users can associate different keys with different windows.
In the current design, Cryptfs uses one Initialization Vector (IV) per mount, used to jump start a sequence of encryption. If not specified, a predefined IV is used. The superuser mounting Cryptfs can choose a different one, but that will make the first eight bytes of all previously encrypted files undecipherable with the new IV. Files that use the same IV and key produce identical ciphertext blocks that are subject to analysis of identical blocks, because using fixed IVs with CFB-mode encryption leaks plaintext. By default, CFS uses no IVs, and we felt that using a fixed one produces a reasonably strong security for this extensible prototype file system.
With the Wrapfs templates, additional security features could be tested and added with relative ease; we expect future developers to implement stronger security mechanisms into Cryptfs. One possible extension to Cryptfs would be to use different IVs for different files, based on the file's inode number and perhaps in combination with the page number (so that each page within a file can be encrypted with a different IV). Making the IV depend on inode numbers or page numbers would not completely secure data pages, as detailed in SFS. Some methods to improve the security of block ciphers are detailed by Luby and Rackoff. Faster construction of block ciphers is outlined by Lucks and use dedicated hash functions. Other more obvious extensions to Cryptfs include the usage of different encryption algorithms, perhaps different ones per user or file.
Cryptfs adds more than 1500 lines of C code to Wrapfs. 70% of it is the Blowfish implementation. Most of the rest is for key management and special file name handling.