[Research] Type Confusion 101으로 시작하는 Chrome Exploit ^-^☆Part 1.(KR)

Introduction

안녕하세요 OUYA77 입니다.

갑자기 뜬금 없이 크롬이 나와서 당황하셨죠. 저도 많이 당황했습니다.

제가 크롬을 하고 있을줄은 몰랐는데, 어쩌다가 컨퍼런스 동영상을 보게 되었고 그냥 “와, 재밌다.”라는 생각이 들어서 아무생각 없이 공부하게 되었습니다(링크).

image.png

크롬에 대해서 버그 바운티를 해야지, 제로데이를 찾아야지 같은 거창한 목표는 없구요. 그냥 ‘재밌으니 공부해보고 싶다’ 정도 인거 같습니다. 크롬에서 취약점 분석 공부를 해보면 Type Confusion이라는 용어를 심심치 않게 들을 수 있는데, 이 Type Confusion 이 뭘까도 궁금했습니다. 인터넷에 있는 블로그 글들을 쭉 봤는데, js, 웹 관련 내용을 1도 모르는 저로서는 이해하기가 너무어려웠어요. 그래도 Chrome Exploit 을 공부해봤는데 아 이래서 Type Confusion 이 발생하고 이걸 이용해서 어떻게 Exploit을 하는구나 싶었습니다.

제가 공부한 내용을 어떻게 생산성있게 의미를 가져갈 수 있을까 고민해보았는데, “배워서 남주자”라는 말처럼 크롬 exploit 공부에 가장 baseline 이 되는 Type Confusion에 대해서 초심자가 입문하기 좋게 한다면 그걸로 이미 유의미한 결과가 아닐까 생각하게 되었습니다.

그래서 나왔습니다!!!!

TypeConfusion101.png

Type Confusion 이 무엇이고 크롬에서는 왜 발생하며, 이를 통해 무엇을 할 수 있는지 한번 알아보도록 하겠습니다! 사실 이미 저의 사심(?)을 담아서 Type Confusion 에 관련한 스포를 이미 하고 있었어요.

https://maily.so/hackyboiz/posts/1gz2v4jxr3q

ㅋㅋㅋㅋㅋㅋㅋ^^

Type Confusion 101 in Chrome 시리즈는 4부작으로 생각 중인데, 인생은 역시 생각한대로 안되는게 재미 아니겠어요?!? 어떻게 될지는 같이 지켜보자구요! ㅎㅎ

오늘은 기본적인 내용부터 알아보겠습니다. 이미 다 아시겠지만 크롬이 무엇이고 Type Confusion 이 발생하는 V8은 어떤 component인지 같이 보러 가시죠

image.png

1. Chrome 전체 아키텍처 개요

1.1 Multi-process 구조

크롬은 딸깍(아이콘 더블클릭)하면 하나의 프로그램이 켜지지만, 사실은 여러 개의 프로세스들이 IPC 통신을 하며 돌아가는 구조를 가지고 있습니다. (IPC 통신이 궁금하시다면 → **[Research] Windows Named Pipe (KR) )**

image.png

ref. https://developer.chrome.com/blog/inside-browser-part1

크롬 브라우저 내부에는 다양한 핵심 프로세스들이 존재합니다. 각각은 특정 역할을 수행하며, 함께 작동해 하나의 통합된 브라우저 환경을 구성합니다.

  • Browser Process

    우리가 매일 보는 탭, 주소창, 즐겨찾기 바 등 브라우저의 UI를 담당합니다. 크롬을 실행했을 때 가장 먼저 작동하는 핵심 프로세스입니다.

  • Renderer Process

    웹페이지의 실질적인 렌더링 작업을 담당합니다. HTML, CSS, JavaScript 같은 프런트엔드 리소스를 해석하여, 우리가 눈으로 볼 수 있는 시각적 결과물로 변환하는 역할을 합니다.

  • GPU Process

    2D/3D 그래픽 처리나 하드웨어 가속과 관련된 작업을 전담합니다.

  • Utility / Network / Extension Process 등

    각각 네트워크 통신, 멀티미디어 처리, 확장 프로그램 실행 같은 별도의 기능들을 수행합니다.

이처럼 각 프로세스가 독립적으로 역할을 분담하는 구조를 우리는 멀티 프로세스 아키텍처라고 부릅니다.

