首页
社区
课程
招聘
[原创]从POC到EXP:从0基础到v8 CVE-2021-38003复现
发表于: 2天前 1232

[原创]从POC到EXP:从0基础到v8 CVE-2021-38003复现

2天前
1232

此文章首发于奇安信攻防社区b83K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3L8%4u0#2L8g2)9J5k6h3u0#2N6r3W2S2L8W2)9J5k6h3&6W2N6q4)9J5c8Y4y4Z5j5i4u0W2i4K6u0r3y4o6R3#2x3b7`.`.

TheHole New World - how a small leak will sink a great browser (CVE-2021-38003)

[V8 Deep Dives] Understanding Map Internals

从 0 开始学 V8 漏洞利用系列篇

Chaos-me-JavaScript-V8

手把手教你入门V8漏洞利用

最近在做2026年SUCTF的赛题复现,做到SU_BOX这一题的时候发现是一个v8引擎利用,之前也没有学过v8就一边学一边做了这一题,学习的过程中也踩了很多坑……

编译的主要流程参考了从 0 开始学 V8 漏洞利用系列篇这一篇文章,这个文章将编译的流程写成了脚本,方便后续编译不同版本的v8。

需要注意的是,编译的参数最好按照官方的来,比如SU_BOX使用的是J2V8,其编译v8的方式是这样的

那我们就要在编译参数上尽可能相同,在此基础上添加部分调试参数进行编译

所以写成build.sh脚本是这样的,由于我是在docker中编译的,因此很多路径都是绝对路径,需要进行修改

如果不按照官方给的参数编译的话,有可能POC无法跑通,就直接影响后续的漏洞利用

同时,经过多次尝试,我建议在运行ubuntu 20.04或者ubuntu 22.04且运行python 3.9或者python 3.10的系统环境中构建,过高或者过低的系统/python版本都会导致编译出错。编译完之后的目录是这样子的

image-20260331214807313

其中可执行文件d8就是我们攻击的目标文件

image-20260331215052227

同时需要将这两个文件导入到gdbinit文件中,这样才能使用v8的调试指令

我们将以下内容写在test.js中

%SystemBreak()就是断点,程序会断在这里;%DebugPrint(a)就是将a列表的调试数据打印到终端

image-20260331215643399

在gdb中调试d8文件,然后运行的时候带上--allow-natives-syntax参数才能使用%SystemBreak() %DebugPrint()两条调试指令,运行效果如下

image-20260331220906009

也可以在gdb中使用job指令查看对象

image-20260331220919903

需要注意的是,v8为了体现数据和地址的不同采用了不同的策略:地址+1存储,也就是说如果0x41414140作为对象地址存储就会变成0x41414141,这一点非常重要,所以这个对象的真实地址是0x3655bb30ee01-1=0x3655bb30ee00

配合x指令打印具体地址信息,可以看到JSArray结构体其实是这样排布的

image-20260331221123711

回到刚刚的程序

JSArray结构体用示意图来表示是这样的

image-20260331222708109

高版本的v8中存在地址压缩,在这个版本中部分字段占8字节,具体每个字段占几个字节需要根据具体版本进行调试分析

我们看一下element是如何存储的

image-20260331222840946

可以看到数据其实是存储在一个FixedDoubleArray结构体对象里的,同时可以看到这个结构体的存储位置是JSArray结构体的上方,示意图如下:

image-20260331223754167

我们调试一下下面的程序,看看其中其他数据类型的存储和浮点类型的数据存储有什么不同

这是b对象的信息

image-20260401201212373

示意图如下,可以看到在这个数据结构中存储element的结构体和JSArray结构体并不是在内存上相邻的

image-20260401202059855

这是c对象的信息

image-20260401202405853

示意图如下,可以看到在这个数据结构中存储对象的FixedArray结构体和JSArray结构体在内存上相邻的

image-20260401202804873

OK,那么我们可以简单总结一下:如果一个JSArray结构体存储的是浮点数和对象,那么这个结构体存储元素的地址和它本身是相邻的

如果我们能通过一个漏洞修改浮点数JSArray的length字段,就可以通过索引来进行越界读写,这其实就是v8漏洞利用的核心

了解了v8底层的数据存储就可以正式开始学习v8的漏洞利用了

v8是如何判断一个JSArray结构体中存储的是浮点数、整数还是对象的呢,其实就是看JSArray的Map,每一种类型的Map都不一样

如果我们将一个存储对象的JSArray结构体的Map修改为浮点数数组对应的Map,那么读取这个结构体的时候就会返回一个浮点数

image-20260401205710362

我们拿到的浮点数是什么呢?诶,这就是对象的地址,v8漏洞利用中我们就可以通过这个方式来泄露对象的地址。我们将这个流程封装成函数addressOf,可以这么调用

将一个存储浮点数的JSArray结构体的Map修改为对象数组对应的Map,那么我读取这个结构体的时候就能返回一个对象,我们可以通过这个功能构造一个fake Object,将这个流程封装成函数fakeObj(),可以这样调用

fake Object有什么用呢,我们可以通过这个fake Object来达到任意地址读和任意地址写的效果

获得addressOffakeObj原语,基本就是靠我们上一块所讲的修改浮点数JSArray的length字段以达到越界写来实现的

由于在v8漏洞中主要利用的还是浮点数的存储,因此需要一些工具函数用于大整数与浮点数之间的互转,函数定义如上,可以直接拿着用

首先我们要通过漏洞实现addressOffakeObj原语,同时已经泄露出了浮点数JSArray的Map值,将其定义为DOUBLE_MAP常量,随后定义or修改浮点数对象如下:

