V8引擎漏洞分析环境与调试方法基础

Posted by GToad on July 25, 2019

本文为本博客V8系列第一篇,主要介绍一下基础的漏洞调试分析环境等。

V8环境搭建

depot_tools

本人使用Ubuntu16.04。

根据官方文档可知,先要进行deptools的安装。先git clone下项目。

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

再将其加入环境变量以方便执行:

export PATH=$PATH:/path/to/depot_tools

V8

然后进行V8源码的下载,需要ke学上网且稳定。

fetch v8
gclient sync

编译代码:

tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

调试方法

gdbinit_v8脚本

使用GDB进行调试,V8团队还写了个GDB的辅助调试脚本gdbinit_v8,可以从这里下载,也可以直接复制黏贴下方脚本,并在.gdbinit中加载。

# Copyright 2014 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Print tagged object.
define job
call (void) _v8_internal_Print_Object((void*)($arg0))
end
document job
Print a v8 JavaScript object
Usage: job tagged_ptr
end

# Print content of v8::internal::Handle.
define jh
call (void) _v8_internal_Print_Object(*((v8::internal::Object**)($arg0).location_))
end
document jh
Print content of a v8::internal::Handle
Usage: jh internal_handle
end

# Print content of v8::Local handle.
define jlh
call (void) _v8_internal_Print_Object(*((v8::internal::Object**)($arg0).val_))
end
document jlh
Print content of a v8::Local handle
Usage: jlh local_handle
end

# Print Code objects containing given PC.
define jco
call (void) _v8_internal_Print_Code((void*)($arg0))
end
document jco
Print a v8 Code object from an internal code address
Usage: jco pc
end

# Print LayoutDescriptor.
define jld
call (void) _v8_internal_Print_LayoutDescriptor((void*)($arg0))
end
document jld
Print a v8 LayoutDescriptor object
Usage: jld tagged_ptr
end

# Print TransitionTree.
define jtt
call (void) _v8_internal_Print_TransitionTree((void*)($arg0))
end
document jtt
Print the complete transition tree of the given v8 Map.
Usage: jtt tagged_ptr
end

# Print JavaScript stack trace.
define jst
call (void) _v8_internal_Print_StackTrace()
end
document jst
Print the current JavaScript stack trace
Usage: jst
end

# Print TurboFan graph node.
define pn
call _v8_internal_Node_Print((void*)($arg0))
end
document pn
Print a v8 TurboFan graph node
Usage: pn node_address
end

# Skip the JavaScript stack.
define jss
set $js_entry_sp=v8::internal::Isolate::Current()->thread_local_top()->js_entry_sp_
set $rbp=*(void**)$js_entry_sp
set $rsp=$js_entry_sp + 2*sizeof(void*)
set $pc=*(void**)($js_entry_sp+sizeof(void*))
end
document jss
Skip the jitted stack on x64 to where we entered JS last.
Usage: jss
end

# Print stack trace with assertion scopes.
define bta
python
import re
frame_re = re.compile("^#(\d+)\s*(?:0x[a-f\d]+ in )?(.+) \(.+ at (.+)")
assert_re = re.compile("^\s*(\S+) = .+<v8::internal::Per\w+AssertScope<v8::internal::(\S*), (false|true)>")
btl = gdb.execute("backtrace full", to_string = True).splitlines()
for l in btl:
  match = frame_re.match(l)
  if match:
    print("[%-2s] %-60s %-40s" % (match.group(1), match.group(2), match.group(3)))
  match = assert_re.match(l)
  if match:
    if match.group(3) == "false":
      prefix = "Disallow"
      color = "\033[91m"
    else:
      prefix = "Allow"
      color = "\033[92m"
    print("%s -> %s %s (%s)\033[0m" % (color, prefix, match.group(2), match.group(1)))
end
end
document bta
Print stack trace with assertion scopes
Usage: bta
end

# Search for a pointer inside all valid pages.
define space_find
  set $space = $arg0
  set $current_page = $space->first_page()
  while ($current_page != 0)
    printf "#   Searching in %p - %p\n", $current_page->area_start(), $current_page->area_end()-1
    find $current_page->area_start(), $current_page->area_end()-1, $arg1
    set $current_page = $current_page->next_page()
  end