이렇게 크롬이 여러 프로세스로 나뉘어 있는 가장 큰 이유는 바로 보안안정성 때문입니다. 각 프로세스는 서로 격리되어 있어서, 설령 하나의 프로세스에 문제가 발생하더라도 전체 브라우저가 먹통이 되는 대참사를 막을 수 있습니다. 예를 들어, 악성 코드가 포함된 웹사이트 때문에 Renderer Process가 비정상적으로 종료되더라도, Browser Process는 여전히 살아남아 “이 페이지가 응답하지 않습니다” 같은 메시지를 사용자에게 전달할 수 있는 것이죠.

image.png

바로 이 격리 개념을 바탕으로 한 Sandbox 보안 모델이 크롬 보안의 핵심 중 하나입니다. (Sandbox는 다음 편에서 더 자세히 다루겠습니다!)

1.2 프론트엔드 리소스 처리 과정 in Chrome

image.png

Chrome 브라우저는 웹페이지를 구성하는 다양한 프론트엔드 리소스, 예를 들어 HTML, CSS, JavaScript 파일을 받아와 이를 렌더링합니다. 우리가 탭 하나를 열고 웹사이트에 접속하는 그 짧은 순간에도 위에서 보셨다시피 멀티 프로세스가 내부적으로는 꽤 복잡하게 작동하고 있습니다.

먼저, 브라우저는 서버로부터 HTML 문서를 가장 먼저 받아오고 이를 파싱합니다. 이 과정에서 <link><script> 태그 등을 만나게 되면, 그에 따라 외부 CSS 파일JavaScript 파일도 순차적으로 요청하고 불러오게 됩니다. 이렇게 받아온 리소스들은 각자 역할이 다릅니다. HTML은 페이지의 기본 구조를 정의하는 뼈대 역할을 하고, CSS는 그 뼈대에 색과 형태를 입히는 스타일 정보를 담당하죠. 마지막으로 JavaScript는 사용자와의 상호작용, 이벤트 처리, 애니메이션 등 웹페이지에 생명력을 불어넣는 동작 로직을 담당합니다.

특히 JavaScript는 다른 언어들과 달리 동적으로 타입이 바뀌고, 실행 중에 객체를 생성하거나 수정할 수 있는 유연한 특성을 가지고 있어요. 이런 특성 덕분에 웹 개발이 훨씬 유연해지지만, 반대로 이를 처리하는 브라우저 내부의 실행 환경은 훨씬 더 복잡해집니다.

image.png

그래서 등장하는 것이 바로 V8 JavaScript 엔진입니다. 크롬 브라우저는 이 V8 엔진을 사용해 JavaScript 코드를 빠르고 효율적으로 처리하며, 그 모든 과정은 렌더러 프로세스라는 독립적인 공간 안에서 이뤄지게 됩니다. 그럼 렌더러 프로세스가 무엇인지 더 알아보러 가시죠!

2. About Render Process

웹페이지를 실제로 “보여주고 동작하게 만드는” 모든 처리는 바로 이 렌더러 프로세스에서 이루어집니다. 하나의 탭은 하나의 렌더러 프로세스가 담당하며, 이 안에서 HTML 파싱과 CSS 적용, DOM 구성, JavaScript 실행, 레이아웃 계산, 페인팅, 컴포지팅까지 전체 렌더링 파이프라인이 진행됩니다.

image.png

JavaScript는 실행 중에 타입이 바뀌거나 객체 구조가 동적으로 변할 수 있는 아주 유연한 언어입니다. 하지만 이런 유연성은 곧, 메모리 변조나 Type Confusion 같은 보안 취약점이 발생할 위험도 함께 가져옵니다. 이런 위험을 줄이기 위해, Chrome은 JavaScript를 별도의 렌더러 프로세스에서 격리하여 실행합니다. 탭 하나에서 문제가 생겨도 해당 프로세스만 종료하면 되고, 다른 탭이나 브라우저 전체에는 영향을 주지 않게 설계된 거죠. 이제 본격적으로, 이런 렌더러 프로세스 내부에서 JavaScript가 실제로 어떻게 실행되는지 살펴보겠습니다.

브라우저의 렌더링 파이프라인에서 HTML 파싱과 화면 렌더링은 Blink 엔진이 담당하고, JavaScript 코드의 실행은 V8 엔진이 담당합니다. 이 두 엔진은 웹페이지를 완전히 구성하기 위해 긴밀하게 연결되어 동작합니다.

Blink는 렌더링 엔진으로, HTML을 파싱하고 DOM을 구성하며

V8은 JavaScript 실행을 전문으로 하는 엔진입니다.

이제 Blink에서 V8으로 제어 흐름이 넘어가는 과정을 살펴보겠습니다.

<script> 태그를 만나면

