Windows CryptoAPI의 주요 스푸핑 취약점 악용
편집 및 추가 기여: 트리샤 하워드(Tricia Howard)
핵심 요약
Akamai Security Intelligence Group은 최근 NSA(National Security Agency)와 NCSC(National Cyber Security Center)에서 Microsoft에 공개한 Windows CryptoAPI의 심각한 취약점을 분석했습니다.
취약점 CVE-2022-34689의 CVSS 점수는 7.5입니다. 이 취약점은 2022년 8월에 패치되었지만 2022년 10월 패치 화요일에 공개적으로 발표되었습니다.
Microsoft에 따르면 공격자는 이 취약점을 이용해 정상적인 개체로 가장할 수 있습니다.
이 버그의 근본 원인은 MD5 기반의 인증서 캐시 인덱스 키가 충돌하지 않는 것으로 가정하는 데 있습니다. MD5의 충돌 내성은 2009년부터 깨져있는 것으로 알려졌습니다.
공격 흐름은 두 가지 단계로 구성됩니다. 첫 번째 단계에서는 정상적인 인증서를 가져가 수정하고 수정된 버전을 피해자에게 제공해야 합니다. 두 번째 단계에서는 MD5가 수정된 정상적인 인증서와 충돌하는 새 인증서를 만들고 새 인증서를 사용해 원본 인증서 주체의 ID를 스푸핑합니다.
Akamai는 온라인에서 이 스푸핑 공격에 취약한 방식으로 CryptoAPI를 사용하는 애플리케이션을 검색했습니다. 지금까지 Chrome(v48 이하) 및 Chromium 기반 애플리케이션의 이전 버전이 악용될 수 있다는 사실을 발견했습니다. Akamai는 온라인에 더 취약한 표적이 있다고 생각하고 계속 연구 중입니다.
데이터 센터에 있는 가시 디바이스 중 패치가 적용된 디바이스는 1% 미만이며 나머지 디바이스는 이 취약점 악용으로부터 보호되지 않는 것으로 나타났습니다.
이 블로그 게시물에서는 잠재적인 공격 흐름과 결과를 자세히 설명하는 것은 물론 완벽한 공격을 입증하는 개념 증명(PoC)을 소개합니다. 또한 CryptoAPI 라이브러리의 취약한 버전을 탐지하기 위한 OSQuery도 제공합니다.
배경
3개월 전 2022년 10월 패치 화요일 분석에서 Windows CryptoAPI의 심각한 스푸핑 취약점(CVE-2022-34689)에 대한 기본 설명을 공유했습니다. Microsoft에 따르면 공격자는 “이 취약점을 악용해 ID를 스푸핑하고 표적 인증서로 인증 또는 코드 서명 등의 작업”을 수행할 수 있습니다.
CryptoAPI는 암호화와 관련된 모든 것을 처리하기 위한 Windows의 사실상의 API입니다. 특히 인증서를 읽고 구문 분석하는 것부터 확인된 CA(인증 기관)에 대해 유효성을 검사하는 것까지 인증서를 처리합니다. 또한 브라우저는 TLS 인증서 유효성 검사에 CryptoAPI를 사용하며 이 프로세스에서 나타난 잠금 아이콘은 모두가 확인해야 합니다.
그러나 인증서 확인은 브라우저에 고유하지 않으며 PowerShell 웹 인증, CURL, WGET, FTP 관리자, EDR, 기타 다양한 애플리케이션 등 기타 TLS 클라이언트에 의해 사용됩니다. 또한 실행 파일 및 라이브러리에서 코드 서명 인증서를 확인하고 드라이버를 로딩할 때 드라이버 서명 인증서를 확인합니다. 상상할 수 있듯이, 인증서 확인 프로세스의 취약점은 공격자에게 매우 유리합니다. 공격자는 ID를 숨기는 동시에 중요한 보안 보호를 우회할 수 있기 때문입니다.
NSA가 CryptoAPI의 취약점을 공개한 것은 이번이 처음이 아닙니다. 2020년에 CurveBall(CVE-2020-0601)을 발견하고 공개했습니다. 취약점 CurveBall 또는 CVE-2022-34689로 인해 ID 스푸핑이 발생하며 CurveBall은 많은 애플리케이션에 영향을 주지만 CVE-2022-34689에는 더 많은 전제 조건이 있기 때문에 취약한 표적의 범위가 더 제한됩니다.
취약점 세부 정보
취약점을 분석하기 위해 먼저 패치된 코드를 찾아보았습니다. Akamai는 널리 사용되는 바이너리 비교 툴인 BinDiff를 사용해 CryptoAPI의 다양한 코드 변경 사항을 관찰했습니다. crypt32.dll에서는 하나의 함수 CreateChainContextFromPathGraph만 변경됐습니다. 이 함수는 두 인증서를 비교합니다. 하나는 입력으로 수신되고 다른 하나는 수신 애플리케이션의 인증서 캐시에 있습니다(캐시는 후반부에 자세히 설명합니다).
변경 사항을 점검한 결과 memcmp 검사가 두 위치에서 함수에 추가되었습니다(그림 1).
패치 전에 이 함수는 수신된 인증서가 MD5 지문을 기준으로 캐시에 이미 있는지 (따라서 인증되었는지) 여부를 확인했습니다. 패치 후 memcmp를 추가하려면 두 인증서의 실제 내용이 완전히 일치해야 합니다.
여기서 Akamai는 공격자가 MD5가 이미 피해자의 인증서 캐시에 있는 것과 충돌하는 악성 인증서를 제공하면 취약한 검사를 우회해 자신의 악성 인증서가 신뢰를 받을 수 있다는 이론을 만들었습니다(그림 2).
CryptoAPI의 인증서 캐시
CryptoAPI는 수신된 최종 인증서에 캐시를 사용해 성능과 효율성을 향상시킬 수 있습니다. 이 메커니즘은 기본적으로 비활성화되어 있습니다. 이 기능을 사용하려면 애플리케이션 개발자는 특정 매개변수를 CertGetCertificateChain으로 전달하고 이 Windows API 함수는 취약한 코드로 이어지게 됩니다(그림 3).
CertGetCertificateChain은 다음과 같은 몇 가지 흥미로운 매개변수를 받습니다.
hChainEngine - 인증서 유효성 검사 방법을 제어하는 데 사용되는 설정 가능한 오브젝트
pCertContext - WinAPI 함수 CertCreateCertificateContext에 의해 입력 인증서를 사용해 구축되는 데이터 구조인 입력 인증서의 컨텍스트
dwFlags - 추가 설정을 지정하는 플래그
ppChainContext - 신뢰 상태를 포함하는 출력 오브젝트, 즉 체인의 인증 결과
최종 인증서에 대한 캐싱 메커니즘을 활성화하려면 개발자는 dwFlags에서 CERT_CHAIN_CACHE_END_CERT 플래그를 설정하거나 사용자 지정 체인 엔진을 만들고 dwFlags 필드에서 CERT_CHAIN_CACHE_END_CERT 플래그를 설정해야 합니다.
캐시의 구현 및 사용 방법을 이해하기 위해 함수 FindIssuerObject를 살펴보겠습니다. 이 함수는 캐시에서 인증서를 가져옵니다. 일반적으로 이 함수는 다음과 같이 동작합니다.
MD5 엄지손가락 지문의 가장 중요도가 낮은 바이트 4개를 기준으로 캐시에서 입력 인증서의 버킷 인덱스를 계산합니다.
함수가 캐시에 있는 경우 캐싱된 인증서의 전체 MD5 지문을 입력 인증서와 비교합니다.
지문이 일치하면(캐시 적중) 입력 인증서가 신뢰되고 반환됩니다. 이제부터 애플리케이션은 캐싱된 인증서가 아닌 입력 인증서 특성(공개 키, 발급자 등)을 사용합니다.
지문이 일치하지 않으면(캐시 미스) 버킷의 다음 인증서로 가서 MD5 지문을 비교한 후 반복합니다.
Microsoft는 기본적으로 캐싱된 인증서의 유효성을 신뢰하며 캐시에 최종 인증서가 발견된 후에는 추가 유효성 검사를 수행하지 않습니다. 이 가정은 그 자체로 타당합니다. 하지만 코드는 MD5 지문이 일치하면 두 인증서가 동일하다고 추가 가정합니다. 이것은 악용될 수 있는 잘못된 가정이며 이로 인해 패치가 개발됐습니다.
가설을 뒷받침하기 위해 CertGetCertificateChain을 사용하는 작은 애플리케이션을 작성하고 crypt32.dll의 인증서 확인 흐름을 디버깅했습니다. WinDbg를 사용해 자체 (서명) 인증서의 MD5 지문이 캐시에 이미 있는 정상적인 인증서와 일치하는 시나리오를 시뮬레이션했습니다. 그림 4에 표시된 것처럼 조작된 인증서가 신뢰되었습니다.
한 가지 검사만 우회하면 Windows가 악성 인증서를 정상적인 인증서로 믿게 만들 수 있습니다.
취약점 악용 방법
주어진 MD5 값과 정확히 일치하는 MD5 지문을 사용해 인증서를 구성하는 것을 프리이미지 공격이라고 하며, 이는 현재까지도 계산적으로 불가능합니다. 그러나 두 개의 선택된 접두사가 있는 두 개의 인증서를 효율적으로 생성할 수 있으며, 이 경우 MD5 지문이 동일하게 되며 이러한 종류의 공격을 선택한 접두사 충돌이라고 합니다.
이 경로를 선택하면 대상 애플리케이션에 두 개의 인증서를 제공해야 합니다. 하나의 인증서가 올바르게 서명, 확인, 캐시됩니다(수정된 표적 인증서'라고 부를 것임). 선택된 접두사 충돌 공격을 촉진하는 방식으로 생성될 것입니다. 두 번째 인증서('악성 인증서'라고 부를 것임)는 스푸핑된 ID를 포함합니다. 첫 번째 인증서의 MD5 지문과 충돌할 것입니다(그림 5).
MD5 충돌을 통한 인증서 스푸핑
MD5 충돌은 약 14년 전, 즉 비욘세(Beyoncé)가 “Single Ladies”를 발표하고 오바마(Obama)가 처음 대통령으로 선출된 시기로 거슬러 올라갑니다. SSL 인증서를 스푸핑하는 데 처음으로 사용되었습니다. 첫 번째 공격과 현재 여기서 다루는 시나리오 사이에는 한 가지 큰 차이가 있습니다. 이전 시나리오에서는 MD5 서명을 공개했지만 현재 취약점에서는 MD5 엄지손가락 지문을 공격합니다. 그 차이를 알아보겠습니다.
우선 RFC 5280섹션 4.1에 따르면 인증서는 두 개의 섹션으로 구성된 ASN.1 시퀀스입니다(그림 6).
tbsCertificate (또는 '서명 예정' 인증서) - 모든 ID 관련 세부 정보(제목, 공개 키, 일련 번호, EKU 등)를 포함하는 부분입니다. 이 부분은 서명되어 있습니다.
signatureAlgorithm 및 signatureValue - 이 필드는 TBS의 서명을 구성합니다.
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
그림 6: 인증서를 정의하는 ASN.1 시퀀스
따라서 인증서 서명은 인증서 내에 포함된 구조체로서 인증서의 TBS 부분에만 서명합니다. 반면, 인증서 지문은 전체 인증서(서명 포함)의 해시입니다.
따라서 인증서를 무효화하지 않고 TBS 외부에 있는 인증서의 일부를 수정할 수 있다면 서명을 변경하지 않고 지문을 수정하게 됩니다. 파서가 서명을 올바르게 구문 분석하고 TBS가 변경되지 않은 경우 전체 인증서 구조가 변경되더라도 인증서는 여전히 유효하고 서명된 것으로 간주됩니다(그림 7).
MD5 선택한 접두사 충돌 - 간략한 개요
길이가 같은 임의의 문자열 A와 B가 있다고 가정하겠습니다. 그러면 다음과 같이 두 문자열 C와 D를 효율적으로 계산할 수 있습니다.
MD5(A || C) = MD5(B || D) |
여기서 || 는 문자열 순차를 나타냅니다.
또한 동일한 최종 MD5 결과일 뿐만 아니라 C 또는 D를 추가한 후 MD5 내부 상태이기도 합니다. 따라서 접미사 E를 사용합니다.
MD5(A || C || E) = MD5(B || D || E) |
(양쪽 끝에 동일한 접미사 E가 추가된 경우)
충돌 블록을 위한 공간 만들기
Akamai는 공격자로서 유효한 것처럼 보이지만 충돌 블록을 위한 공간이 있는 인증서를 생성해야 합니다(위의 설명에서 문자열 C와 D). 이렇게 하면 다음에 사용할 수 있는 악성 인증서(MD5 지문이 동일한 인증서)를 만들 수 있습니다.
이제 RFC 5280, 섹션 4.1.1.2에 따르면 signatureAlgorithm 의 구조는 다음과 같습니다.
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
RSA 알고리즘의 매개변수 필드( RFC 3279기준) “ASN.1 종류 NULL이어야 합니다.” 즉, RSA는 서명 매개변수를 사용하지 않고 대신 NULL을 값으로 사용합니다. CryptoAPI가 RSA 서명에 대해 이 필드를 무시할 수 있을까요?
(충돌 블록을 위한 준비로) 이 필드에 자리 표시자 바이트를 삽입하기 위해 ASN.1 종류를 NULL에서 비트 문자열로 변경하려고 했습니다. OpenSSL뿐만 아니라 CryptoAPI에 대한 테스트에서도 제대로 작동합니다. 즉 인증서가 여전히 유효한 것으로 간주됩니다. TBS를 수정하지 않았기 때문에 서명은 변경되지 않고 깨지지 않습니다 (물론 MD5 지문이 변경됩니다.).
인증서 MD5 지문 충돌
이제, 여러 항목을 함께 조합하고 이미 서명된 기존 인증서를 조작해 악성 인증서의 MD5 지문과 충돌할 수 있는 방법을 제공할 수 있습니다.
웹사이트의 TLS 인증서(EMC의 '표적 인증서')와 같이 RSA가 서명한 정상적인 최종 인증서를 사용합니다.
인증서의 TBS 부분에서 흥미로운 필드(제목, 확장, EKU, 공개 키 등)를 수정해 악성 인증서를 만듭니다. 참고: 서명에 손을 대지 않으므로 악성 인증서가 잘못 서명됩니다. 여기서는 공개 키를 수정해야 합니다. 이렇게 하면 공격자가 악성 인증서로 서명할 수 있습니다.
두 인증서에 대해 signatureAlgorithm 필드의 매개변수 필드를 수정해 두 인증서의 동일한 오프셋에서 MD5 충돌 블록(위의 설명에서 C 및 D)을 배치할 수 있는 충분한 공간을 마련합니다.
MD5 충돌 블록을 배치할 위치에서 두 인증서를 길이를 짧게 만듭니다.
MD5 선택 접두사 충돌 계산을 수행하고 그 결과를 인증서에 복사합니다.
정상적인 인증서의 서명 값(위 설명의 접미사 E)을 불완전한 두 인증서에 연결합니다.
실제 사례
MD5 충돌에 대한 이해를 바탕으로 이제 실제 표적으로 이 CVE를 악용할 수 있습니다. 검사한 수많은 애플리케이션 중에서 취약한 표적인 Chrome v48을 찾을 수 있었습니다. (이 애플리케이션은 단순히 CERT_CHAIN_CACHE_END_CERT 플래그를 CertGetCertificateChain에 전달하기 때문에 취약합니다.) 당시의 다른 Chrome 기반 애플리케이션도 이 CVE에 취약합니다.
이 취약점을 악용하려면 먼저 MD5 지문이 동일한 두 개의 인증서를 만들어야 했으며 이를 위해 HashClash 를 사용했습니다(그림 8).
그런 다음 수정된 표적 인증서를 Chrome의 캐시에 삽입하는 방법을 찾아야 했습니다. 개인 키를 모르면 인증서를 제공할 수 없기 때문에 까다로웠습니다.
TLS 1.2에는 두 가지 확인 단계가 있습니다.
서버 키 교환 메시지 - 이 메시지는 인증서에 의해 서명되었으므로 인증서의 개인 키를 알고 있는 사람만 작성할 수 있습니다
서버 핸드셰이크 완료 메시지 - 이 메시지에는 이전의 모든 핸드셰이크 메시지에 대한 변조 방지 확인이 포함됩니다
(TLS 1.3은 다르므로 초점을 맞추지 않았습니다.).
공격의 첫 단계에서 수정된 인증서를 Chrome의 최종 인증서 캐시에 삽입해야 한다는 점을 기억하세요.
Python 스크립트를 프록시로 사용해 MITM(Machine-In-The-Middle) 공격을 수행합니다.
악성 MITM 서버는 실제 서버와 대화하고 TLS 핸드셰이크의 첫 번째 메시지를 피해자에게 반영합니다.
서버 인증서 메시지에서 악성 MITM 서버는 실제 서버의 메시지를 수정하고 실제 표적 인증서를 수정된 인증서로 대체합니다.
서버 키 교환 메시지는 변경 없이 반영될 수 있습니다.
악성 서버가 단순히 서버 핸드셰이크 완료 메시지를 전달할 수는 없습니다. 핸드셰이크가 실제로 변조되었기 때문입니다. 따라서 연결을 종료합니다.
서버 키 교환 메시지를 확인하려면 Chrome에서 CryptoAPI를 사용해 수정된 인증서를 로딩해야 하므로 캐시에 삽입됩니다. Chrome은 끊어진 연결을 TLS 보안 문제로 취급하지 않습니다. 임의 네트워크 문제일 수도 있기 때문입니다. Chrome이 다시 연결을 시도하고 이번에는 실제 웹사이트의 메시지를 반영하는 대신 악성 서버가 악성 인증서를 사용해 웹사이트에 서비스를 제공합니다. Chrome은 인증서가 이미 캐시에 있다고 생각하기 때문에 전체 확인 프로세스를 건너뜁니다. 결과적으로 정상적인 것처럼 보이는 Microsoft 웹사이트에 원활하게 방문할 수 있습니다(그림 9 및 10). 전체 악용 흐름은 비디오에서 확인할 수 있습니다.
탐지
Microsoft는 취약한 crypt32.dll 버전을 탐지하기 위한 OSQuery를 제공합니다(그림 11). Akamai Guardicore Segmentation 고객은 이 쿼리와 함께 Insight 기능을 사용해 취약한 자산을 검색할 수 있습니다.
자산이 취약하게 되려면 패치되지 않은 crypt32.dll 버전이 있어야 하며 취약한 애플리케이션을 실행해야 합니다. (지금까지 Chrome v48만 취약하다는 사실을 발견했습니다.)
WITH product_version AS (
WITH os_minor AS (
WITH os_major AS (
SELECT substr(product_version, 0, instr(product_version, ".")) as os_major, substr(product_version, instr(product_version, ".")+1) as no_os_major_substr
FROM file
WHERE path = "c:\windows\system32\crypt32.dll"
)
SELECT substr(no_os_major_substr, instr(no_os_major_substr, ".")+1) as no_os_minor_substr, substr(no_os_major_substr, 0, instr(no_os_major_substr, ".")) as os_minor, os_major
FROM os_major
)
SELECT
CAST(substr(no_os_minor_substr, instr(no_os_minor_substr, ".")+1) AS INTEGER) AS product_minor,
CAST(substr(no_os_minor_substr, 0, instr(no_os_minor_substr, ".")) AS INTEGER) AS product_major,
CAST(os_minor AS INTEGER) AS os_minor,
CAST(os_major AS INTEGER) AS os_major
FROM os_minor
)
SELECT
CASE
WHEN os_major = 6 AND os_minor = 3 THEN "not supported"
WHEN (
(product_major = 20348 AND product_minor >= 887)
OR
(product_major = 17763 AND product_minor >= 3287)
OR
(product_major = 14393 AND product_minor >= 5291)
OR
(product_major >= 19041 AND product_minor >= 1889)
)
THEN
"patched"
ELSE
"not patched"
END is_patched
FROM product_version
결론
인증서는 온라인에서 ID를 확인하는 데 중요한 역할을 하므로 공격자에게 유리합니다. 이 취약점은 심각한 것으로 분류됐지만 CVSS 점수는 7.5에 불과했습니다. 이는 취약점의 전제 조건이 충족되는 취약한 애플리케이션 및 Windows 구성요소의 범위가 제한되기 때문으로 보입니다.
그러나 이 API를 사용하는 코드가 여전히 많고 Windows 7 같은 Windows의 단종 버전에도 패치를 적용하면서 이 취약점에 노출될 수 있습니다.
Microsoft에서 릴리스한 최신 보안 패치를 사용해 Windows 서버와 엔드포인트를 패치하는 것이 좋습니다. 개발자의 경우 CertVerifyCertificateChainPolicy같은 다른 WinAPI를 사용해 인증서의 유효성을 다시 검사하면 이 취약점을 방어할 수 있습니다. 최종 인증서 캐싱을 사용하지 않는 애플리케이션은 취약하지 않습니다.
PoC 코드는 GitHub 리포지토리에서 확인할 수 있습니다. 또한 Twitter 계정을 통해 최신 Akamai Security Intelligence Group 간행물을 확인할 수 있습니다.