← All issues

[9] SubtleCrypto RSA import OOB read from missing ASN.1 bounds check

Severity: Medium | Component: WebCore SubtleCrypto | bbec6de

Medium으로 평가한 이유는 실제로 관측되는 영향이 안정적으로 재현 가능한 renderer crash에 한정되기 때문입니다. Hardened 빌드에서는 crypto.subtle.importKey()를 통해 어느 웹 페이지에서나 매번 동일하게 SIGTRAP을 발생시킬 수 있습니다. Information disclosure에 관해서는 commit에서 뒷받침하는 근거가 없습니다. Non-hardened 빌드에서 OOB heap read로 확장될 가능성은 존재하지만, 읽힌 데이터는 CCRSACryptorImport로만 전달됩니다. 이 함수가 해당 데이터를 거부하기 때문에 info-leak 가능성은 제한적입니다.

CryptoKeyRSA::importSpki()CryptoKeyRSA::importPkcs8()는 이제 ASN.1 헤더 크기 계산이 완료된 후, keyData.subspan(headerSize) 호출 이전에 keyData.size() < headerSize 검사를 수행합니다.

Source/WebCore/crypto/cocoa/CryptoKeyRSAMac.cpp

if (keyData.size() < headerSize + 1)
return nullptr;
headerSize += bytesUsedToEncodedLength(keyData[headerSize]) + sizeof(InitialOctet);
+ if (keyData.size() < headerSize)
+ return nullptr;
 
CCRSACryptorRef ccPublicKey = nullptr;
auto dataAfterHeader = keyData.subspan(headerSize);
if (keyData.size() < headerSize + 1)
return nullptr;
headerSize += bytesUsedToEncodedLength(keyData[headerSize]);
+ if (keyData.size() < headerSize)
+ return nullptr;
 
CCRSACryptorRef ccPrivateKey = nullptr;
auto dataAfterHeader = keyData.subspan(headerSize);

LayoutTests/crypto/subtle/rsa-import-pkcs8-truncated-key.html

+ // headerSize 계산 과정:
+ // 5. headerSize = 21 + 3 = 24
+ // 6. 22바이트 버퍼에서 subspan(24) 호출 → CRASH (수정 전)
+ var truncatedPkcs8Key = hexStringToUint8Array("30000000000000000000000000000000000000000082");
+ shouldReject('crypto.subtle.importKey("pkcs8", truncatedPkcs8Key, {name: "RSA-OAEP", hash: "sha-256"}, extractable, ["decrypt"])');

importSpki()importPkcs8() 각각에 동일한 한 줄이 추가되었습니다. 두 곳 모두 마지막 headerSize += bytesUsedToEncodedLength(...) 연산 이후, keyData.subspan(headerSize) 호출 이전에 if (keyData.size() < headerSize) return nullptr; 검사가 삽입되었습니다. 기존의 중간 단계 bounds check는 변경 없이 유지됩니다.

ASN.1은 태그-길이-값(TLV) 인코딩 방식을 사용합니다. 길이 필드는 "short form"(단일 바이트, ≤ 127)과 "long form"(첫 바이트 = 0x80 + N, 이후 N바이트로 실제 길이를 인코딩) 두 가지 형태로 구분됩니다. bytesUsedToEncodedLength()는 길이 필드가 소비하는 총 바이트 수를 반환하며, long form의 경우 이 값은 (firstByte - 128) + 1에 해당합니다. WebKit의 RSA key import 함수는 headerSize 오프셋을 점진적으로 계산하면서 ASN.1 헤더를 수동으로 제거한 뒤, 해당 오프셋을 기준으로 std::span::subspan()을 호출해 입력 버퍼를 분리합니다.

Hardened C++ 표준 라이브러리 빌드에서는 오프셋이 범위를 벗어날 경우 subspan()이 매번 동일하게 trap(EXC_BREAKPOINT / SIGTRAP)을 발생시킵니다. Non-hardened 빌드에서는 동일한 상황에서 subspan()이 버퍼 끝을 넘어선 지점에서 시작하는 span을 조용히 생성합니다.

증분 방식의 ASN.1 header size 계산 완료 후, span slicing 이전의 최종 bounds check가 누락된 패턴.

importSpki()importPkcs8() 두 함수 모두 ASN.1 길이 필드를 기반으로 headerSize를 누적 계산하는 과정에서 중간 단계마다 bounds check를 수행했습니다. 그러나 마지막 증분 연산인 bytesUsedToEncodedLength(keyData[headerSize]) 추가 이후에는 후속 검사가 없었습니다. 마지막 위치에 큰 length-of-length 지시자가 포함된 조작된 키를 사용하면 headerSizekeyData.size() 너머로 밀어낼 수 있었습니다. 예를 들어 0x82는 추가 길이 바이트 2개를 의미하므로 함수가 3을 반환합니다. 테스트 케이스는 이를 직접 보여줍니다. 22바이트 PKCS#8 키에서 마지막 0x82 바이트로 인해 headerSize가 24에 도달하고, 22바이트 버퍼에서 subspan(24)를 호출하면 crash가 발생합니다.

🔒

Explores the exploitability boundaries of this OOB access and what limits escalation beyond a crash

더 확인하려면 구독해 주세요

🔒

Multiple audit patterns identified for ASN.1 parsing across WebKit's crypto subsystem, with concrete search targets

더 확인하려면 구독해 주세요