Blink는 HTML을 파싱하던 중 <script> 태그를 만나면, JavaScript 파일을 스트리밍 방식으로 받아들이기 시작합니다. 스트리밍된 JS 코드는 문자열 형태로 V8 엔진에 전달되고, 이제부터는 V8이 실행 흐름을 이어받습니다.

image.png


Scanner: 문자열 → 토큰(Token) 분해

V8 내부에서는 Blink로부터 전달받은 UTF-16 문자열 형태의 JavaScript 코드를 먼저 스캐너가 처리합니다. 스캐너는 이 문자열을 자바스크립트의 문법 규칙에 따라 의미 있는 최소 단위인 토큰으로 분해합니다. 예를 들어 function, if, =, 123, 'hello' 등은 모두 각각의 토큰으로 분리되며, 이후 구문 분석 단계에 사용됩니다.

image.png


③ Parser: AST 생성

이제 Parser가 토큰들을 분석해서 AST (추상 구문 트리, Abstract Syntax Tree)를 만듭니다. 이 트리는 코드의 구조와 의미를 표현한 나무 구조의 데이터인데, 컴파일러 이론이 포함되므로 이번 글에서는 넘어가겠습니다.

image.png


④ Ignition: Bytecode로 변환

생성된 AST는 V8의 인터프리터인 Ignition으로 전달됩니다. Ignition은 이 AST를 순회하며, JavaScript 코드를 V8 내부에서 실행 가능한 바이트코드로 변환합니다. 이 바이트코드는 CPU에서 직접 실행되는 머신 코드보다 한 단계 추상화된 중간 표현으로, 빠른 실행을 가능하게 합니다.

image.png


⑤ 실행!

최종적으로 생성된 바이트코드는 Ignition 인터프리터에 의해 순차적으로 실행됩니다. 이 단계에서 우리가 작성한 JavaScript 코드가 실제로 동작하며, 이벤트 리스너를 등록하거나, DOM을 수정하거나, 애니메이션을 실행하는 동작들이 수행됩니다. V8은 실행 중 코드의 실행 패턴을 관찰하여, 반복적으로 호출되는 함수나 특정 조건에서 자주 실행되는 코드를 감지하면, 이를 더욱 빠르게 실행하기 위해 최적화 컴파일러인 TurboFan을 동작시킵니다. 이 과정을 통해 바이트코드는 기계어 수준의 네이티브 코드로 JIT(Just-In-Time) 컴파일되며, 성능이 크게 향상됩니다.

이제부터는 이러한 JavaScript 실행 과정 뒤에 숨어 있는 V8 내부 구조를 조금 더 들여다보겠습니다.

2.2 V8 엔진 소개

image.png

V8이라는 이름은 고성능 자동차 엔진에서 따온 것입니다. Chrome 팀은 자신들이 만든 JavaScript 엔진이 마치 V8 엔진처럼 빠르고 강력하길 바라는 마음에서 이 이름을 붙였습니다. 실제로 V8은 단순한 인터프리터가 아니라, 다양한 최적화 기술이 결합된 복잡하고 정교한 실행 엔진입니다.

앞에서도 언급했듯이, JavaScript는 동적 타이핑 언어이기 때문에 실행 시점까지 변수의 타입이나 객체의 구조를 정확히 알 수 없습니다. 이는 개발자 입장에서 매우 유연하고 편리하지만, 엔진 입장에서는 성능 최적화와 보안 측면에서 매우 큰 도전 과제가 됩니다. 이러한 문제를 해결하기 위해 V8은 다단계 실행 파이프라인을 도입했습니다. 초기에는 바이트코드 인터프리터 없이, 곧바로 JIT 최적화 컴파일러인 Crankshaft를 사용해 JavaScript 코드를 빠르게 머신 코드로 변환했지만, 이는 다음과 같은 한계에 부딪혔습니다:

  • 모든 코드를 최적화하려다 보니 메모리 사용량이 많고 시작 속도가 느림
  • 동적 언어 특성상 최적화 실패 가능성이 높고, 이를 복구(deopt)하는 비용도 큼
  • ECMAScript 표준 발전에 따른 새로운 문법 지원이 어려움

이러한 이유로 Crankshaft는 유지 보수가 어려운 구조가 되었고, V8 팀은 이를 대체하기 위해 새로운 컴파일러 아키텍처를 설계합니다. 그 결과로 등장한 것이 바로 TurboFan입니다.

image.png

TurboFan의 등장 (v5.9 이후)

TurboFan은 Crankshaft를 대체하는 고급 최적화 컴파일러로, 다음과 같은 철학을 바탕으로 개발되었습니다:

  • 전체 JavaScript 언어 스펙을 지원
  • 중간 표현(IR)을 중심으로 분석과 최적화를 체계화
  • 다양한 플랫폼과 아키텍처에 대해 확장성과 이식성 확보

