Web browsers are our primary gateway to the Internet. Currently, they play a crucial role in modern organizations, as a growing number of software applications are provided to users through browsers in the form of web applications. Virtually any activity you perform online requires the use of a browser, making them one of the most widely used software products by consumers worldwide.

Being the primary gateways to the Internet, browsers also represent a significant security risk for personal devices. Their extensive functionality and constant network connection make them attractive targets for cybercriminals. Additionally, the complexity of their code and the need to process potentially malicious content makes them vulnerable to various forms of attack, as we will see later.

Why Chrome?

Mainly because it is the most widely used browser in the world (around 70% market share according to some articles).

This article will introduce this technology with the goal of providing a knowledge base so you can research further on your own. To avoid making this article excessively long, several links will be referenced, and their reading is highly recommended.

JavaScript for dummies

Let’s start with the basics. JavaScript is a dynamically typed language, which means that data types are determined at runtime, unlike languages like C++ where types are defined at compile time. This allows any object in JavaScript to modify its properties flexibly during program execution. This concept is very well explained by Jhalon on his blog. Additionally, it’s one of the resources where I’ve learned the most about this topic.

We’ll use his example, imagine a variable with value 42:

var item = 42;

By using the typeof operator on the item variable, we can see that it returns its data type, which will be Number.

typeof item
'number'

Now, what would happen if we tried to set it to a string and then check its data type?

item = "Hello!";
typeof item
'string'

If we analyze this, the item variable is now set to the String data type and not Number. This is what makes JavaScript dynamic by nature. Unlike C++, if we tried to create an integer variable (int) and then tried to set it to a string, it would fail.

Another important point to explain is objects:

In JavaScript, objects are a collection of properties that are stored as key-value pairs. Each object has associated properties, which can simply be explained as a variable that helps define the object’s characteristics. Let’s look at an example:

let person = {
    name: "Juan",
    lastName: "Pérez",
    age: 30,
    gender: "male",
    nationality: "Italian"
};

Additionally, each object property is assigned property attributes, which are used to define and explain the state of object properties. You can consult property attributes here:

Now that we know the basics about JavaScript, let’s see what happens when the code is executed.

How the JavaScript engine works

Engines are those programs responsible for converting high-level code (JavaScript, Python, C) to low-level code (Machine Code, Bytecode). Each browser has its own engine to compile and interpret JavaScript.

Browsers today use numerous different JavaScript engines, such as:

  • V8: Google’s high-performance, open-source JavaScript and WebAssembly engine, used in Chrome.
  • SpiderMonkey: Mozilla’s JavaScript and WebAssembly engine, used in Firefox.
  • Chakra: a proprietary JScript engine developed by Microsoft for use in IE and Edge.
  • JavaScriptCore: Apple’s built-in JavaScript engine for WebKit use in Safari.

During my research, I found this very interesting blog about exploiting vulnerabilities in Firefox:

Why are JavaScript engines and all their complexity necessary?

JavaScript is an object-oriented, lightweight, interpreted programming language. In interpreted languages, code is executed line by line, and its result is obtained immediately, without needing to compile it previously into another format before the browser processes it. However, this approach usually negatively affects performance. This is where compilation comes into play, specifically the Just-In-Time (JIT) technique. With JIT, JavaScript code is translated into bytecode, an intermediate representation of machine code, and then optimized to make it much more efficient and, therefore, faster.

If you want to learn more about JIT, you can read the following article:

So, what really happens after executing JavaScript code?

Diagram of JavaScript execution flow showing Parser, AST, Interpreter, and Compiler

