Introduction

This is the first part of a series on the chrome browser and its javascript engine V8. In this part I will cover general information about chrome's browser architecture, how V8 fits in and its general compilation pipeline to execute javascript code, and finally how to set up a suiteable debugging environment.

When I started getting into browser exploitation I ran into the issue that there weren't many well condensed resources out there, so I had to go through many individual blogposts until I eventually started getting a decent understanding of how a browser works. My goal with this series is to condense much of this information into these posts and provide a strong baseline for anyone interested in this topic.

Chrome Architecture

Unlike many other programs that you may be used to, the chrome browser uses multiple processes to achieve its functions. These individual processes then communicate with each other via IPC.

The first process is the browser process. It is the main process that is executed when you first start the browser and stays alive throughout the browser's entire lifetime. It is the central coordinator of all the processes, and operates at the highest privilege level available to the browser. It controls features such as the address bar, bookmarks and the back/forward/reload buttons. Since this is the most privileged process it does not trust the data given to it by any of the other processes. It does however handle privileged operations such as UI, networking or filesystem storage for the other processes when necessary.

The next process is the renderer process. This process controls anything inside the actual website tab. To be more precise, there are many individual renderer processes. One entirely separate process for each tab the browser currently has opened, and since 2018 it can even have a separate tab for each individual iframe used by a given website. I am going to talk a little more about this topic later in the section on Site Isolation. These renderer processes take care of parsing the site, drawing it on the users screen, and executing any relevant javascript. Chrome currently uses a renderer engine called Blink to take care of this. Since the renderer process is in charge of executing javascript it exposes a very large attack surface to potential hackers. Due to this it is heavily sandboxed. It has no filesystem access, can't execute OS calls, etc. Since the renderer is especially relevant for browser security we will be talking a lot more about this later.

Moving on we get to the plugin and extension processes. The plugin process controls any plugins that a website may be using (such as flash) while the extension process is in charge of managing browser extensions. This process allows users to customize their browser using custom plugins and it was a pretty impactful browser improvement when it was first introduced.

Finally there's the GPU process. This process attempts to offload certain operations to the GPU to release pressure from the CPU and increase the quality at which data is displayed to the user. This oftentimes handles tasks such as scrolling through websites.

At this point you may be wondering why on earth would a browser need this many processes. While it is true that this can lead to very high memory overhead (especially when multiple tabs start coming into play), it is also necessary for a browser to work as you would expect it to. The first reason for this is stability. If one of the websites you are using suddenly dies, chrome/you can just shutdown that individual tab, and continue operating the browser without issues. If the browser only used a single process, the entire browser would have to be shutdown in such a case. Another big reason is security. Since browsers are so widespread and are used to execute remote code, they provide a very interesting attack surface to malicious hackers. The current process layout allows the browser to sandbox security critical processes such as the renderer so that even if the renderer process is exploited, the attacker still does not have access to the underlying system.

This is where site isolation becomes very relevant. This feature was added to chrome in 2018. With it, not only tabs, but also individual iframes get their very own renderer process. A big security concern for browsers is that attackers might be able to abuse vulnerabilities to access other sites the user may have open at the same time. This was oftentimes a real possibility using iframes, however since the iframes now run in a new process entirely, iframes can no longer be used for attacks in this context.

Blink and V8

As mentioned earlier, chrome uses Blink as its rendering engine. It takes care of most of the things mentioned above for the renderer process. Blink then uses V8 as its javascript engine. Most major browsers have their own rendering and javascript engines.

1. Chrome: Renderer: Blink | Javascript: V8
2. Safari: Renderer: Webkit | Javascript: JavaScriptCore
3. Firefox: Renderer: Gecko | Javascript: SpiderMonkey

All of these engines have different implementations, however from a high level they essentially do the same. The Renderer parses the provided html and passes any javascript code it finds on to the browsers respective javascript engine. In this series we will be focusing entirely on chrome's implementations.

Compilation Pipeline

Lets start talking about what happens once the V8 engines gets access to the javascript code. It starts off by parsing the code and generating an Abstract Syntax Tree (AST) from it. This is basically a graph based representation of the source code that is easier to parse for a compiler than pure text. This AST is then passed on to Ignition. Ignition is V8's bytecode generator and bytecode interpreter. It first transforms the AST into bytecode, and then passes this bytecode on to its interpreter section which then starts executing the bytecode. At this point there is one major step left in the compilation pipeline. When some piece of code is frequently executed, the section's bytecode may be passed onto Turbofan. Turbofan is a just in time compiler (JIT) that takes the bytecode and transforms it into highly optimized machine code. This is much faster than what the interpreter can do, and thus results in much higher execution speeds. Turbofan makes some speculations when generating this machine code, if these end up not holding, it needs to throw away the optimized code and transfer execution back to the interpreter. The image below showcases this compilation pipeline. Do not worry too much about specifics yet. We will be covering each of these sections in much greater depth in future parts of the series.



V8 Timeline

2008:
V8 was initially created in 2008. At that point in time neither Ignition nor Turbofan existed. It started by generating an AST from the source code which it then passed on to its compiler, Codegen, which just output machine code.

2010:
At this point Crankshaft was added to the browser. Similar to today's Turbofan, Crankshaft was a just in time compiler. Its purpose was to generate highly optimized machine code. Unfortunately however, crankshaft had a lot of code generation issues. There were many popular code constructs it couldn't handle such as try/excepts, and thus programs oftentimes ran much slower than they potentially could. Crankshafts peak performances was not bad, however it also led to very unpredictable performance with certain code constructs and had a really slow worst case.

2014:
4 years later, TurboFan was finally introduced into V8. Its goal was to improve on Crankshafts weak points and generate consistent fast code. At this point in time both Crankshaft and Turbofan existed within the browser and user's could just their preference.

