作者:Carl Yu@墨云科技VLab Team
原文鏈接:https://mp.weixin.qq.com/s/PY_QiNgEk9F3nSSgxECfTg

10月28日,谷歌Chrome在發布95.0.4638.69版本時修復了天府杯上昆侖實驗室提交的漏洞CVE-2021-38001。由于此漏洞的PoC非常簡潔使得作者對V8引擎產生了強烈的興趣,分析此漏洞也是作者對V8的一次學習。V8是谷歌用C++編寫的JavaScript和WebAssembly引擎,在Chrome和Node.js中都有使用。

內聯緩存

該漏洞與內聯緩存(Inline Caching)有關,內聯緩存是一種運行時環境(runtime environment)的優化技巧。由于動態語言必須在運行時進行方法綁定(method binding),此優化手法對于動態語言來說十分重要,舉一個例子:

def foo(a,b):
  a.func(b)

這段代碼的Python bytecode如下:

Disassembly of <code object foo at 0x000000000356EEA0, file "<dis>", line 2>:
  3           0 LOAD_FAST                0 (a)
              2 LOAD_METHOD              0 (func)
              4 LOAD_FAST                1 (b)
              6 CALL_METHOD              1
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

在執行時,LOAD_METHOD會去確認a的類型,然后利用a的類型尋找add

如果沒有IC,那第二次執行a.func(b)時就必須重復做同樣的事情(在同樣的context下)。這樣做邏輯上是比較嚴謹的,但是執行效率會很低,那么有沒有什么方法可以提速呢?其實,程序員寫代碼時,大概率會寫成下面的形式:

def foo(a,...):
  a.func(b)
  a.func(c)
  ...
  a.func(z)

