6.6 Device Files

Computer systems usually have peripheral devices attached to them. These devices may be involved with I/O (terminals, printers, modems), they may involve mass storage (disks, tapes), and they may have other specialized functions. The Unix paradigm for devices is to treat each one as a file, some with special characteristics.

Unix devices are represented as inodes, identical to files. The inodes represent either a character device or a block device (described in the sidebar). Each device is also designated by a major device number, indicating the type of device, and a minor device number, indicating which one of many similar devices the inode represents. For instance, the partitions of a physical disk will all have the same major device number, but different minor device numbers. For a serial card, the minor device number may represent which port number is in use. When a program reads from or writes to a device file, the kernel turns the request into an I/O operation with the appropriate device, using the major/minor device numbers as parameters to indicate which device to access.

Block Versus Character Devices

Most devices in Unix are referenced as character devices. These are also known as raw devices because that is what you get?raw access to the device. You must make your read and write calls to the device file in the natural transfer units of the device. Thus, you probably read and write single characters at a time to a terminal device, but you need to read and write sectors to a disk device. Attempts to read fewer (or more) bytes than the natural block size results in an error, because the raw device doesn't work that way.

When accessing the filesystem, we often want to read or write only the next few bytes of a file at a time. If we used the raw device, it would mean that to write a few bytes to a file, we would need to read in the whole sector off disk containing those bytes, modify the ones we want to write, and then write the whole sector back out. Now consider every user doing that as they update each file. That would be a lot of disk traffic!

The solution is to make efficient use of caching. Block devices are cached versions of character devices. When we refer to a few bytes of the block device, the kernel reads the corresponding sector into a buffer in memory, and then copies the characters out of the buffer that we wanted. The next time we reference the same sector, to read from or write to, the access goes to the cached version in memory. If we have enough memory, most of the files we will access can all be kept in buffers, resulting in much better performance.

There is a drawback to block devices, however. If the system crashes before modified buffers are written back out to disk, the changes our programs made won't be there when the system reboots. Thus, we need to periodically flush the modified buffers out to disk. That is effectively what the sync( ) system call does: schedule the buffers to be flushed to disk. Most systems have a sync or fsflush daemon that issues a sync( ) call every 30 or 60 seconds to make sure the disk is mostly up to date. If the system goes down between sync( ) calls, we need to run a program such as fsck or checkfsys to make certain that no directories with buffers in memory were left in an inconsistent state.

Unix usually has some special device files that don't correspond to physical devices. The /dev/null device simply discards anything written to it, and nothing can ever be read from it?a process that attempts to do so gets an immediate end-of-file condition. Writing to the /dev/console device results in output being printed on the system console terminal. And reading or writing to the /dev/kmem device accesses the kernel's memory. Devices such as these are often referred to as pseudo-devices.

Device files are one of the reasons Unix is so flexible?they allow programmers to write their programs in a general way without having to know the actual type of device being used. Unfortunately, they can also present a major security hazard when an attacker is able to access them in an unauthorized way.

For instance, if attackers can read or write to the /dev/kmem device, they may be able to alter their priority, UID, or other attributes of their process. They could also scribble garbage data over important data structures and crash the system. Similarly, access to disk devices, tape devices, network devices, and terminals being used by others can lead to problems. Access to your screen buffer might allow an attacker to read what is displayed on your screen. Access to your audio devices might allow an attacker to eavesdrop on your office without your knowing about it.

In standard configurations of Unix, all the standard device files are located in the directory /dev. There is usually a script (e.g., MAKEDEV) in that directory that can be run to create the appropriate device files and set the correct permissions. A few devices, such as /dev/null, /dev/tty, and /dev/console, should always be world-writable, but most of the rest should be unreadable and unwritable by regular users. Note that on some System V-derived systems, many of the files in /dev are symbolic links to files in the /devices directory, which are the files whose permissions you need to check.

Check the permissions on these files when you install the system, and periodically thereafter. If any permission is changed, or if any device is accessible to all users, you should investigate. This research should be included as part of your checklists.

6.6.1 Unauthorized Device Files

Although device files are normally located in the /dev directory, they can, in fact, be anywhere on your system. A not uncommon method used by system crackers is to access the system as the superuser and then create a writable device file in a hidden directory, such as the /dev/kmem device hidden in /usr/lib and named to resemble one of the libraries. Later, if they wish to become superuser again, they know the locations in /dev/kmem that they can alter with a symbolic debugger or custom program to allow them that access. For instance, by changing the code for a certain routine to always return true, they can execute su to become root without needing a password. Then, they set the routine back to normal.

You should periodically scan your disks for unauthorized device files. The ncheck command, mentioned earlier, will print the names of all device files when run with the -s option. Alternatively, you can execute the following:

# find / \( -type c -o -type b \) -exec ls -l {} \;

If you have NFS-mounted directories, use this version of the script:

# find / \( -local -o -prune \) \( -type c -o -type b \) -exec ls -l {} \;

Note that some versions of NFS allow users on client machines running as root to create device files on exported volumes.[18] This is a major problem. Be very careful when exporting writable directories using NFS (see Chapter 15 for more information).

[18] Of course, these modifications cannot be made if the filesystem is exported read-only.

Not Everything Is a File or a Device!

The two commands:

find / \! -type f -a \! -type d -exec ls -l {} \;


find / \( -type c -o -type b \) -exec ls -l {} \;

are not equivalent!

The first command prints all of the entries in the filesystem that are not files or directories. The second prints all of the entries in the filesystem that are either character or block devices.

Why aren't these commands the same? Because there are other things that can be in a filesystem that are neither files nor directories. These include:

  • Symbolic links

  • Sockets

  • Named pipes (FIFOs)

    Part VI: Appendixes