TurboFan은 강력한 최적화 기능을 제공했지만, 여전히 최적화까지 도달하기 위한 시간과 비용이 문제였습니다. 특히 브라우저에서 수많은 스크립트가 짧게 실행되는 경우, TurboFan은 과한 무기였던 셈입니다.

SparkPlug의 도입 (2021)

이 문제를 해결하기 위해, V8 팀은 중간 단계의 경량 JIT 컴파일러인 SparkPlug를 도입합니다. SparkPlug는 다음과 같은 목표로 설계되었습니다:

  • 바이트코드를 기반으로 하여, 이미 해석된 코드를 빠르게 컴파일
  • 타입 분석이나 복잡한 최적화를 생략하고, 빠른 코드 생성을 우선시
  • TurboFan보다 빠르지만 덜 최적화된 머신 코드를 생성

SparkPlug는 Ignition 인터프리터보다 빠르고, TurboFan보다 가벼운 실행을 제공함으로써, JavaScript 실행의 초기 단계에서 큰 효율을 가져왔습니다. 특히 페이지 로딩이나 초기 사용자 상호작용에 큰 도움이 되었습니다.

Maglev의 추가 (2022~)

SparkPlug의 성능은 나쁘지 않았지만, 여전히 중간급 성능과 낮은 최적화 수준에 머무는 한계가 있었습니다. V8 팀은 SparkPlug와 TurboFan 사이에 더 빠르면서도 더 똑똑한 컴파일러를 원했고, 그렇게 등장한 것이 바로 Maglev입니다.

Maglev는 다음과 같은 기술적 이유로 개발되었습니다:

  • SparkPlug보다 더 aggressive한 레지스터 기반 코드 생성
  • 타입 피드백을 어느 정도 반영하여 최적화된 코드 생성
  • 컴파일 속도는 SparkPlug와 비슷하지만, 실행 성능은 TurboFan에 근접

Maglev는 모바일과 같이 리소스가 제한된 환경에서 성능 최적화가 필요한 경우 특히 유용합니다. 또한, Tiering 구조에서 Ignition → Maglev → TurboFan으로 이어지는 계층적 실행 모델을 구축하여, 코드의 “핫함”(hot code)에 따라 동적으로 최적화 수준을 조절할 수 있게 했습니다.

2.3 V8 실행 파이프라인 구조

위 내용을 토대로 V8 실행 파이프라인 구조를 정리해보겠습니다.

image.png

가장 먼저 등장하는 것은 Ignition 인터프리터입니다. 여기서는 자바스크립트 소스를 바이트코드로 변환하고, 순차적으로 실행하면서 런타임 정보를 수집합니다. Ignition은 빠른 시작과 낮은 메모리 소비를 목표로 설계되었으며, 크롬 v5.9에서 기존의 Full-codegen을 대체했습니다. 하지만 인터프리터만으로는 고성능을 기대하기 어렵기 때문에, 일정 기준을 넘는 “핫 코드(hot code)”는 이후 JIT 컴파일러 단계로 진입하게 됩니다.

이때 등장하는 것이 바로 SparkPlug입니다. SparkPlug는 바이트코드를 해석한 결과를 바탕으로, 복잡한 타입 분석이나 최적화 없이 빠르게 네이티브 코드로 컴파일합니다. 이는 실행 속도를 높이고, 동시에 CPU 자원과 메모리 사용량도 절약할 수 있는 효율적인 중간 단계입니다. SparkPlug는 페이지 로딩처럼 빠른 반응이 필요한 시점에서 가볍고 즉각적인 실행 성능을 확보하는 데 최적화되어 있습니다.

하지만 때로는 더 높은 최적화가 필요합니다. 이때 등장하는 것이 V8의 고성능 컴파일러인 TurboFan입니다. TurboFan은 Ignition이나 SparkPlug 단계에서 수집된 타입 피드백, 호출 패턴, 루프 구조 등의 정보를 활용해, 고급 최적화를 수행합니다. 함수 인라이닝, 루프 전개, 타입 특화 등 다양한 기법을 적용하여, 정적으로 작성된 언어 못지않은 성능의 머신 코드를 생성합니다. 한 번 최적화된 코드는 캐시되어 이후 호출 시 빠르게 실행되므로, 반복적으로 호출되는 코드에서는 TurboFan의 효과가 매우 큽니다.