2016:
Ignition is added to the browser. One of its main purpose's was to relieve some of the memory pressure from Turbofan by producing compact bytecode for Turbofan to use instead of the large source code. At this point however the browser architecture was way too complex, which leads us to the next set of major changes.

2017:
This is the current modern browser architecture that chrome uses. Crankshaft and Codegen are both fully removed and Ignition/Turbofan take care of executing the code.



We will be diving much deeper into Ignition and Turbofan in future parts, however for now this should suffice to give you a decent understanding of chrome's general architecture. Let's set up our debugging environment!

Debugging Setup

I will be setting up my debugging environment on Linux. Feel free to use something else, however your setup process may differ.

Before we can start discussing any debugging we need to build the browser. Chrome however is huge and with its large amount of processes, debugging it effectively would prove to be very challenging. Since we only really care about chrome's V8 engine, we will just build V8 separately and work with that.

To setup V8, you can just copy paste the below commands into your terminal. Make sure to edit the path to reflect the path you are installing depot_tools on. This build process may take a while.

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo "export PATH=/opt/depot_tools:$PATH" >> ~/.bashrc
fetch v8
cd v8
./build/install-build-deps.sh
//git checkout <preferred_patch>
gclient sync

./tools/dev/V8gen.py x64.release
ninja -C ./out.gn/x64.release # Release version

./tools/dev/V8gen.py x64.debug
ninja -C ./out.gn/x64.debug # Debug version

At this point you should have both a V8 release and debug binary. You do not necessarily need both if space is a concern, however the debug binary enables some extra debug flags that will be useful later. These binaries will be called d8, and should spawn an interactive javascript shell when executed.

Lets go over some useful flags and debug functions that we will use throughout the next parts. Below you can find a list of 7 commandline flags that you can pass to the d8 binary when running it to retrieve extra information. We will be using all of them in various sections in future parts.

--allow-natives-syntax
Enabled various debug-functions in javascript. Going to cover 3 of them below

--print-bytecode --print-bytecode-filter=func
This flag is passed along to print out the bytecode generated for input

--print-ast
This flag prints out the initial abstract syntax tree. It requires the debug version of d8 to work

--print-opt-code/--print-opt-code-filter
This flag prints out the optimized code

--trace-turbo
This flag prints out the Sea of Nodes graph. We will make extensive use of this in part 4

--shell
You can also pass a .js script to the d8 shell instead of using the interactive shell. Using the --shell command gives you an interactive shell after completion of the script

--log-function-events
This flag provides extensive logs of V8's activities

There are many more possible flags that can be passed to d8, however the above should suffice for now. Lets quickly cover some of the native functions before finally debugging our first program.

%DebugPrint(obj)
%SystemBreak()
%PrepareFunctionForOptimization(func)
%OptimizeFunctionOnNextCall(func)

These functions get added to V8 as builtins when running d8 with the "--allow-natives-syntax" flag. %DebugPrint(obj) can be used to retrieve the address in memory of an object in addition to information about its type, values, properties, etc. It will be crucial when actually debugging javascript code. %SystemBreak() can be used to set a breakpoint in the js script. Finally %OptimizeFunctionOnNextCall(func) passes the given function on to Turbofan to produce optimized machine code. This is a very useful function when we are trying to debug various features of the JIT. Similarly %OptimizeFunctionOnNextCall(func) is used to prepare a function for optimization. This propagates inline caches used by turbofan for its optimizations.

Finally lets quickly look at 2 more tools/scripts to make our lives a little easier:

1. https://github.com/v8/v8/blob/master/tools/gdbinit / https://github.com/v8/v8/blob/master/tools/gdb-v8-support.py
2. https://github.com/v8/v8/tree/lkgr/tools/turbolizer

The 2 scripts linked under the first point are gdb helper scripts that make debugging v8 code a little easier and the turbolizer tool is a very useful tool that lets us visualize graphs used by V8's turbofan compiler. We will make use of this tool in the Turbofan part of this series.

Now that we have covered some useful debugging features, let's actually debug a small program. We can simply open up d8 in gdb and debug it similarly to any other binary. I will be using pwndbg, but feel free to use any gdb extension you prefer, or even base gdb.

We will be debugging the following binary. As you can see we perform some simple arithmetic operations and make use of 2 of the aforementioned debug functions to print out information about the array and set a breakpoint.

After opening up the binary in gdb, we will run it with: "run --allow-natives-syntax test.js". This will first print out the requested debug information for the array before breaking on the %SystemBreak(). There is a lot of information to unpack here so we will just ignore most of it for now. One field is particularly interesting to us right now: elements: 0x009d082934e1. This indicates that the arrays elements are stored at address 0x009d082934e1 - 1. We need to subtract a 1 from the address due to a memory management principle called pointer tagging that v8 uses to distinguish numbers and pointers. We will talk more about it in part 3 of this series when we talk more about V8's memory management. For now lets just move onwards assuming that this is the correct address.

In gdb we can execute "x/3xw 0x009d082934e1-1" to print out the processes memory at this address. You should see two values, 0x2 and 0x4. These represent our 2 array values 1 and 2. Once again, it's a little odd that we see 0x2 and 0x4 instead of 0x1 and 0x2. This is also due to pointer tagging.

If you are using the debug version you should also have access to source code debugging. At this point you could set an earlier breakpoint and step through your script and try to reason about the c++ code that v8 executes to handle your javascript.

You should now know enough to start debugging simple js scripts using gdb + d8. Most objects in V8 are quite complex, so don't worry if much of the information you see does not make sense yet. It should become a lot clearer in the third part when we cover memory.

In the next part we will talk more about Ignition and observe how it generates and executes its bytecode.