Secure File Access
In the previous two chapters we talked a lot about what can go wrong when using the file system. Primarily, we're worried about being lazy when it comes to performing access control, and we're worried about race conditions to which we may be susceptible. It would be nice to be able to manipulate files in a way that is guaranteed to be secure. In the previous section we discussed a way of avoiding TOCTOU problems when we can deal directly with file descriptors, and perform checks on open files, instead of on symbolic filenames. Unfortunately, this technique isn't always something you can use, because many common calls do not have alternatives that operate on a file descriptor, including link(), mkdir(), mknod(), rmdir(), symlink(), unmount(), unlink(), and utime().
The best solution is to keep files on which we would like to operate in their own directory, where the directory is only accessible by the UID of the program performing file operations. In this way, even when using symbolic names, attackers are not able to exploit a race condition, unless they already have the proper UID (which would make exploiting a race condition pointless).
To accomplish this goal, we need to make sure that an attacker cannot modify any of the parent directories. The way to do this is to create the directory, chdir() into it, then walk up the directory tree until we get to the root, checking to make sure that the entry is not a link, and making sure that only root or the user in question can modify the directory. Therefore, we need to check the owning UID and owning GID every step of the way.
The following code takes a directory name and determines whether the specified directory is "safe" in terms of using calls that would otherwise be susceptible to race conditions. In standard C style, it returns 0 on success (in other words, if the directory can be considered safe). The user passes in a path, as well as the UID that should be considered "trusted" (usually geteuid() if not running as root). The root UID is implicitly trusted. If any parent directory has bad permissions, giving others more access to the directory than necessary (in other words, if the write or execute permissions are granted to anyone other than the owner), then safe_dir fails:
include <sys/types.h> include <sys/stat.h> include <fcntl.h> include <unistd.h> static char safe_dir(char *dir, uid_t owner_uid) { char newdir[PATH_MAX+1]; int cur = open(".", O_RDONLY); struct stat linf, sinf; int fd; if(cur == -1) { return 1; } if(lstat(dir, &linf) == 1) { close(cur); return 2; } do { chdir(dir); if((fd = open(".", O_RDONLY)) == 1) { fchdir(cur); close(cur); return 3; } if(fstat(fd, &sinf) == 1) { fchdir(cur); close(cur); close(fd); return 4; } close(fd); if(linf.st_mode != sinf.st_mode || linf.st_ino != sinf.st_ino || linf.st_dev != sinf.st_dev) { fchdir(cur); close(cur); return 5; } if((sinf.st_mode & (S_IWOTH|S_IWGRP)) || (sinf.st_uid && (sinf.st_uid != owner_uid))) } fchdir(cur); close(cur); return 6; } dir = ".."; if(lstat(dir, &linf) == 1) { fchdir(cur); close(cur); } return 7; if(!getcwd(new_dir, PATH_MAX+1)) { fchdir(cur); close(cur); return 8; } } while(strcmp(new_dir, "/")); fchdir(cur); close(cur); return 0; }
The previous code is a bit more draconian than it strictly needs to be, because we could allow for appropriate group permissions on the directory if the owning GID is the only nonroot user with access to that GID. This technique is simpler and less error prone. In particular, it protects against new members being added to a group by a system administrator who doesn't fully understand the implications.
Once we've created this directory, and assured ourselves it cannot fall under the control of an attacker, we will want to populate it with files. To create a new file in this directory, we should chdir() into the directory, then double check to make sure the file and the directory are valid. When that's done, we open the file, preferably using a locking technique appropriate to the environment. Opening an existing file should be done the same way, except that you should check appropriate attributes of the preexisting file for safety's sake.
What about securely deleting files? If you're not using the secure directory approach, you often cannot delete a file securely. The reason is that the only way to tell the operating system to remove a file, the unlink() call, takes a filename, not a file descriptor or a file pointer. Therefore, it is highly susceptible to race conditions. However, if you're using the secure directory approach, deleting a file with unlink() is safe because it is impossible for an attacker to create a symbolic link in the secure directory. Again, be sure to chdir() into the secure directory before performing this operation.
Avoiding a race condition is only one aspect of secure file deletion. What if the contents of the files we are deleting are important enough that we wish to protect them after they are deleted? Usually, "deleting" a file means removing a file system entry that points to a file. The file still exists somewhere, at least until it gets overwritten. Unfortunately, the file also exists even after it gets overwritten. Disk technology is such that even files that have been overwritten can be recovered, given the right equipment and know-how. Some researchers claim that if you want to delete a file securely you should first overwrite it seven times. The first time, overwrite it with all ones, the second time with all zeros. Then, overwrite it with an alternating pattern of ones and zeros. Finally, overwrite the file four times with random data from a cryptographically secure source (see Chapter 10).
Unfortunately, this technique probably isn't good enough. It is widely believed that the US government has disk recovery technology that can stop this scheme. If you are really paranoid, then we recommend you implement Peter Gutmann's 35-pass scheme as a bare minimum [Gutmann, 1996]. An implementation of this technique can be found on this book's companion Web site.
Of course, anyone who gives you a maximum number of times to write over data is misleading you. No one knows how many times you should do it. If you want to take no chances at all, then you need to ensure that the bits of interest are never written to disk. How do you store files on the disk? Do so with encryption, of course. Decrypt them directly into memory. However, you also need to ensure that the memory in which the files are placed is never swapped to disk. You can use the mlock() call to do this (discussed in Chapter 13), or mount a ramdisk to your file system. Additionally, note that it becomes important to protect the encryption key, which is quite difficult. It should never reach a disk itself. When not in use, it should exist only in the user's head.