Check out the new USENIX Web site. next up previous
Next: Measurement Bypass-Protection Up: Implementation Previous: Inserting Measurement Points

Taking Measurements

This section describes the implementation of the kernel level measure call used at the measurement points to initiate the measurement of a file or a memory area (in case of kernel modules). The measure call takes one argument, namely, a pointer to the file structure containing the file to be measured. From the file structure one can look up the corresponding inode and data blocks, and take a SHA1 over the data blocks.

There are three places from which a measure call is issued: (1) the implementation of the write/store routine to the the pseudo file system /sys/security/measure used by user level applications, (2) the file_mmap security LSM hook measuring files that are being memory-mapped as executable code, and (3) the load_module routine measuring kernel module code in memory before it is relocated. The file_mmap hook receives the file pointer as argument, and the write routine of the sysfs entry receives the file descriptor, from which the file pointer is retrieved using the fget routine. We ignore file_mmap calls where the $PROT\_EXEC$ bit is not set in the properties parameter, as those files are not mapped executable.

The consistency between file-measurements and what is actually loaded depends on: (1) accurate identification of the inode loaded and (2) detection of any subsequent writes to the file described by the inode. Both cases are handled by the kernel in the case of memory-mapped executables. Protective locks that the kernel holds at measurement time ensure that the file cannot be written to by others as long as it is mapped executable. This lock is held by the mapping function at the time of measurement. Modules are measured when they are already in kernel memory, thus they are not susceptible to such inconsistencies. For files measured from user space, we assume that the measuring application keeps the file descriptor -used to initiate the measurement- open until it is done reading the contents or to issue a new measurement call when the file is re-opened. This ensures that the file measured is the file actually read. Second, there could be a race between the measure and read user level calls and another write call that modifies the data. We call this case a Time-of-Measure-Time-of-Use (ToM-ToU) race condition and describe in Section 5.3 how we handle this case. However, remote NFS files cannot be measured dependably unless the file's complete contents are cached and protected on the local system. We do not implement such caching at present.

A naive measurement implementation would be to take a fingerprint for every measure call. This approach would, however, incur significant performance overhead (see Section 6.2) for executable files and libraries that are loaded quite often.

Instead, we use caching to reduce performance overhead. The idea is to keep a cache of measurements that have already been performed, and take a new measurement only if the file has not been seen before (cache-miss) or the file might have changed since last measurement. For the latter case, we only record a new file measurement if the file has actually changed. Recording identical measurements each time an application runs would have severe impact on the management (storage, retrieval, validation) of the list. Kernel modules are always measured in memory at load-time but their measurement is added only if it is not yet in the measurement list.

We store all measurements in a singly-linked, ordered list. The order of measurements is essential to detect any modification to the measurement list. If the measurements are not checked in order, then the aggregate hash will not match the TPM aggregate that results from the TPM_extend operations. Additionally, we gather meta information related to the measured file -such as the file name, user ID, group ID or security labels of the loading entity, or the file system type-, which might be useful for evaluating the impact of loading this file or matching it with local security policies. At this time, our implementation gathers this additional data informally in the measurement list, but does not include it in the measurement.

For efficiency reasons, we overlay the linked list with two hash tables, one keyed with the inode number and device number of the measured file, the second keyed with the resulting fingerprint (SHA1 value) of the measured file. Thus, each measurement entry can be reached by traversing the measurement list, by its inode (for file measurements only), or by its fingerprint. The measure call uses the inode corresponding to the file descriptor of the target file to quickly look up the file in the hash table and see if it has been measured before.

Each measurement entry contains a dirty flag bit, indicating whether the file is CLEAN (not modified), or DIRTY (possibly modified). We describe the semantics of measurement below.

Measuring new files: If the file is not found in the inode-keyed hash table, then we measure the file by computing a SHA1 hash over its complete content. At this point, we use the computed fingerprint to check whether it is present in the hash table keyed by the SHA1 hash value of existing measurements. If the measured fingerprint is not found, then we create a new measurement entry, and add it to the list and adjust the hash table structures. We finally extend the relevant Platform Configuration Register in the protected TPM hardware by the SHA1 hash before returning from the call and allowing the loading of the executable content. If the fingerprint was already measured before, then we return from the system call without extending the TPM or the measurement list. This can happen if executable files are copied and thus yield the same fingerprint. In this case, we assume for our purpose that both executables are equivalent.

Remeasuring files: If the file is found in the inode-keyed hash table, then it was measured before. If the dirty flag of the found measurement entry is CLEAN (clean-hit), then nothing needs to be done, and the system call returns. If the dirty flag bit is DIRTY (dirty-hit), then we compute the SHA1 value of the file. If the measured fingerprint is identical to the one stored in the measurement list, then we re-set the dirty flag. We do not extend the PCR or record this measurement as it is known already.

If the measured fingerprint differs from the one stored in the found measurement entry for the inode, then we look up the new fingerprint in the hash table using the SHA1 value as the key. If the SHA1 value exists, then the same file contents were measured before (copy of the current file). We return without recording the measurement, as above. If the SHA1 value does not exist in the hash table, then the current file has changed. A new measurement entry is created and added to the table, and the PCR is extended before the measure call returns.

Dirty flagging: We set the dirty flag bit to DIRTY whenever the target file (a) was opened with write, create, truncate, or append permission, (b) was located on a file system we can't control access to (e.g., NFS), or (c) belongs to a file system which was unmounted. This seems a bit conservative, since an open for write (or unmounting a file) does not necessarily result in modifications to the file. The SHA1-keyed hash table enables us to clear the dirty flag if a file did not change after an open with write permission. If we control access to the file, then we clear the dirty flag in such cases. Experiments show that on a non-development system using local file systems, the percentage of dirty-hits on the cache is far less than 1%.

Measuring kernel modules: We issue a measure calls whenever a kernel module is being prepared for integration into the kernel. We calculate the SHA1 value of the memory area where the not-yet relocated kernel module resides in the load_module kernel function and thus we yield a single representative measurement for each kernel module independently of its final memory location. Then, we check whether this SHA1 fingerprint is already in the measurement list using the SHA1-keyed hash table over all existing measurements. If it is known, then we return form the measure call. If not, then we extract the module name from its ELF headers, which are located at the beginning of the memory area, add the measurement as a new measurement to the measurement list, and finally extend the TPM register to reflect the updated measurement list. Kernel modules must always be measured because we do not have any information easily available to indicate a dirty flag state. However, there are usually only a few kernel modules loaded. Alternatively, the user level applications insmod and modprobe can measure the files when loading kernel modules into memory. In this case, their measurement follows the file measurement procedures described before.

next up previous
Next: Measurement Bypass-Protection Up: Implementation Previous: Inserting Measurement Points
sailer 2004-05-18