QuickJs入门
QuickJS是一个小型并且可嵌入的Javascript引擎,它支持ES2020规范,包括模块,异步生成器和代理器。
它可选支持数学扩展,例如大整数 (BigInt),大浮点数 (BigFloat) 以及运算符重载。
具体参考官方文档
安装(linux)
依赖:
sudo apt-get install -y build-essential gcc-multilib
git clone https://github.com/quickjs-zh/QuickJS.git
cd QuickJS
make install #默认安装到/usr/local
更换安装路径可以修改MakeFile中prefix=/home/llgoer/quickjs
完成编译后则在/home/llgoer/quickjs目录下看到quickjs相关的可执行文件
windows参考文档
快速使用
qjs
是命令行解析器 (Read-Eval-Print Loop). 您可以将Javascript文件和/或表达式作为参数传递以执行它们:
./qjs examples/hello.js
qjsc
是命令行编译器:
./qjsc -o hello examples/hello.js
./hello
生成一个没有外部依赖的 hello
可执行文件。
qjsbn
和 qjscbn
是具有数学扩展的相应解释器和编译器:
./qjsbn examples/pi.js 1000
显示PI的1000位数字
./qjsbnc -o pi examples/pi.js
./pi 1000
编译并执行PI程序。
命令行选项
qjs
解释器
用法: qjs [options] [files]
选项:
-h
--help
选项列表。
-e `EXPR`
--eval `EXPR`
执行EXPR.
-i
--interactive
转到交互模式 (在命令行上提供文件时,它不是默认模式).
-m
--module
加载为ES6模块(默认为.mjs文件扩展名)。
高级选项包括:
-d
--dump
转存内存使用情况统计信息。
-q
--quit
只是实例化解释器并退出。
qjsc
编译器
用法: qjsc [options] [files]
选项:
-c
仅输出C文件中的字节码,默认是输出可执行文件。
-e
main()
C文件中的输出和字节码,默认是输出可执行文件。
-o output
设置输出文件名(默认= out.c或a.out)。
-N cname
设置生成数据的C名称。
-m
编译为Javascript模块(默认为.mjs扩展名)。
-M module_name[,cname]
添加外部C模块的初始化代码。查看c_module
示例。
-x
字节交换输出(仅用于交叉编译)。
-flto
使用链接时间优化。编译速度较慢,但可执行文件更小更快。使用选项时会自动设置此选项-fno-x
。
-fno-[eval|string-normalize|regexp|json|proxy|map|typedarray|promise]
禁用所选语言功能以生成较小的可执行文件。
逆向二进制文件实例
QuickJs编译原理大概是提供了一个js运行的环境,即虚拟环境解释器,将js源码转换成字节码bytecode,然后使用c语言加载js运行环境,然后运行js字节码实现运行js程序,尝试可以发现不同程序生成的c代码壳子一样,只有字节码不同。
test.js如下:
console.log("Hello World");
编译成二进制文件test
qjsc -o test test.js
只生成js转换成的c程序
qjsc -e test.js -o test.c
// test.c
/* File generated automatically by the QuickJS compiler. */
#include "quickjs-libc.h"
const uint32_t qjsc_test_size = 77;
const uint8_t qjsc_test[77] = {
0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,
0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72,
0x6c, 0x64, 0x0e, 0x74, 0x65, 0x73, 0x74, 0x2e,
0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00, 0xa2, 0x01,
0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x14, 0x01,
0xa4, 0x01, 0x00, 0x00, 0x00, 0x38, 0xe3, 0x00,
0x00, 0x00, 0x42, 0xe4, 0x00, 0x00, 0x00, 0x04,
0xe5, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0xcf,
0x28, 0xcc, 0x03, 0x01, 0x00,
};
static JSContext *JS_NewCustomContext(JSRuntime *rt)
{
JSContext *ctx = JS_NewContextRaw(rt);
if (!ctx)
return NULL;
JS_AddIntrinsicBaseObjects(ctx);
JS_AddIntrinsicDate(ctx);
JS_AddIntrinsicEval(ctx);
JS_AddIntrinsicStringNormalize(ctx);
JS_AddIntrinsicRegExp(ctx);
JS_AddIntrinsicJSON(ctx);
JS_AddIntrinsicProxy(ctx);
JS_AddIntrinsicMapSet(ctx);
JS_AddIntrinsicTypedArrays(ctx);
JS_AddIntrinsicPromise(ctx);
JS_AddIntrinsicBigInt(ctx);
return ctx;
}
int main(int argc, char **argv)
{
JSRuntime *rt;
JSContext *ctx;
rt = JS_NewRuntime();
js_std_set_worker_new_context_func(JS_NewCustomContext);
js_std_init_handlers(rt);
JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);
ctx = JS_NewCustomContext(rt);
js_std_add_helpers(ctx, argc, argv);
js_std_eval_binary(ctx, qjsc_test, qjsc_test_size, 0);
js_std_loop(ctx);
js_std_free_handlers(rt);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
也就是说我们拿到二进制文件需要逆的就是qjsc_test字节码对应的逻辑,这些字节码opcode显然是quickjs的虚拟机实现,具体需要看qjs的虚拟机实现逻辑,但是这个项目开源,我们有源码,查看源码发现源码中有对应的DUMP宏来控制是否输出字节码的转换过程。
所以我们就有思路,将qjs修改,将DUMP_BYTE相关的宏打开,重新编译,编译后的qjs和qjsc就可以输出转换过程中的字节码了
将目标程序的字节码提取出来,换到test.c的壳子中,重新编译,再次运行时就可以输出目标字节码的对应的opcode和其他信息,类似汇编的东西就好逆向了
第一步,修改源码quickjs.c,diff文件如下
diff --git a/quickjs.c b/home/yrl/QuickJS/quickjs.c
old mode 100755
new mode 100644
index 4e58a98..93b6f75
--- a/quickjs.c
+++ b/home/yrl/QuickJS/quickjs.c
@@ -89,7 +89,7 @@
32: dump line number table
64: dump compute_stack_size
*/
-//#define DUMP_BYTECODE (1)
+#define DUMP_BYTECODE (1)
/* dump the occurence of the automatic GC */
//#define DUMP_GC
/* dump objects freed by the garbage collector */
@@ -103,7 +103,7 @@
//#define DUMP_SHAPES /* dump shapes in JS_FreeContext */
//#define DUMP_MODULE_RESOLVE
//#define DUMP_PROMISE
-//#define DUMP_READ_OBJECT
+#define DUMP_READ_OBJECT
/* test the GC by forcing it before each object allocation */
//#define FORCE_GC_AT_MALLOC
@@ -36076,6 +36076,9 @@ static JSValue JS_ReadFunctionTag(BCReaderState *s)
bc_read_trace(s, "}\n");
}
b->realm = JS_DupContext(ctx);
+ #if DUMP_BYTECODE
+ js_dump_function_bytecode(ctx, b);
+ #endif
return obj;
fail:
JS_FreeValue(ctx, obj);
重新编译qjs,替换test.c中的bytescode为目标bytescode,目标bytescode在程序的js_std_eval_binary(ctx, qjsc_test, qjsc_test_size, 0);
函数的第二个参数处,ida或gdb提取出来即可
然后重新编译
gcc -o test test.c quickjs-libc.o libquickjs.a -I/home/xxx/QuickJS -lm -ldl
- -I/path/to/quickjs: 添加 QuickJS 源码目录到包含路径,以便找到 quickjs-libc.h 文件。
- quickjs-libc.o: 链接 QuickJS 库的对象文件。
- libquickjs.a: 链接 QuickJS 静态库文件。
- -lm -ldl: 链接数学库和动态加载库。
现在程序运行环境变成了我们修改过的运行环境,可以自动将bytescode反编译成opcode的形式输出出来:
➜ attachments ./test
0000: 02 04 4 atom indexes {
1"console"
1"log"
1"Hello World"
1"test.js"
0002: 0e 63 6f 6e 73 6f 6c 65
06 6c 6f 67 16 48 65 6c
6c 6f 20 57 6f 72 6c 64
0e 74 65 73 74 2e 6a 73 }
0022: 0e function {
0023: 00 06 00 a2 01 00 01 00
03 00 00 14 01 name: "<eval>"
args=0 vars=1 defargs=0 closures=0 cpool=0
stack=3 bclen=20 locals=1
vars {
0030: a4 01 00 00 00 name: "<ret>"
}
bytecode {
0035: 38 e3 00 00 00 42 e4 00
00 00 04 e5 00 00 00 24
01 00 cf 28 at 1, fixup atom: console
at 6, fixup atom: log
at 11, fixup atom: "Hello World"
}
debug {
0049: cc 03 01 00 filename: "test.js"
}
test.js:1: function: <eval>
locals:
0: var <ret>
stack_size: 3
opcodes:
get_var console
get_field2 log
push_atom_value "Hello World"
call_method 1
set_loc0 0: "<ret>"
return
}
Hello World
成功将字节码反汇编出来,接下来就是逆向js了。
但如果出题人将quickjs的opcode改掉了,这种方式就行不通了,需要先找到opcode做了哪些修改,将源码还原后再重新编译相同的环境,之后再使用以上方式得到js源码
如果是修改了opcode顺序,可以计算原始opcode基本块和修改后的opcode基本块的hash,找到对应关系,可通过idapython脚本实现此功能。