end

define heap_find
  set $heap = v8::internal::Isolate::Current()->heap()
  printf "# Searching for %p in old_space  ===============================\n", $arg0
  space_find $heap->old_space() ($arg0)
  printf "# Searching for %p in map_space  ===============================\n", $arg0
  space_find $heap->map_space() $arg0
  printf "# Searching for %p in code_space ===============================\n", $arg0
  space_find $heap->code_space() $arg0
end
document heap_find
Find the location of a given address in V8 pages.
Usage: heap_find address
end

set disassembly-flavor intel
set disable-randomization off

# Install a handler whenever the debugger stops due to a signal. It walks up the
# stack looking for V8_Dcheck and moves the frame to the one above it so it's
# immediately at the line of code that triggered the DCHECK.
python
def dcheck_stop_handler(event):
  frame = gdb.selected_frame()
  select_frame = None
  message = None
  count = 0
  # limit stack scanning since they're usually shallow and otherwise stack
  # overflows can be very slow.
  while frame is not None and count < 5:
    count += 1
    if frame.name() == 'V8_Dcheck':
      frame_message = gdb.lookup_symbol('message', frame.block())[0]
      if frame_message:
        message = frame_message.value(frame).string()
      select_frame = frame.older()
      break
    if frame.name() is not None and frame.name().startswith('V8_Fatal'):
      select_frame = frame.older()
    frame = frame.older()

  if select_frame is not None:
    select_frame.select()
    gdb.execute('frame')
    if message:
      print('DCHECK error: {}'.format(message))

gdb.events.stop.connect(dcheck_stop_handler)
end

# Code imported from chromium/src/tools/gdb/gdbinit
python

import os
import subprocess
import sys

compile_dirs = set()


def get_current_debug_file_directories():
  dir = gdb.execute("show debug-file-directory", to_string=True)
  dir = dir[
      len('The directory where separate debug symbols are searched for is "'
         ):-len('".') - 1]
  return set(dir.split(":"))


def add_debug_file_directory(dir):
  # gdb has no function to add debug-file-directory, simulates that by using
  # `show debug-file-directory` and `set debug-file-directory <directories>`.
  current_dirs = get_current_debug_file_directories()
  current_dirs.add(dir)
  gdb.execute(
      "set debug-file-directory %s" % ":".join(current_dirs), to_string=True)


def newobj_handler(event):
  global compile_dirs
  compile_dir = os.path.dirname(event.new_objfile.filename)
  if not compile_dir:
    return
  if compile_dir in compile_dirs:
    return
  compile_dirs.add(compile_dir)

  # Add source path
  gdb.execute("dir %s" % compile_dir)

  # Need to tell the location of .dwo files.
  # https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
  # https://crbug.com/603286#c35
  add_debug_file_directory(compile_dir)

# Event hook for newly loaded objfiles.
# https://sourceware.org/gdb/onlinedocs/gdb/Events-In-Python.html
gdb.events.new_objfile.connect(newobj_handler)

gdb.execute("set environment V8_GDBINIT_SOURCED=1")

end

脚本指令

找到想要调试的JS脚本,通过在脚本中事先添加两种代码来辅助之后的调试过程:

  1. %DebugPrint(arg1)——该方法会打印目标对象的内存地址并对其主要信息进行输出。
  2. %SystemBreak()——该方法可以在脚本中下断点。

%DebugPrint方法输出例子如下:

DebugPrint: 0x36922172ba11: [Function]
 - map = 0x2c928df840f1 [FastProperties]
 - prototype = 0x3692217040b9
 - elements = 0x21c960102241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
 - initial_map = 
 - shared_info = 0x36922172b4c9 <SharedFunctionInfo Check>
 - name = 0x36922172aff9 <String[5]: Check>
 - formal_parameter_count = 0
 - context = 0x369221703951 <FixedArray[235]>
 - literals = 0x21c960104b21 <FixedArray[1]>
 - code = 0x3e808c684481 <Code: BUILTIN>
 - properties = {
   #length: 0x21c9601555d9 <AccessorInfo> (accessor constant)
   #name: 0x21c960155649 <AccessorInfo> (accessor constant)
   #arguments: 0x21c9601556b9 <AccessorInfo> (accessor constant)
   #caller: 0x21c960155729 <AccessorInfo> (accessor constant)
   #prototype: 0x21c960155799 <AccessorInfo> (accessor constant)
 }