What does each of these components do?

  • Parser: once we execute the JavaScript code, the code is passed to the engine and we begin our first step, which is to parse the code. The parser converts the code into Tokens. For example, var num = 1 is broken down into var,num,=,1 and each token or element is labeled with its type, so in this case it would be Keyword,Identifier,Operator,Number.
  • AST (Abstract Syntax Tree): Once we have the code converted to tokens, these are converted into an AST. Using the previous code, it would look like this:
{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 7,
            "name": "num"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"

This can be verified using the following tool: AST Explorer.

  • Interpreter: The AST is passed to the interpreter, which is responsible for generating and executing bytecode. The example code would look like this:

Bytecode generated by the V8 interpreter

The following tutorial explains how to do it: How to Generate the Bytecode of Your JavaScript Code

You can find a list of V8 instructions here: JavaScript Bytecode – v8 Ignition Instructions

Finally, if you want to understand bytecode, I recommend the following article: Understanding V8’s Bytecode

  • Compiler: The compiler works ahead of time using an algorithm called Profiler, which monitors and watches the code that needs to be optimized. If something known as a hot function exists, the compiler takes that function and generates optimized machine code to execute. Otherwise, if it sees that a hot function that was optimized is no longer being used, it will deoptimize it and convert it back to bytecode.

The Profiler is a tool that monitors the behavior of JavaScript code in real time. Its main objective is to identify which parts of the code (particularly functions) are being used frequently and could benefit from additional optimization.

  • Performance monitoring: The Profiler is constantly watching which parts of the code are executed most frequently, called hot functions.
  • Hot functions: These are the functions that the Profiler detects are called repeatedly or have intensive resource usage. When it identifies a hot function, the Profiler tells the compiler to optimize that function.

As for Google’s V8 JavaScript engine, the compilation process is quite similar. However, V8 incorporates an additional non-optimizing compiler that was added in 2021. Currently, each V8 component has a specific name, which are as follows:

  • Ignition: V8’s fast low-level register-based interpreter that generates bytecode. You can find more details at: Firing up the Ignition interpreter.
  • SparkPlug: V8’s new non-optimizing JavaScript compiler that compiles from bytecode, iterating through the bytecode and emitting machine code for each bytecode as it’s visited.
  • TurboFan: V8’s optimizing compiler that translates bytecode into machine code with more numerous and sophisticated code optimizations. It also includes JIT (Just-In-Time) compilation.

If you want to understand TurboFan (one of the most important components), I recommend the following reading: Introduction to TurboFan.

Special mention to the Turbolizer tool, or if you prefer its online version: Turbolizer online.

According to the official repository, this tool visualizes the optimized code throughout the different phases of Turbofan’s optimization process, allowing easy navigation between source code, Turbofan’s IR graphs, scheduled IR nodes, and the generated assembly code.

A practical example would be starting from code like this:

JavaScript example code for analysis with Turbolizer

Generate the necessary file:

Command to generate the TurboFan trace file

And the tool would look like this:

Turbolizer interface showing optimized code visualization

Compatibility issues

V8 and Ignition are written in C++, so the interpreter and compiler must determine how JavaScript intends to use some of the data. This is fundamental for efficiency and security, as if the interpreter and compiler misinterpret the code, type confusion vulnerabilities can occur.

How does V8 handle this?

To solve the lack of type information in JavaScript and improve performance, V8 implements an optimization mechanism called hidden classes, inspired by the use of classes and structures in languages like C++.

What are Hidden Classes?

Hidden classes are internal structures automatically generated by V8 to track an object’s structure. Every time you create an object and add or modify a property, V8 generates an internal class that describes that object’s structure at that specific moment.

When creating an empty object obj, V8 associates that object with an initial hidden class that has no properties.

When property x is added, V8 creates a new hidden class that now contains property x.

let obj = {};
obj.x = 10;

When adding a new property y to obj, V8 generates a new hidden class that has properties x and y. This class is different from the previous one.

obj.y = 20;

This mechanism allows V8 to manage changes in object structure at runtime without losing the ability to optimize access to object properties.

If you want to delve deeper, you can rely on the following article:

Security in Chrome

To see the big picture and put all the pieces of the puzzle together, we must start by understanding Chrome’s security model. After all, this post is a tour of the browser’s internals and its exploitation.

As we know, JavaScript engines are an integral part of executing JavaScript code on systems. While they play an important role in making browsers fast and efficient, they can also expose them to failures, application crashes, and even security risks. But JavaScript engines aren’t the only part of a browser that can have problems or vulnerabilities. Many other components, such as APIs or the HTML and CSS rendering engines that are used, can also have stability issues and vulnerabilities that could be exploited, whether intentionally or not.

Security in Chrome can be summarized in two words: multi-process architecture and sandboxing.

Multi-process architecture and Sandboxing

Chromium’s multi-process architecture divides the browser into different types of processes to improve stability, security, and performance. Each tab or window operates in its own process, isolating the rendering engine from other components to prevent a failure from affecting the entire browser. Additionally, a sandbox is employed to restrict process access to system resources. This architecture also optimizes memory usage and allows freeing resources from inactive tabs.

Diagram of Chrome's multi-process architecture showing process separation

If you want to delve deeper into the topic, the following readings are recommended:

Common vulnerabilities

Now that we understand some concepts of the V8 development process and compiler optimizations, we can analyze what types of vulnerabilities are present in browsers. As we know, the JavaScript engine and all its components, including the compiler, are implemented in C++.

Among the most common vulnerabilities are the famous buffer overflows, heap overflows, use-after-free, off-by-one errors, and out-of-bound reads, among others.

Here’s a repository with CVEs:

In addition to the usual C++ errors, we can also have logic errors and machine code generation errors that can occur during the optimization phase due to the nature of speculative assumptions. These types of problems are known as type confusion, where the compiler doesn’t verify the type or shape of the object passed to it, causing the compiler to use the object blindly.

If you want to understand and analyze the exploitation of a real vulnerability, I recommend the following article by Samuel Gross:

In that article, the CVE-2018-17463 vulnerability, an RCE in Google Chrome, is explained.

Conclusion

The discovery and exploitation of vulnerabilities in browsers like Google Chrome is a complicated task, and it requires understanding in detail how each component works in order to know the best attack vectors. I hope this introduction has served to provide the reader with a knowledge base with which they can research further on their own.

Other interesting references