在上面代碼中,a的類型是不變的。Deutsch和Schiffman在他們的文章(http://web.cs.ucla.edu/~palsberg/course/cs232/papers/DeutschSchiffman-popl84.pdf)中提到:'在代碼執行的某個時點,接收者(receiver)的類型通常和上次此時點的類型一樣'。比如說上面例子中,a的類型并未發生變化,所以這里可以將a的類型進行緩存以便后面使用。

V8使用的是Data-driven IC,這種IC將屬性的加載存儲信息編碼成數據結構。其他函數(例如LoadICStoreIC)會讀取這個結構然后執行相應的操作。以下是V8之前的Patching IC和現在的Data-Driven IC的區別。

這里右邊的圖中的FeedbackVector的功能是記錄和管理所有執行反饋,此數據結構對于JavaScript的執行效率提升十分關鍵。同時,在圖中可以發現有Fast-path,Slow-path和Miss。Miss很好理解,即需要運行時確認類型。那么Slow-path和Fast-path分別對應了什么情況呢?通過以下例子可以理解:

let a = {foo:3}
let b = {foo:4}

這里ab的架構一樣,在處理上就沒有必要為這兩個對象建立不同的架構。V8的處理方式是將對象的架構與值分成對象的形狀(Object Shapes)和一個帶有值的vector,對象形狀在V8中被稱為Maps。上面例子中,V8會先創造一個形狀Map[a]。此形狀擁有屬性foo位于偏移0,在對應vector[0]的值為3。在創建對象b的時候,只需將b的Map指向Map[a],然后讓對應的vector[0]=4即可。這個即為Fast-path。

假設后面是

a.foo1 = 4

那么V8會新建一個Map[a1]并將a的Maps改為Map[a1]Map[a1]擁有屬性foo1位于偏移1并指向Map[a],同時將對應的vector[1]設為4。即為Slow-path。

以下例子將會包含以上三種情況:

function load(a) {
  return a.key;
}
//IC of load: [{ slot: 0, icType: LOAD, value: UNINIT }]
let first = { key: 'first' } // shape A
let fast = { key: 'fast' }   // the same shape A
let slow = { foo: 'slow' }   // new shape B

load(first) //IC of load: [{ slot: 0, icType: LOAD, value: MONO(A) }] --> Miss
load(fast) //IC of load: [{ slot: 0, icType: LOAD, value: MONO(A) }]  --> Fast
load(slow) //IC of load: [{ slot: 0, icType: LOAD, value: POLY[A,B] }]  --> Slow. Now it needs to check 2 shapes. 

漏洞成因

該漏洞的修復Commit修改了兩個函數HandleLoadICSmiHandlerLoadNamedCaseComputeHandler。對這兩個函數進行追蹤可以發現以下調用鏈:

ComputeHandler
              ^
UpdateCaches
              ^
Load
              ^
Runetime_LoadWithReceiverIC_Miss

HandleLoadICSmiHandlerLoadNamedCase 
              ^
HandleLoadICSmiHandlerCase
              ^
HandleLoadICHandlerCase
              ^
GenericPropertyLoad

從函數名可以看出,這里是在加載屬性,那么可以聯想到在了解IC時討論的屬性加載的問題。通過查看bytecode,可以發現屬性加載是通過LdaNamedProperty來實現的。通過搜索發現以下代碼:

// LdaNamedProperty <object> <name_index> <slot>
//
// Calls the LoadIC at FeedBackVector slot <slot> for <object> and the name at
// constant pool entry <name_index>.
IGNITION_HANDLER(LdaNamedProperty, InterpreterAssembler) {
  TNode<HeapObject> feedback_vector = LoadFeedbackVector();

  // Load receiver.
  TNode<Object> recv = LoadRegisterAtOperandIndex(0);

  // Load the name and context lazily.
  LazyNode<TaggedIndex> lazy_slot = [=] {
    return BytecodeOperandIdxTaggedIndex(2);
  };
  LazyNode<Name> lazy_name = [=] {
    return CAST(LoadConstantPoolEntryAtOperandIndex(1));
  };
  LazyNode<Context> lazy_context = [=] { return GetContext(); };

  Label done(this);
  TVARIABLE(Object, var_result);
  ExitPoint exit_point(this, &done, &var_result);

  AccessorAssembler::LazyLoadICParameters params(lazy_context, recv, lazy_name,
                                                 lazy_slot, feedback_vector);
  AccessorAssembler accessor_asm(state());
  accessor_asm.LoadIC_BytecodeHandler(&params, &exit_point);
.....
}

注意最后一行,追蹤LoadIC_BytecodeHandler發現此函數處理了所有關于屬性訪問的情況。第一次訪問時并不會FeedbackVector所以會進入LoadIC_NoFeedBack函數。

void AccessorAssembler::LoadIC_NoFeedback(const LoadICParameters* p,
                                          TNode<Smi> ic_kind) {
  Label miss(this, Label::kDeferred);
  TNode<Object> lookup_start_object = p->receiver_and_lookup_start_object();
  GotoIf(TaggedIsSmi(lookup_start_object), &miss);
  TNode<Map> lookup_start_object_map = LoadMap(CAST(lookup_start_object));
  GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss);

  TNode<Uint16T> instance_type = LoadMapInstanceType(lookup_start_object_map);

  {
    // Special case for Function.prototype load, because it's very common
    // for ICs that are only executed once (MyFunc.prototype.foo = ...).
    Label not_function_prototype(this, Label::kDeferred);
    GotoIfNot(IsJSFunctionInstanceType(instance_type), &not_function_prototype);
    GotoIfNot(IsPrototypeString(p->name()), &not_function_prototype);

    GotoIfPrototypeRequiresRuntimeLookup(CAST(lookup_start_object),
                                         lookup_start_object_map,
                                         &not_function_prototype);
    Return(LoadJSFunctionPrototype(CAST(lookup_start_object), &miss));
    BIND(&not_function_prototype);
  }

  GenericPropertyLoad(CAST(lookup_start_object), lookup_start_object_map,
                      instance_type, p, &miss, kDontUseStubCache);

  BIND(&miss);
  {
    TailCallRuntime(Runtime::kLoadNoFeedbackIC_Miss, p->context(),
                    p->receiver(), p->name(), ic_kind);
  }
}

在這里找到了GenericPropertyLoad。同時發現無論如何都會執行Runtime::kLoadNoFeedbackIC_Miss。這個函數其實就是RUNTIME_FUNCTION(Runtime_LoadWithReceiverIC_Miss)