0x2c928df840f1: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 72
 - inobject properties: 0
 - elements kind: FAST_HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - callable
 - constructor
 - back pointer: 0x21c960102311 <undefined>
 - instance descriptors (own) #5: 0x36922170ac59 <FixedArray[17]>
 - layout descriptor: 0
 - prototype: 0x3692217040b9 <JS Function (SharedFunctionInfo 0x21c960107ec9)>
 - constructor: 0x36922170ab39 <JS Function Function (SharedFunctionInfo 0x21c96013b631)>
 - code cache: 0x21c960102241 <FixedArray[0]>
 - dependent code: 0x21c960102241 <FixedArray[0]>
 - construction counter: 0

GDB指令

使用GDB对编译出来的d8文件进行调试,并设定参数set args --natives-syntax /path/to/jsscript.js使其加载指定的JS脚本。

然后使用r命令运行后,脚本就会暂停在%SystemBreak()处,并且已经执行的%DebugPrint()方法会输出指定对象的信息。

使用gdbinit_v8中的job指令可以对指定内存地址上的对象进行信息输出。

pwndbg> job 0x36922172ba11
0x36922172ba11: [Function]
 - map = 0x2c928df840f1 [FastProperties]
 - prototype = 0x3692217040b9
 - elements = 0x21c960102241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
 - initial_map = 
 - shared_info = 0x36922172b4c9 <SharedFunctionInfo Check>
 - name = 0x36922172aff9 <String[5]: Check>
 - formal_parameter_count = 0
 - context = 0x369221703951 <FixedArray[235]>
 - literals = 0x21c960104b21 <FixedArray[1]>
 - code = 0x3e808c684481 <Code: BUILTIN>
 - properties = {
   #length: 0x21c9601555d9 <AccessorInfo> (accessor constant)
   #name: 0x21c960155649 <AccessorInfo> (accessor constant)
   #arguments: 0x21c9601556b9 <AccessorInfo> (accessor constant)
   #caller: 0x21c960155729 <AccessorInfo> (accessor constant)
   #prototype: 0x21c960155799 <AccessorInfo> (accessor constant)

注意,V8中object的地址+1才会被理解为object,否则末尾bit为0的话会被理解为SMI。具体JS引擎这么设计的原理可以参考这个PPT,这里就不多介绍了。

使用telescope查看指定内存的数据情况,此处由于不涉及对象的理解,直接使用实际地址即可。也可以使用GDB自带的x/命令,个人比较喜欢后者。

pwndbg> telescope 0x36922172ba10
00:0000│   0x36922172ba10 —▸ 0x2c928df840f1 ◂— 0x90000037e29e022
01:0008│   0x36922172ba18 —▸ 0x21c960102241 ◂— 0x37e29e023
... ↓
03:0018│   0x36922172ba28 —▸ 0x21c960102351 ◂— 0xff0000037e29e025
04:0020│   0x36922172ba30 —▸ 0x36922172b4c9 ◂— 0x810000037e29e028
05:0028│   0x36922172ba38 —▸ 0x369221703951 ◂— 0x37e29e02c
06:0030│   0x36922172ba40 —▸ 0x21c960104b21 ◂— 0x37e29e023
07:0038│   0x36922172ba48 —▸ 0x3e808c6844e0 ◂— mov    r8, qword ptr [rdi + 0x1f] /* 0x17408b4d1f478b4c */

漏洞版本

之前编译的V8版本可能不是已知存在漏洞的版本,为了验证V8的漏洞,我们需要去查看漏洞信息,找到修复该漏洞的版本,然后记下该版本的上一个版本。最后使用git checkout命令来切换后,重新进行编译。例如CVE-2016-5198

git checkout a7a350012c05f644f3f373fb48d7ac72f7f60542
gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

使用编译后的d8对漏洞的POC进行运行,如果能够达到异常的效果则说明版本对了,然后进一步使用上面的方法进行调试。

JIT优化

由于最近大部分漏洞会需要触发JIT优化,因此,可以使用--print-code来直接获得JIT优化前后的机器代码。例如:

--- Raw source ---

() {
    n.xyz = 0x826852f4;
    parseInt('AAAAAAAA');
}


--- Code ---

source_position = 54
kind = FUNCTION
name = Check
compiler = full-codegen
Instructions (size = 220)
0x14161c9069c0     0  55             push rbp
0x14161c9069c1     1  4889e5         REX.W movq rbp,rsp
0x14161c9069c4     4  56             push rsi
0x14161c9069c5     5  57             push rdi
0x14161c9069c6     6  488b4f2f       REX.W movq rcx,[rdi+0x2f]
0x14161c9069ca    10  488b490f       REX.W movq rcx,[rcx+0xf]
0x14161c9069ce    14  83411b01       addl [rcx+0x1b],0x1
0x14161c9069d2    18  493ba5600c0000 REX.W cmpq rsp,[r13+0xc60]
0x14161c9069d9    25  7305           jnc 32  (0x14161c9069e0)
0x14161c9069db    27  e800bff5ff     call StackCheck  (0x14161c8628e0)    ;; code: BUILTIN
0x14161c9069e0    32  48b80000000002000000 REX.W movq rax,0x200000000
0x14161c9069ea    42  e8f1d9ffff     call 0x14161c9043e0     ;; code: LOAD_GLOBAL_IC
0x14161c9069ef    47  50             push rax
0x14161c9069f0    48  48b8a1c36a3a94050000 REX.W movq rax,0x5943a6ac3a1    ;; object: 0x5943a6ac3a1 <Number: 2.18788e+09>
0x14161c9069fa    58  5a             pop rdx
0x14161c9069fb    59  48b919b06a3a94050000 REX.W movq rcx,0x5943a6ab019    ;; object: 0x5943a6ab019 <String[3]: xyz>
0x14161c906a05    69  48bf0000000004000000 REX.W movq rdi,0x400000000
0x14161c906a0f    79  e84cb8f0ff     call 0x14161c812260     ;; code: STORE_IC
0x14161c906a14    84  488b75f8       REX.W movq rsi,[rbp-0x8]
0x14161c906a18    88  48b80000000008000000 REX.W movq rax,0x800000000
0x14161c906a22    98  e8b9d9ffff     call 0x14161c9043e0     ;; code: LOAD_GLOBAL_IC
0x14161c906a27   103  50             push rax
0x14161c906a28   104  49ba112328f6373d0000 REX.W movq r10,0x3d37f6282311    ;; object: 0x3d37f6282311 <undefined>
0x14161c906a32   114  4152           push r10
0x14161c906a34   116  49ba39b06a3a94050000 REX.W movq r10,0x5943a6ab039    ;; object: 0x5943a6ab039 <String[8]: AAAAAAAA>
0x14161c906a3e   126  4152           push r10
0x14161c906a40   128  48ba0000000006000000 REX.W movq rdx,0x600000000
0x14161c906a4a   138  488b7c2410     REX.W movq rdi,[rsp+0x10]
0x14161c906a4f   143  b801000000     movl rax,0x1
0x14161c906a54   148  e8e7ddffff     call 0x14161c904840     ;; code: CALL_IC
0x14161c906a59   153  488b75f8       REX.W movq rsi,[rbp-0x8]
0x14161c906a5d   157  4883c408       REX.W addq rsp,0x8
0x14161c906a61   161  498b45a0       REX.W movq rax,[r13-0x60]
0x14161c906a65   165  48bb81c46a3a94050000 REX.W movq rbx,0x5943a6ac481    ;; object: 0x5943a6ac481 Cell for 6144
0x14161c906a6f   175  83430bd1       addl [rbx+0xb],0xd1
0x14161c906a73   179  791f           jns 212  (0x14161c906a94)
0x14161c906a75   181  50             push rax
0x14161c906a76   182  e8e5bdf5ff     call InterruptCheck  (0x14161c862860)    ;; code: BUILTIN
0x14161c906a7b   187  58             pop rax
0x14161c906a7c   188  48bb81c46a3a94050000 REX.W movq rbx,0x5943a6ac481    ;; object: 0x5943a6ac481 Cell for 6144
0x14161c906a86   198  49ba0000000000180000 REX.W movq r10,0x180000000000
0x14161c906a90   208  4c895307       REX.W movq [rbx+0x7],r10
0x14161c906a94   212  c9             leavel
0x14161c906a95   213  c20800         ret 0x8

--- Optimized code ---

optimization_id = 1
source_position = 54
kind = OPTIMIZED_FUNCTION
name = Check
stack_slots = 5
compiler = crankshaft
Instructions (size = 186)
0x14161c906c40     0  55             push rbp
0x14161c906c41     1  4889e5         REX.W movq rbp,rsp
0x14161c906c44     4  56             push rsi
0x14161c906c45     5  57             push rdi
0x14161c906c46     6  4883ec08       REX.W subq rsp,0x8
0x14161c906c4a    10  488b45f8       REX.W movq rax,[rbp-0x8]
0x14161c906c4e    14  488945e8       REX.W movq [rbp-0x18],rax
0x14161c906c52    18  488bf0         REX.W movq rsi,rax
0x14161c906c55    21  493ba5600c0000 REX.W cmpq rsp,[r13+0xc60]
0x14161c906c5c    28  7305           jnc 35  (0x14161c906c63)
0x14161c906c5e    30  e87dbcf5ff     call StackCheck  (0x14161c8628e0)    ;; code: BUILTIN
0x14161c906c63    35  48b879bd6a3a94050000 REX.W movq rax,0x5943a6abd79    ;; object: 0x5943a6abd79 PropertyCell for 0x20753df547c1 <a Set with map 0x316d38a8c391>
0x14161c906c6d    45  488b400f       REX.W movq rax,[rax+0xf]
0x14161c906c71    49  49ba0000805e0a4de041 REX.W movq r10,0x41e04d0a5e800000
0x14161c906c7b    59  c4c1f96ec2     vmovq xmm0,r10
0x14161c906c80    64  488b4007       REX.W movq rax,[rax+0x7]
0x14161c906c84    68  488b400f       REX.W movq rax,[rax+0xf]
0x14161c906c88    72  c5fb114007     vmovsd [rax+0x7],xmm0
0x14161c906c8d    77  49ba112328f6373d0000 REX.W movq r10,0x3d37f6282311    ;; object: 0x3d37f6282311 <undefined>
0x14161c906c97    87  4152           push r10
0x14161c906c99    89  49ba39b06a3a94050000 REX.W movq r10,0x5943a6ab039    ;; object: 0x5943a6ab039 <String[8]: AAAAAAAA>
0x14161c906ca3    99  4152           push r10
0x14161c906ca5   101  48bf51d8683a94050000 REX.W movq rdi,0x5943a68d851    ;; object: 0x5943a68d851 <JS Function parseInt (SharedFunctionInfo 0x3d37f62bce11)>
0x14161c906caf   111  488b75e8       REX.W movq rsi,[rbp-0x18]
0x14161c906cb3   115  488b7727       REX.W movq rsi,[rdi+0x27]
0x14161c906cb7   119  498b55a0       REX.W movq rdx,[r13-0x60]
0x14161c906cbb   123  b801000000     movl rax,0x1
0x14161c906cc0   128  bb02000000     movl rbx,0x2
0x14161c906cc5   133  e836efefff     call ArgumentsAdaptorTrampoline  (0x14161c805c00)    ;; code: BUILTIN
0x14161c906cca   138  48b8112328f6373d0000 REX.W movq rax,0x3d37f6282311    ;; object: 0x3d37f6282311 <undefined>
0x14161c906cd4   148  488be5         REX.W movq rsp,rbp
0x14161c906cd7   151  5d             pop rbp
0x14161c906cd8   152  c20800         ret 0x8
0x14161c906cdb   155  90             nop

可见优化后的代码去除了一些“冗余”检查,也带来了一些安全问题。

参考

Attacking Client-Side JIT Compilers StarCTF-Freebuf