1. 개요
Chrome V8 엔진의 Turboshaft 최적화 컴파일러에서 Single-Block Loop 구문 처리 중 예외 처리 미흡으로 발생한 Type Confusion 취약점으로, 특수하게 조작된 HTML 페이지를 통해 취약점을 악용하여 원격 코드 실행이 가능하다. Chrome 버전 131.0.6778.241 까지 영향을 받으며, 131.0.6778.264 버전에서 해당 취약점에 대한 패치가 이뤄졌다.
2. 사전지식
2.1. Turboshaft 이해
Turboshaft는 기존의 최상위 최적화 컴파일러인 Turbofan을 대체하기 위해 개발된 최적화 컴파일러다. Turboshaft는 기존의 Sea of Nodes(이하 SoN) 기반의 Intermediate Representation(이하 IR) 에서 Control Flow Graph(이하 CFG) 기반의 IR로 바꿈으로써 컴파일러의 성능과 유지보수성을 크게 향상했다.
그림 1. Sea of Nodes 시각화
위 그림은 왼쪽의 JS 코드를 SoN 형태로 변환한 그래프다. SoN의 특징은 명령어들로 전부 분리를 하여 각 명령어의 의존성만을 나타낸다. 각 노드는 단일 명령어이며, 엣지는 값의 사용을 나타낼 뿐이다. 문제는 너무 많은 노드가 생성되고, 이에 따른 너무 많은 엣지가 생성되면서 그래프가 굉장히 복잡해지고, 엔지니어가 해석하기 힘들어진다.
그림 2. Control-Flow Graph 시각화
위 그림은 같은 JS 코드를 CFG 형태로 변환한 그래프다. SoN 방식과 다르게 프로그램의 기본 블록을 사용하여 그래프를 생성한다. 이때 기본 블록은 연속된 명령어 시퀀스를 의미하고, 이 블록이 그래프의 노드가 되고 엣지는 프로그램의 Control Flow를 나타낸다. 위 SoN 방식의 그래프보다 훨씬 이해하기 쉽고 디버깅 하기 편해지며, 루프 구조나 종료 조건 파악이 쉬워진다.
2.2. CFG based IR 정의
Turboshaft는 위에서 설명한 바와 같이 CFG 기반 IR을 사용하고 있다. 이 때 IR 연산의 핵심 개념 중 사용되는 것이 Phi 노드다. Phi 노드는 Control Flow가 합쳐지는 지점에서 여러 경로로부터 오는 값을 병합하는 연산이다.
CFG Based IR은 기본적으로 Static Single Assignment (이하 SSA) 형태를 띠고 있다. SSA란 모든 변수가 단 한 번만 값을 할당 받도록 하는 IR인데, 이런 형태에서 서로 다른 Control-Flow에서 같은 변수에 할당된 값을 하나로 합치기 위해 Phi 노드가 필수적인 역할을 한다.
if (condition) {
x = 10;
} else {
x = 20;
}
// x = ?
JavaScript
복사
코드 1. SSA 설명
위와 같은 코드가 존재할 때, SSA의 경우 아래와 같은 코드로 변환한다.
if (condition) {
x1 = 10;
} else {
x2 = 20;
}
// x = ?
JavaScript
복사
코드 2. SSA 설명
이때, if 문 이후 x의 값은 10이나 20이 된다. 그러나 이후 x값에 새로운 값을 할당하게 되면 SSA 규칙이 깨진다.
x3 = Φ(if: x1, else: x2)
y = x3 + 5;
JavaScript
복사
코드 3. Phi 노드 설명
위 경우에 사용되는 것이 Phi 노드다. 정해지지 않은 값에 대해 새로운 값을 할당할 수 없기 때문에 x3 = Φ(if: x1, else: x2) 과 같은 방식으로 하나의 정의를 갖게 하도록 하는 함수를 사용한다.
2.3. 핵심 메소드 정보
WasmGCTypeAnalyzer은 Turboshaft에서 GC 코드의 타입 정보를 추론하여 최적화한다. 핵심 분석 과정은 아래와 같다.
1.
CFG의 각 블록을 순회하면서 타입 정보를 수집한다.
2.
각 GC연산에 대해 전용 처리 메서드를 제공한다.
a.
WasmTypeCast
b.
WasmTypeCheck
c.
AssertNotNull
d.
StructGet/StructSet
e.
Phi
3.
분석 완료 후 각 연산의 추론된 입력 타입을 제공한다.
WasmGCTypeAnalyzer을 이용하여 CFG 기반 Type 추론이 완료되면 WasmGCTypedOptimizationReducer을 통해 새로운 그래프로 축소하여 타입을 구체화한다. (WasmGCTypedOptimizationReducer 내부적으로 WasmGCTypeAnalyzer을 포함)
핵심 최적화 방안은 아래와 같다.
1.
타입 캐스트 제거
2.
Null 체크 제거 (Non-Nullable이 확실한 경우)
3.
타입 체크 상수화
3. 취약점 원인 분석
void WasmGCTypeAnalyzer::Run() {
...
// TODO(14108): This currently encodes a fixed point analysis where the
// analysis is finished once the backedge doesn't provide updated type
// information any more compared to the previous iteration. This could
// be stopped in cases where the backedge only refines types (i.e. only
// defines more precise types than the previous iteration).
if (needs_revisit) {
block_to_snapshot_[loop_header.index()] = MaybeSnapshot(snapshot);
// This will push the successors of the loop header to the iterator
// stack, so the loop body will be visited in the next iteration.
iterator.MarkLoopForRevisitSkipHeader();
}
...
}
C++
복사
코드 4. 취약점 Root Cause
취약점은 WasmGCTypeAnalyzer의 Run() 메소드에서 발생한다. Run() 메소드는 코드 블럭을 순회하며 각 지점에서의 변수 타입을 분석한다. 이때 Loop 하는 코드 블록을 순회하는 과정에서 돌아가는 Flow인 Backedge를 발견하면 루프를 감지하게 되는데, 이후 루프 헤더를 다시 처리하고 이전 스냅샷과 새로운 스냅샷을 비교한다. 만약 타입 정보가 변경되어 재방문이 필요한 경우 루프 헤더의 자식 블록들을 Iterator 스택에 push하고 이후 자식 블록으로 방문하도록 스케줄링한다. 이 과정은 타입 정보가 변경되지 않을 때까지 반복된다.
void AnalyzerIterator::MarkLoopForRevisit() {
DCHECK_NOT_NULL(curr_.block);
DCHECK_NE(curr_.generation, kNotVisitedGeneration);
DCHECK(curr_.block->HasBackedge(graph_));
const Block* header =
curr_.block->LastOperation(graph_).Cast<GotoOp>().destination;
stack_.push_back({header, ++current_generation_});
}
void AnalyzerIterator::MarkLoopForRevisitSkipHeader() {
DCHECK_NOT_NULL(curr_.block);
DCHECK_NE(curr_.generation, kNotVisitedGeneration);
DCHECK(curr_.block->HasBackedge(graph_));
const Block* header =
curr_.block->LastOperation(graph_).Cast<GotoOp>().destination;
for (const Block* child = header->LastChild(); child != nullptr;
child = child->NeighboringChild()) {
stack_.push_back({child, ++current_generation_});
}
}
C++
복사
코드 5. MarkLoopFOrRevisitSkipHeader 메소드 구현 코드
위 코드는 MarkLoopFOrRevisitSkipHeader() 메소드의 코드다. 코드를 보면 헤더의 자식 노드들을 스택에 push 하는 모습을 볼 수 있는데 문제는 Single-block Loop의 타입을 분석할 때 발생한다. Single-Block Loop의 경우에는 헤더와 바디가 동일하기 때문에 헤더를 건너뛰는 위 메소드를 사용 시 바디의 타입을 분석하지 않는 문제가 발생한다.
바디의 타입 분석이 불완전하면 Phi 노드들, 즉 여러 번 타입이 바뀌는 경우에 정확한 타입 정보를 받을 수 없다. 이 경우, 변수 간에 악용할 수 있는 Type Confusion 취약점이 발생하게 된다.
4. 검증 결과 및 시연
영상 1. 임의 코드 실행 공격 성공 영상
calc.exe를 실행하는 임의 코드 실행 공격에 성공한 모습이다. 위 사진은 해당 취약점에 추가적으로 V8 Sandbox를 우회하는 취약점을 체이닝하여 chrome.exe --no-sandbox 환경에서 실행한 결과다.
5. 대응 방안
5.1. 사전 예방 및 방어 전략
•
Chrome 버전 업데이트
◦
Chrome 131.0.6778.264/.265 및 이후 버전 (Windows, Mac)
◦
Chrome 131.0.6778.264 및 이후 버전 (Linux)
•
의심 사이트 및 첨부파일 주의
◦
CVE-2025-0291은 조작된 HTML 페이지로 트리거되므로, 알 수 없는 링크 클릭 자제.
◦
메일/메신저를 통한 의심 웹페이지 접근 최소화.
•
방어 쳬계의 실효성 검증 및 고도화(BAS 도입)
당사의 솔루션인 PurpleHound는 실전 기반 시나리오를 재구성하여 기업이 보유한 보안 장비와 시스템이 실제 공격에 얼마나 효과적으로 대응할 수 있는지를 검증할 수 있도록 지원하며, 최근 Chrome v8의 보안 취약점(CVE-2025-0291)을 선제적으로 분석하여 보안 위협에 앞장서 대응하고 고객사가 신속하고 정확하게 위협을 검증할 수 있도록 전문적인 지원을 제공하고 있다.
◦
보안 공백 식별: 현재 운영 중인 보안 솔루션(EDR, 방화벽 등)의 탐지/차단 누락, 설정 오류, 정책 미비점 등 우리가 미처 인지하지 못했던 실질적인 보안의 허점을 선제적으로 식별한다.
◦
데이터 기반 방어 최적화: 어떤 공격 경로가 유효하고 어떤 방어 체계가 효과적인지에 대한 객관적인 데이터를 확보하여, 추측이 아닌 증거에 기반한 보안 투자 및 정책 개선을 가능하게 한다