至此完整的調用鏈已經找到了,那根據此調用鏈,可以發現在第一次訪問屬性時,由于沒有FeedbackVector,會調用LoadIC_NoFeedback。假設lookup_start_object不是小整數且沒有被淘汰(被回收),那么就會調用GenericPropertyLoad,隨后再調用LoadNoFeedbackIC_Miss。在ComputeHandler中,發現修改的判斷分支檢查了holder是否在IsJSModuleNamespace,但是在HandleLoadICSmiHandlerLoadNamedCase中卻加載的是receiver,此對象并不在JSModuleNamespace中。所以當FeedbackVector被創建后,內部的IC中的類型記錄可能與真正調用時的類型不符,假設此時使用IC中儲存的對象類型調用JSModuleNamespace中的某些屬性,那么V8會根據FastPath使用IC中存儲的類型,但是由于receiver不是此類型,就會導致類型混淆。

綜上所述,復現此漏洞需要以下條件:

  1. JSModuleNamespace中放置一個可以隨時調用的屬性/函數

此條件可以通過export文件中的函數或屬性即可,比如說在“一個文件”中:

export let foo = {}
//或者(筆者使用的方法)
export function foo()
{
  ....
}

在“另一個文件”中:

import * as foo from "一個文件.mjs";
%DebugPrint(foo)

會有以下結果:

/*
DebugPrint: 000003BF080496D9: [JSModuleNamespace]
 - map: 0x03bf082077f9 <Map(DICTIONARY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x03bf08002235 <null>
 - elements: 0x03bf08003295 <NumberDictionary[7]> [DICTIONARY_ELEMENTS]
 - module: 0x03bf081d3229 <Other heap object (SOURCE_TEXT_MODULE_TYPE)>
 - properties: 0x03bf080496ed <NameDictionary[17]>
 - All own properties (excluding elements): {
   0x03bf08005669 <Symbol: Symbol.toStringTag>: 0x03bf080049f5 <String[6]: #Module> (data, dict_index: 1, attrs: [___])
   f: 0x03bf081d3349 <AccessorInfo> (accessor, dict_index: 2, attrs: [WE_])
 }
 - elements: 0x03bf08003295 <NumberDictionary[7]> {
   - max_number_key: 0
 }
000003BF082077F9: [Map]
 - type: JS_MODULE_NAMESPACE_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: DICTIONARY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - dictionary_map
 - may_have_interesting_symbols
 - non-extensible
 - prototype_map
 - prototype info: 0x03bf081d3369 <PrototypeInfo>
 - prototype_validity cell: 0x03bf08142405 <Cell value= 1>
 - instance descriptors (own) #0: 0x03bf080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x03bf08002235 <null>
 - constructor: 0x03bf081c3bed <JSFunction Object (sfi = 000003BF08144745)>
 - dependent code: 0x03bf080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
 */
  1. 在另一個文件中創建一個對象,并使得此對象的lookup_start_objectholder不同并符合holderJSModuleNamespaceType。之后,調用“一個文件”中的函數(訪問屬性),以觸發LoadWithReceiverIC_Miss導致的UpdateCaches
import * as foo from "一個文件.mjs";
class Test(){}
class Test1(){}
let tmp = new Test();
Test.prototype.__proto__=Test1;//修改lookup_start_object
Test.prototype.__proto__.__proto__=foo;//修改holder
  1. 重復以上步驟直到IC開始使用FeedbackVector中的信息。由于
  TNode<Module> module =
        LoadObjectField<Module>(CAST(p->receiver()), JSModuleNamespace::kModuleOffset);

認為這里會提供一個foo,但是p->receiver并不是foo。此時便會觸發類型混淆。

修復方案

修復該漏洞只需保證ic.ccaccessor-assembler.cc中使用的對象類型是相同的即可,V8選擇的方式為在HandleLoadICSmiHandlerLoadNamedCase中使用holder(而不是receiver)作為Load的參數。并在ComputeHandler中為smi類別單獨開分了一個判斷分支,以確保在處理HandleLoadICSmiHandlerLoadNmaedCase中一定會拿到smi_handler

修復Commit內容如下:

近期Google官方修復了包括在天府杯中披露的和已發現存在在野利用的多個高危漏洞,建議Chrome用戶積極將程序升級到最新穩定版以免受到攻擊,目前最新穩定版本為96.0.4664.77。

參考鏈接

https://github.com/v8/v8/commit/e4dba97006ca20337deafb85ac00524a94a62fe9

https://github.com/maldiohead/TFC-Chrome-v8-bug-CVE-2021-38001-poc

http://web.cs.ucla.edu/~palsberg/course/cs232/papers/DeutschSchiffman-popl84.pdf

http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-202110-2070


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2007/