01개요
V8은 Chrome과 Node.js가 함께 쓰는 자바스크립트 엔진입니다. 코드를 여러 단계로 나눠 최적화하는데, Maglev는 2023년에 추가된 비교적 젊은 중간 단계 최적화 컴파일러입니다. CVE-2024-0517은 이 Maglev가 클래스 상속 — 정확히는 파생 클래스가 super()를 호출하는 코드 — 을 컴파일하는 과정에서 발생하는 out-of-bounds write(경계를 벗어난 쓰기) 취약점입니다.
공격자가 만든 HTML 페이지를 열기만 해도, 그 페이지 안의 자바스크립트가 실행되는 시점에 힙 메모리가 손상됩니다. 이 손상은 V8이 돌아가는 렌더러 프로세스 안에서의 임의 코드 실행으로 이어질 수 있지만, 그 자체로 시스템 전체 장악은 아닙니다 — 렌더러 밖으로 나가려면 별도의 샌드박스 탈출이 한 번 더 필요합니다. 이 구분은 심각도를 읽을 때 중요합니다.
CVSS: 8.8 (CVSS 3.1, High) · CWE-787(Out-of-bounds Write)
같은 2024년 1월 16일자 Chrome 안정 채널 업데이트에는 CVE-2024-0518·CVE-2024-0519도 함께 패치됐습니다. 그중 실제 악용(In-the-wild)이 확인된 건 별개 취약점인 CVE-2024-0519이며, CVE-2024-0517 자체가 실사용 공격에 쓰였다는 근거는 확인되지 않습니다 — 같은 날 나란히 발표돼 혼동하기 쉬운 지점이라 짚어둡니다. 확인
02영향받는 버전
| 대상 | 취약 버전 | 안전 버전 |
|---|---|---|
| Google Chrome | 120.0.6099.224 미만 | 120.0.6099.224 이상 |
| V8 엔진(standalone / Node.js 등 임베더) | 12.0.267.16 미만 분석 | 12.0.267.16 이상 분석 |
| Chromium 기반 브라우저(Edge·Brave·Opera 등) | Chrome과 동일 V8 라인을 공유하는 동시대 빌드 | 각 벤더가 이후 배포한 대응 업데이트 분석 — 벤더별 개별 확인은 하지 않음 |
03배경 — 컴파일러가 '접어서' 할당하는 이유
V8은 코드를 한 번에 최적화하지 않습니다. 처음엔 인터프리터(Ignition)가 그대로 실행하고, 자주 실행되는(hot) 함수만 골라 순차적으로 더 똑똑한 컴파일러에 넘깁니다. Maglev는 그 중간 단계 — TurboFan만큼 시간을 들이지는 않으면서 인터프리터보다는 훨씬 빠른 '적당히 최적화된' 기계어를 빠르게 뽑아내는 역할을 맡습니다.
이 컴파일러들이 즐겨 쓰는 최적화 중 하나가 할당 접기(allocation folding)입니다. 자바스크립트 객체 하나를 만들 때 실제로는 객체 본체·프로퍼티 저장 공간·배열 등 여러 조각이 연달아 할당되는 경우가 많습니다. 컴파일러가 '이 조각들은 항상 같이 만들어진다'는 걸 미리 알면, 힙 할당자에게 여러 번 요청하는 대신 한 번에 큰 덩어리를 받아 각 조각을 그 안의 고정된 오프셋에 배치합니다 — 요청 횟수를 줄여 속도를 버는 방식입니다.
이 최적화가 위험해지는 지점은 딱 하나입니다. 첫 조각과 둘째 조각 사이에 가비지 컬렉션(GC)이 끼어드는 경우입니다. V8의 이동식(moving) GC는 살아있는 객체를 다른 자리로 옮기고, 그 순간 아무도 참조하지 않는 자리는 회수해 재사용합니다. 컴파일러가 '이 오프셋엔 나중에 둘째 조각이 들어갈 것'이라 약속해 둔 그 자리가, GC 입장에서는 그냥 '아직 아무것도 없는 빈 공간'입니다. 이 둘의 인식 차이가 다음 섹션의 단서입니다.
04취약점 발생 원인
이 취약점은 코드 한 줄의 실수가 아니라, 최적화 로직이 자신의 상태를 언제 지워야 하는지에 대한 관리 실패에 가깝습니다. 아래 두 가지 미흡점이 함께 있어야 완성됩니다 — 하나는 '상태를 정리하지 않은 것', 다른 하나는 '그 상태가 실제로 위험해지는 경로를 만들어내는 것'입니다.
1"이 할당 자리는, 다음 할당이 쓸 때까지 그대로 켜 둔다"
여러 할당을 하나로 접는 최적화의 '진행 중' 상태를 다 쓰고 난 뒤 명시적으로 꺼야 하는데, 클래스 상속을 처리하는 경로에서 그 호출이 빠졌습니다.
Maglev의 할당 접기는 current_raw_allocation_이라는 내부 상태 하나로 관리됩니다. 이 값이 켜져 있으면 '지금 접을 수 있는 할당이 진행 중'이라는 뜻이고, 컴파일러는 다음 객체를 새로 요청하는 대신 이 값이 가리키는 자리 안쪽에 이어 붙입니다. 이 상태를 관리하는 ExtendOrReallocateCurrentRawAllocation()은 상태를 켜는 일은 하지만, 다 쓰고 난 뒤 끄는 일은 호출한 쪽의 책임으로 남겨 뒀습니다.
실제 패치 diff(Chromium 커밋 78dd4b31847ab1f5b06ef3d8742a9f3835fb6919, chromium-review 5173470)의 컨텍스트를 그대로 따른 발췌 — fast path에서 객체 할당 직후 접기 상태(current_raw_allocation_)를 비우지 않는 지점. 함수 앞부분(부모 생성자 탐색)은 생략. 확인
문제는 클래스 상속을 컴파일하는 경로 — super() 호출을 처리하는 VisitFindNonDefaultConstructorOrConstruct() — 가 이 '끄기'를 하지 않는다는 점입니다. 파생 클래스의 생성자를 만들며 BuildAllocateFastObject()로 객체를 할당한 뒤, 접기 상태를 켠 채로 함수가 끝납니다.
왜 이렇게 됐나Maglev는 짧은 시간 안에 수많은 함수를 최적화해야 하는 JIT이라, 할당 경로마다 상태를 세심히 정리하기보다 '호출한 쪽이 필요하면 정리한다'는 계약으로 설계됐습니다. 이런 계약은 호출부가 하나둘 늘어날수록, 새로 추가되는 경로 하나가 정리를 빠뜨리기 쉬운 구조입니다. 분석
2"내가 접어 둔 자리는, 컴파일이 끝날 때까지 그대로 있다"
할당 접기가 '그 사이에 가비지 컬렉션이 끼어들 수 있다'는 사실을 반영하지 않아, 이동한 객체와 회수된 자리 사이에서 쓰기가 실제 메모리 밖으로 빠져나갑니다.
W1에서 켜진 접기 상태가 실제로 위험해지려면 한 가지가 더 필요합니다 — 그 상태가 켜진 채로 가비지 컬렉션을 유발할 수 있는 코드를 거친 뒤, 다른 무관한 객체(예: 배열 리터럴)가 또 만들어지는 경우입니다. 파생 클래스 생성자 안에서 super() 호출 다음에 프로퍼티를 대입하고, 그 다음에 배열을 만드는 코드가 정확히 이 조건을 채웁니다.
프로퍼티 대입이 새 힙 공간을 요구하며 GC를 유발하면, 이동식 GC는 W1에서 만들어진 첫 객체(살아있는, 참조되는 객체)는 새 자리로 옮기고, 컴파일러가 '나중에 둘째 조각이 들어올 것'이라 약속해 둔 뒷자리는 — 아직 아무도 참조하지 않으므로 — 그냥 비어 있는 공간으로 취급해 회수합니다. 이후 배열 리터럴이 그 접힌 오프셋에 초기화될 때, 실제로는 이미 다른 용도로 재배치됐거나 비어 있는 메모리에 값을 씁니다. 컴파일 시점에 계산한 오프셋과 실행 시점의 실제 힙 배치가 어긋난 것입니다.
왜 이렇게 됐나이동식 GC는 '살아있는 객체를 옮기고, 아무도 안 쓰는 자리는 회수한다'는 원칙으로 동작합니다. 할당 접기 최적화는 이 GC의 자유를 전혀 고려하지 않은 가정 — '컴파일 시점에 정한 오프셋은 실행 시점까지 그대로 유효하다' — 위에 설계됐습니다. 서로 다른 두 서브시스템(컴파일러의 최적화, 런타임의 GC)이 상대의 규칙을 몰랐던 셈입니다. 분석
이 취약점의 무게 중심은 W2(할당 접기가 GC 경계를 넘을 수 있다는 사실을 반영하지 않음)입니다. W1(상태 미정리)만 있고 그 사이에 GC를 유발하는 코드가 끼지 않으면 접기는 정상적으로 끝나 아무 문제가 없습니다.
바꿔 말하면 이건 '한 줄의 실수'가 아니라 '두 서브시스템이 서로의 계약을 몰랐던' 문제입니다. 그래서 패치도 결국 그 계약의 경계 — 접기 상태가 언제 끝나야 하는지 — 를 명시적으로 그어주는 방향으로 정리됐습니다. 분석
05공격 과정
5.1 공격 과정 요약
- ① 공격자가 특정 형태의 파생 클래스(부모 클래스 + new.target + super() + 프로퍼티 대입 + 배열 리터럴)를 담은 페이지를 서비스한다.
- ② 이 함수가 여러 번 실행돼 Maglev의 최적화 대상(hot)이 되면, super() 처리 경로가 할당 접기 상태를 켠 채로 끝난다.
- ③ 접기 상태가 켜진 사이에 실행되는 프로퍼티 대입이 가비지 컬렉션을 유발한다.
- ④ 이동식 GC가 첫 객체는 옮기고, 접혀 있던 둘째 자리는 회수한다 — 배열 리터럴이 그 낡은 오프셋에 쓰이며 실제 메모리 경계를 벗어난다.
- ⑤ 이 out-of-bounds 쓰기가 서로 다른 종류의 배열(포인터 배열 vs 실수 배열)을 겹치게 만들어 타입 컨퓨전을 일으킨다.
- ⑥ 겹친 메모리를 통해 주소 유출과 임의 읽기/쓰기 프리미티브를 얻어, 렌더러 프로세스 안에서 임의 코드 실행에 도달한다(추가 샌드박스 탈출 없이는 렌더러 밖으로 못 나감).
5.2 상세 공격 과정
공격은 서버가 아니라 브라우저에서 실행되는 자바스크립트 코드로 시작합니다. 공격자는 앞서 설명한 조건 — 부모 클래스가 있고, 생성자가 new new.target()과 super()를 호출하며, 그 뒤에 프로퍼티 대입과 배열 리터럴이 이어지는 파생 클래스 — 를 만족하는 함수를 준비하고, 이 함수를 반복 호출해 V8이 '자주 쓰이는 함수'로 인식하게 만듭니다(Maglev로 컴파일이 트리거되는 조건입니다).
Maglev가 이 함수를 컴파일하면서 super() 호출을 VisitFindNonDefaultConstructorOrConstruct()로 처리하는 순간, W1의 상태 누락이 발동합니다 — 이후 이어지는 프로퍼티 대입·배열 생성 코드까지 같은 접기 상태를 공유하게 됩니다.
생성된 코드가 실제로 실행될 때, 프로퍼티 대입이 힙 공간을 필요로 해 가비지 컬렉션을 유발하면 W2가 발동합니다. 이 시점에 접혀 있던 자리가 재배치되고, 뒤이은 배열 리터럴 초기화가 실제로는 다른 메모리(이미 회수됐거나 다른 목적으로 쓰이는 공간)에 값을 씁니다.
여기서부터는 이미 패치된 취약점에 대한 공개 n-day 연구가 보여준 일반적인 익스플로잇 구조를 개념 수준으로만 설명합니다 — 실제 동작 코드는 싣지 않습니다. 손상된 메모리는 정수·객체를 담는 배열과 부동소수점을 담는 배열이 같은 자리를 가리키게 만드는 데 쓰여, 한쪽 배열에 객체를 넣고 다른 쪽에서 실수로 읽으면 객체의 메모리 주소가 그대로 드러납니다(addrof 프리미티브). 주소를 알면 배열 메타데이터(길이 등)를 조작해 힙 전체에 대한 사실상 임의의 읽기/쓰기로 확장할 수 있습니다.
임의 쓰기를 확보한 뒤에는, 엔진이 내부적으로 쓰는 함수 포인터나 실행 가능 메모리 영역의 포인터를 겨냥해 실행 흐름을 바꾸는 것이 전형적인 다음 단계입니다. 이 지점부터는 V8·브라우저 자체의 후속 하드닝(예: 민감한 포인터를 별도 보호 영역으로 옮기는 조치)이 계속 발전해 온 영역이라, 구체적 기법의 유효성은 Chrome 버전마다 다릅니다 — 이 글에서는 원리만 남기고 세부 절차는 다루지 않습니다.
5.3 미흡점 ↔ 공격 흐름 매핑
| 미흡점(§4) | 발화 지점(§5.2) | 여는 프리미티브 | → 다음으로 잇는 것 |
|---|---|---|---|
| W1 — 접기 상태 미정리 | 8.2의 super() 처리(② 지점) | 이후 무관한 할당이 같은 접기 상태를 공유 | GC 유발 지점(③)까지 상태가 남음 |
| W2 — GC 경계 미인지 | 8.2의 프로퍼티 대입 → GC(③ 지점) | 이동된 객체 vs 회수된 오프셋 불일치 → OOB 쓰기 | 배열 리터럴 초기화가 실제 메모리 밖을 침범(④) |
| (귀결) 배열 타입 겹침 | OOB 쓰기로 만들어진 메모리 겹침(⑤) | 포인터 배열 ↔ 실수 배열 상호 해석 → 주소 유출·임의 읽기/쓰기 | 렌더러 프로세스 내 코드 실행(⑥) — 시스템 전체 장악은 별도 샌드박스 탈출 필요 |
06취약점 패치
왜여러 독립된 공개 n-day 분석(Exodus Intelligence 및 개별 리서처 2건)이 공통으로 지목하는 원인은 '접기 상태를 다 쓰고도 정리하지 않은 것'입니다. W1이 성립하던 바로 그 자리를 막기 위한 수정으로 보입니다. 분석 — 실제 커밋 diff 원문은 이번 조사에서 직접 확인하지 못했습니다. 위 코드는 여러 2차 분석의 공통 설명을 바탕으로 한 재구성이며, 실제 패치의 정확한 줄 위치·문법은 다를 수 있습니다.
어떻게여러 소스가 공통으로 말하는 수정 방향은, 파생 클래스 생성자 최적화 경로가 끝나는 지점에서 접기 상태를 비우는 호출을 추가해 이후의 무관한 할당이 같은 자리에 접히지 못하게 막는 것입니다. 분석
비고덧붙여, 이후 V8은 WebAssembly 인스턴스처럼 공격에 자주 쓰이는 민감한 포인터를 별도의 보호된 메모리 영역(Trusted Space)으로 옮기는 하드닝을 계속 추가해 왔습니다. 이는 이 CVE 하나를 겨냥한 수정이 아니라, 임의 쓰기를 코드 실행으로 전환하는 익스플로잇 체인 후반부를 어렵게 만드는 별개의 방어층입니다. 분석
07대응방안
근본 해결은 패치뿐입니다. Chrome을 120.0.6099.224 이상으로 올리면 이 경로 자체가 사라집니다. Node.js 등 V8을 임베드하는 다른 런타임은 사용 중인 V8 버전이 이 수정을 포함했는지 별도로 확인해야 합니다.
- 이 취약점은 JIT 컴파일러(Maglev) 내부 로직 버그라, WAF·CSP 같은 요청·콘텐츠 레벨 방어로는 막을 수 없습니다 — 트리거는 평범한 자바스크립트 문법(클래스 상속)이라 시그니처 차단 대상이 되지 않습니다. 확인
- 업데이트가 급하지 않은 내부망 전용 브라우저·임베디드 V8 애플리케이션이라면, 최적화 컴파일러 티어를 낮추는 실험적 플래그로 공격 표면을 줄이는 방법이 있습니다 — 다만 이는 상당한 성능 저하를 감수하는 임시 조치일 뿐 공식 완화책이 아니며, 패치를 대체할 수 없습니다. 분석
- 탐지 관점. 개별 요청 단위가 아니라 렌더러 프로세스 단위로 봅니다 — 반복적인 렌더러 크래시·비정상 종료(chrome://crashes, EDR의 렌더러 프로세스 이상 종료 로그)가 실질적인 관찰 대상입니다. 다만 이런 신호는 사후 탐지에 가깝고, 예방은 결국 버전 관리로 귀결됩니다.
08왜 이게 반복되는가
이 사건이 흥미로운 이유는 버그 자체보다 버그가 사는 자리입니다. 최적화 컴파일러는 '이 값은 항상 이 모양이다', '이 자리는 항상 비어 있다'처럼 실행 시점에 다시 확인하지 않는 가정 위에서 속도를 법니다. 그 가정이 컴파일러가 통제하지 못하는 다른 서브시스템(여기선 가비지 컬렉터) 규칙과 충돌하면, 정확성과 성능을 맞바꾼 대가가 취약점으로 돌아옵니다. 저희가 앞서 다룬 "데이터를 되살리려던 코드가, 코드를 되살렸다"(React2Shell, CVE-2025-55182)와 뿌리가 같습니다 — 거기서는 역직렬화기가 '내가 처리하는 건 전부 내가 만든 것'이라 믿었고, 여기서는 컴파일러가 '내가 예약한 자리는 그대로 있다'고 믿었습니다.
이번 글부터 심층판에 패치 완전성 평가를 정식으로 넣습니다. 패치 하나를 보고 끝내지 않고, ① 근본원인을 막았는가 ② 변형(variant)이 남았는가 ③ 조용히 넘어간 패치는 아닌가 — 세 가지를 확인하는 절차입니다. 이 CVE로 그 틀을 처음 적용해 봤습니다.
① 근본원인인가 증상인가. 여러 2차 분석이 공통으로 설명하는 수정 방향(접기 상태를 명시적으로 비우는 호출 추가)은 W1이 성립하던 원인 지점을 직접 겨냥합니다. 다만 이 수정의 적용 범위가 super() 경로 하나에 국한됐는지, 할당 접기를 쓰는 다른 경로 전체까지 아우르는 계약 강화였는지는 공개 정보만으로 확정하기 어렵습니다. 분석
② variant가 남았나. 0517과 같은 할당 접기 경로가 재발한 사례는 확인하지 못했습니다. 다만 같은 컴파일러(Maglev)에서 같은 취약점 계열(타입 컨퓨전)이 이후에도 반복됐다는 사실은 확인됩니다 — 약 4개월 뒤인 2024년 5월, 모듈 네임스페이스 객체의 접근 정보 처리 오류로 인한 별개의 Maglev 타입 컨퓨전(CVE-2024-4947)이 실제 악용 상태로 패치됐습니다. 원인은 서로 다르지만(하나는 GC 경계 인식 실패, 다른 하나는 접근 정보 추론 오류), '젊은 JIT 티어가 컴파일러 불변식을 계속 새로 깨뜨린다'는 같은 패턴입니다. 확인 — CVE-2024-4947의 존재·시기·컴포넌트는 확인. 0517과의 코드 수준 인과관계는 없음(같은 계열일 뿐 직접 파생은 아님) 이 패턴 자체가, Maglev의 다른 바이트코드 방문자 함수들 중 유사한 '상태 정리 계약 누락'이 남아있는지 정적으로 훑어볼 헌팅 가치를 시사합니다.
③ silent patch인가. 아닙니다. 정식 CVE 등록·NVD 공개·Chrome 정기 업데이트 공지·신고자 1만 6천 달러 바운티까지 통상적인 책임 있는 공개 절차를 그대로 따랐습니다. 같은 날 실제 악용이 확인된 CVE-2024-0519가 함께 발표돼 관심이 그쪽에 더 쏠렸을 뿐입니다. 확인
이 틈은 패치됐지만, 'JIT 컴파일러가 자신이 통제하지 못하는 다른 서브시스템(GC)의 규칙까지 알아야 한다'는 숙제는 Maglev가 계속 성장하는 한 반복해서 시험대에 오를 것입니다. 그래서 저희는 이 CVE 번호가 아니라, 이 사고방식과 패치 완전성을 확인하는 절차를 기록으로 남깁니다.
09결론
CVE-2024-0517은 V8의 Maglev 컴파일러가 클래스 상속을 최적화하며 할당 접기 상태를 정리하지 않아, 그 사이에 낀 가비지 컬렉션과 충돌해 발생하는 out-of-bounds write입니다. CVSS 8.8(High), CWE-787로 등록됐고 Chrome 120.0.6099.224로 패치됐습니다. 패치는 원인 지점을 직접 겨냥한 것으로 보이지만, 같은 컴파일러의 같은 취약점 계열(타입 컨퓨전)은 이후에도 재발했습니다 — 패치 하나로 클래스 전체가 닫히지는 않는다는 근거입니다.
참고 자료
공개된, 이미 패치 완료된 취약점을 저희가 직접 분석·재구성한 연구 글입니다. 본문의 분석·판단·도식은 저희 것이며, 취약 함수의 코드 발췌는 V8(BSD 라이선스) 관련 공개 n-day 분석 다건을 교차 확인해 재구성했고, 패치 diff는 실제 Chromium 커밋(78dd4b31847ab1f5b06ef3d8742a9f3835fb6919, chromium-review 5173470)을 직접 대조해 확인했습니다.
- NVD — CVE-2024-0517 공식 설명·CVSS·CWE
- Chromium 버그 트래커 — crbug.com/1515930 · 원 신고(2024-01-06, Toan (Suto) Pham / Qrious Secure)
- Exodus Intelligence — Google Chrome V8 CVE-2024-0517 Out-of-Bounds Write Code Execution (2024-01-19) · 저희가 재구성한 원 분석
- CVE-2024-4947 — NVD (GitHub Advisory / SentinelOne 요약) · §패치 완전성 평가 ②(variant) 논의의 근거
- 공개 n-day 리서치 2건(개인 기술 블로그) — 사실 교차 확인용, 본문에 직접 인용하지 않았습니다.
- securityaffairs.com — Chrome 2024년 1월 업데이트 보도(CVE-2024-0517·0518·0519 구분 확인용)
- infiltr8 Lab, 「데이터를 되살리려던 코드가, 코드를 되살렸다」(React2Shell, CVE-2025-55182) — 같은 계열 논평 연결