그런데 TurboFan은 성능은 뛰어나지만 컴파일 비용이 크고, 최적화가 실패(deopt)할 경우 오히려 성능이 저하될 수 있습니다. 이를 보완하기 위해 등장한 것이 Maglev입니다. Maglev는 SparkPlug와 TurboFan 사이의 성능-컴파일 시간 균형을 맞추기 위해 설계된 최신 JIT 컴파일러입니다. SparkPlug보다 더 정교하게 레지스터를 다루며, 부분적인 타입 정보를 활용한 최적화를 수행하지만, TurboFan만큼 무겁지는 않습니다. 2022년부터 점진적으로 도입된 Maglev는 특히 모바일과 같이 자원이 제한된 환경에서 탁월한 성능을 발휘하며, 다양한 실행 시나리오에 유연하게 대응할 수 있는 새로운 중간 계층을 형성합니다.

image.png

요약하자면, V8의 실행 파이프라인은 단일한 JIT 컴파일러가 아닌, 상황에 따라 다른 전략을 선택할 수 있는 유연한 구조로 이루어져 있습니다. 코드를 실행할 때 처음부터 모든 비용을 들이지 않고, 필요한 만큼만 투자하면서 점점 성능을 끌어올리는 구조죠. 이 계층화된 실행 체계 덕분에 V8은 빠른 시작, 낮은 메모리 소비, 높은 실행 성능이라는 세 마리 토끼를 동시에 잡을 수 있게 되었습니다.


이 구조 덕분에 JavaScript는 인터프리터 언어임에도 불구하고 네이티브 수준에 가까운 성능을 낼 수 있게 되었으며, 개발자는 별도의 성능 튜닝 없이도 유연한 코드를 빠르게 실행할 수 있게 되었습니다.

image.png

하지만 이렇게 정교한 실행 파이프라인은 동시에 보안 취약점의 가능성도 내포하고 있습니다. V8의 최적화는 대부분 “이 객체는 앞으로도 같은 구조일 것이다”, “이 함수는 항상 같은 방식으로 호출될 것이다”와 같은 가정에 기반하여 이루어집니다. 그리고 이러한 가정이 깨지는 순간, V8은 잘못된 전제 위에서 메모리 접근이나 객체 해석을 수행하게 되고, 바로 이 지점에서 대표적인 보안 취약점인 Type Confusion이 발생할 수 있습니다. Type Confusion은 말 그대로, 객체나 값의 타입에 대해 엔진이 잘못된 추론을 수행하거나, 기존 가정과 다른 타입으로 전환되었음에도 불구하고 이를 인식하지 못한 채 코드를 실행하는 상황을 의미합니다. 예를 들어, 초기에는 단순한 정수 배열로 취급되던 객체가 런타임 도중 복잡한 객체 배열로 바뀌었음에도, V8이 여전히 이를 정수 배열로 간주하고 최적화된 코드를 실행한다면, 이는 잘못된 메모리 참조나 내부 구조 손상으로 이어질 수 있습니다.

이러한 상황은 특히 TurboFan이나 Maglev와 같은 고급 JIT 컴파일러가 수행하는 타입 특화(type specialization), 인라이닝(inlining), 객체 구조 고정(hidden class assumption) 등의 최적화 중에서 발생하기 쉽습니다. 성능을 극대화하기 위해 타입이나 구조를 고정한 채 코드를 생성하게 되면, 그 이후의 예외적인 실행 흐름이나 구조 변화는 고려되지 않아 취약한 지점이 만들어지는 것입니다. 예를 들어 공격자는 일부 함수를 반복적으로 호출하여 엔진이 해당 패턴을 “안전하다”고 판단하게 만든 뒤, 특정 시점에 의도적으로 타입이나 구조를 변조하여, V8이 생성한 Native Code의 잘못된 동작을 유도할 수 있습니다. 이는 실제로 다수의 Chrome 취약점에서 악용된 방식이며, 최근까지도 V8 기반 Type Confusion 취약점은 고위험군에 해당하는 공격 벡터로 분류되고 있습니다.

이제 V8 내부에서 코드가 어떻게 실행되는지 기본 구조를 이해했으니, 다음 단계에서는 이 엔진 내부에서 객체가 어떻게 표현되고, 최적화되며, 나아가 이 구조가 어떻게 Type Confusion과 같은 보안 이슈로 이어질 수 있는지를 알아보겠습니다. 그래서 한호흡 끊고 가는게 좋겠죠? ㅎ.ㅎ 그럼 다음편을 예고하며 저는 Part 2로 돌아오겠습니다.

  • Part 2 예고)

image.png

Reference

v Conference Video

v V8 Engine