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 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.