看雪论坛作者ID:苏啊树
0x1 关于v8引擎
1.1、v8的指针为真实地址+1,这样最后一位为1,据说是为了加快寻址的速度,根据最后一位直接就可以判断该值其代表的是指针还是自然数。
这点在逆向的角度就是优化后生成的指令,
a)看到到有add rX,1这样的指令片段一般就是代表着开始对指针指向对象的初始化。
b)在内存中看到的数值为真实数值*2。
1.2、在比较新版本的v8对一个地址只是存储其低4位,要进行寻址的时候再加上其高八位。
1.3、v8在多次执行一个函数的时候,会触发JIT(优化)过程,直接生成函数对应的机器码,这也是这篇分析文章的理论基础。
0x2 JIT产生的优化指令分析
const _arr = new Uint32Array([2**31]);
function foo() {
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 corr = new Array(x);
arr.shift();
var arr = [1.1, 1.2, 1.3];
return [arr, cor];
}
console.log("ready !!");
for(i=0;i<0x3000;i++)
{
foo();
}
%SystemBreak();
var x = foo();
var corr=x[0];
var arr=x[1];
%DebugPrint(corr);
%DebugPrint(arr);
console.log("Analyze Over!")
分析一下图2.1.1.1的指令逻辑,这里是对是否已经优化做判断,如果已经优化就执行优化后生成的指令否则则是通过r10跳转到d8.Builtins_CompileLazyDeoptimizedCode,先执行优化过程。
运行到这里显然已经完成了优化,继续下去执行的是优化后生成的指令。
图2.1.1.2
图2.1.1.3
2.1.2、调试器走到图2.1.1.3所在的位置,这里的指令为:
这里进行的是v8的数组的取值操作,取完值以后对该值进行+1操作,对应的是poc中的x=(_arr[0] ^ 0)+1的js代码。
图2.1.2.1
图2.1.2.2
走到图2.1.2.1的add rcx,1这句指令后,结果为0x80000001,并将其放在rcx指令中,接下来用一些特殊指令进行处理,如下图所示:
图2.1.2.3
2.1.3、这些指令对应的是poc中的x = Math.abs(x) js语句,应该是做了些优化;
但是这些指令执行完以后,rcx还是为0x80000001,而后面的指令中使用的是ecx作为参数进行运算。
也就是说v8优化后产生的这些指令,并未正确处理x = Math.abs(x);的操作。这也是后面一连串错误操作的起始原因。
图2.1.3.1
2.1.4、流程运行到这里直接用rdi寄存器的的ecx来进行计算,由上图可以看到,导致运算为0x80000001-0x7fffffff,结果等于2。
接下来是指令:
cmp ecx,0
jl 39000C40CA
这里是执行源poc里面的x = Math.max(x, 0)的js语句,最后结构返回x=2。这段指令将结果放入到rdi寄存器中,此时edi表示参数x。
图2.1.4.1
图2.1.4.2
图2.1.4.3
2.1.5、上图所示,edi进行-1运算(也就是poc js文件里面的参数x),此时edi=1,结果自然是不等于0xFFFFFFFF,最后越过poc 中的x==-1的判断。对应poc中的js代码if(x==-1) x = 0;
2.2、漏洞数组初始化分析:
图2.2.1.1
图2.1.4.1上EIP指向的指令为:
cmp rdi,0
je 39000C419D
也就是对x是否为0进行了判断。
根据v8指针的特点,以及调试分析可以判断,这里执行的流程是进行数组对象的初始化。
2.2.1、初始化r8-1指向的数组。(这里将其定义为array1)
add r8,1
lea r9d,qword ptr ds:[rdi+rdi]//rdi=x
mov r12,qword ptr ds:[r13+D0]
mov dword ptr ds:[r8-1],r12d //初始化array1数组的map。
mov dword ptr ds:[r8+3],r9d//初始化array1数组的大小,内存值为2*x。
xor r9,r9
lea r14,qword ptr ds:[r9+1]
mov r15,qword ptr ds:[r13+98]
mov dword ptr ds:[r8+r9*4+7],r15d /*这里进行了初始化,也就是该数组第一个元素为[r8+7],在[array1+8] */
图2.2.1.2
2.2.2、图2.2.1.2EIP前面的一段指令是初始化r9-1指向的数组。(这里将其定义为array2)
add r9,1
mov r14d,824394D
mov dword ptr ds:[r9-1],r14d//初始化array2数组的map
mov r14,qword ptr ds:[r13+158]
mov dword ptr ds:[r9+3],r14d//初始化array2数组的prototype
lea r15d, qword ptr ds:[rdi+rdi]
mov dword ptr ds:[r9+7],r8d//初始化array2数组的大小,内存值为2*x。
mov [r9+7],r8d//初始化array2数组的elements,让其指向array1
图2.2.1.1、R8、R9寄存器此时指向的内存
图2.2.2.2、array1指向的内存
图2.2.2.3、array2指向的内存
2.2.3、此时array1和array2两者内存关系如下图所示:
图2.2.3.4、array1和array2的内存关系图
这内存关系在后面的漏洞利用比较重要。如果改变了sizeofarray1的值,就对后面的内存进行读写,我们在poc中的js对corr 进行索引,其实是索引element1开始的内存,也就是实际上控制这里的array1数组。corr[0]索引到的值为element1,corr[1] 索引到的值为array2(map),corr[2]索引为到的值为sizeofarray2,由此继续下去。
2.3、数值越界的直接原因
图2.3.1.1
图2.3.1.1这里的指令为:mov dword ptr ds:[r9+B],FFFFFFFE;
接下来的指令是直接将0xFFFFFFFE这个硬编码放入了array2存放数组大小的内存位置中,v8 引擎对array2索引的时候,会把0xFFFFFFFE当成array2数组的大小,从而将array2视为一个0xFFFFFFFE/2大小的数组。(不过经过调试这个内存的数值并不是v8用来索引poc中js数组corr的数值,只要该值不为0,就会去索引实际控制内存的数组array1)。
2.3.2、紧接着会执行指令:
mov rdi,qword ptr ds:[r13+98]
mov dword ptr ds:[r8+3],edi
这两句指令是对原来array1的数组大小的内存位置,也就是[array1+4]的值进行填充,填充的值为[r13+98]指向的值,这里是数字0x08042429,v8引擎会在后面把这个填进来的数字解释为array1的大小。(这是造成这漏洞数组corr可以越界读写的直接原因)
图2.3.2.1
对这一段指令总结:
在v8执行生成和执行poc 优化后的corr.shift() js指令时,并没有对array2指向array1的关系进行改变。
在对array2数组处理时,原来存放数组大小的位置改为了数字0xFFFFFFFE,但这个数值v8会认为其是合法的数组数值,因此我们poc中的js语句中还是用corr数组还是可以继续索引到array1数组。
而array1会因为优化的原因,把代表着array1长度的内存值修改为0x08042429(这个数值根据调试环境而定,但是一定是个比2大很多的值)。而v8解释器会把这个值解释为array1数组的大小也就是poc的js中corr数组的大小,从而造成了corr数组可以越界读写。
0x3 浮点数组生成验证越界读写
图3.1.1.1
0x4 写在最后
看雪ID:苏啊树
https://bbs.pediy.com/thread-267128.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!