此时内存中是这样存储的

image-20260401212535943

然后通过addressOf原语获得标红区域的内存,将其传入fakeObj原语中,就可以拿到fake Object,将其定义为fake_object

最后我们可以通过fake_object[0]来进行任意地址读,由于这个fake_object是伪造的存储浮点数的JSArray,因此通过fake_object[0]获取的值并不是addr中存储的数据,而是addr+0x10中存储的数据,原理可以看下面这一张图,因为addr应该是一个FixedDoubleArray结构体的地址,而存储数据的地址是addr+0x10

image-20260401213220982

我们可以将其封装成read64函数

这里的addr就是我们想泄露的地址,那么写到fake_object中就应该是addr-0x10+1

这个1的产生就是我们之前说过的v8存储地址和普通程序的差异

任意地址写和任意地址读差不多,无非就是最后的从fake_object获取值改成了修改fake_object的存储的值

由于低版本v8中会给WASM一个可读可写可执行的段,因此我们可以考虑通过shellcode替换原有的WASM内容以达到执行shellcode的效果

image-20260402123048418

当执行到断点时,vmmap就可以看到出现了一个可读可写可执行段,我们只需要想办法把shellcode写入这个段的开始地址,也就是0x11d80365f000,随后执行f()就可以触发shellcode

需要注意的是,在较高版本的v8中,WASM段已经不是可读可写可执行了,而是变成了可读可执行,因此就没有办法通过这个方式来进行利用了

我们回头看看之前的任意地址写,如果通过之前的方式写入shellcode会导致以下两个问题

因此我们需要一种向某个对象中写入数据不需要经过map和length的方式来实现任意地址写

调试结果如下

image-20260402132746936

可以看到,本质上来说setFloat64是在向JSArrayBuffer的backing_store指向的内存中写入内容,那么我们只要通过原有的任意地址写write64控制这个字段为可读可写可执行段的开始地址,就可以通过setFloat64方法向内存中无限制写入数据

讲到这里,v8漏洞利用就差不多了,可以开始具体分析题目了,因为addressOffakeObj原语都和具体题目有关,不同的题目获得原语的方式也不同。获得了这两个原语才能再写read64函数和write64函数

这个CVE的POC可以从谷歌纰漏漏洞的网站找到44eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6M7%4y4#2k6i4y4Q4x3X3g2U0K9s2u0G2L8h3W2#2L8g2)9J5k6h3!0J5k6#2)9J5c8X3W2K6M7%4g2W2M7#2)9J5c8U0b7H3x3o6f1%4y4K6p5H3

关于漏洞产生的原理本文不过多赘述,我们关注于漏洞点的利用,也就是已知CVE如何利用漏洞

我们将最后的循环删掉,然后打印一下map.size,看看POC有没有生效

image-20260402134235807

可以看到POC是有效的,那么我们就可以将这个POC改写成EXP进行利用

修改POC的整体流程可以配合7dcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3q4J5L8r3q4T1M7#2)9J5k6i4y4Y4i4K6u0r3j5X3I4G2k6#2)9J5c8U0t1H3x3U0u0Q4x3V1j5I4x3W2)9J5k6s2c8Z5k6g2)9J5k6r3S2G2L8r3g2Q4x3X3c8F1k6i4N6Q4x3X3c8%4L8%4u0D9k6q4)9J5k6r3S2G2N6#2)9J5k6r3q4Q4x3X3c8K6L8h3q4D9L8q4)9J5k6r3I4W2j5h3E0Q4x3X3c8%4K9h3I4D9i4K6u0V1M7$3W2F1K9#2)9J5k6r3q4Q4x3X3c8Y4M7X3g2S2N6q4)9J5k6r3u0J5L8%4N6K6k6i4u0Q4x3X3c8U0N6X3g2Q4x3X3b7J5x3o6t1I4i4K6u0V1x3K6R3H3x3o6y4Q4x3V1j5`.

这篇文章食用,但是这篇文章的绝大多数数据需要在本地进行调试得出,我们接下来就开始我们的调试流程

首先我们看一下正常的map对象是什么样子的

image-20260402135808832

JSMap在底层是通过OrderedHashMap实现的,因此我们重点需要分析OrderedHashMap这个结构体,这个结构体的原理可以看这篇文章f65K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6N6r3&6W2P5s2c8Q4x3X3g2A6L8#2)9J5c8Y4j5^5i4K6u0V1k6r3g2W2M7q4)9J5k6r3c8A6N6X3g2K6i4K6u0V1N6h3&6V1k6i4u0K6N6r3q4F1k6r3W2F1k6#2)9J5k6r3#2S2M7q4)9J5k6r3W2F1N6r3g2J5L8X3q4D9M7#2)9J5k6o6b7#2k6h3t1&6y4r3p5I4z5o6y4V1k6R3`.`.

这个结构体的示意图如下:

image-20260402194444073

当我们执行map.set(key, value)时,会先对我们的key取哈希,随后和bucket_count-1进行与操作,获得hash_table_index

随后current_index就是目前已经放入数据的个数,如果hashTable[hash_table_index] == -1就代表这个哈希表还是空的,就会将key和value写入dataTable中

当触发map.size == -1的漏洞时,我们看一下此时新建键值对会对内存产生什么影响

image-20260402145454092

image-20260402145509508

可以看到0x41和0x42这两个值分别放在了buckets CounthashTable[0]的位置上,这样的话我们就可以通过这一次异常操作来挟持OrderedHashMap中hashTable和dataTable的个数,进而达到越界写的目的


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回