0ctf/tctf2023-0gn nodejs引擎魔改分析
碎碎念
本文也可以改名《Shino 为什么是一个啥比》,一个首波逆向题坐牢两天没做出来(还是我太菜了)
主要是复盘记录一下当时做题的几个思路和分析以及反思,虽然都没有成功但是也算一次经验积累,以供以后参考。
赛时分析
js部分 解混淆
首先格式化一下js,然后简单看一下混淆的大概pattern。
有一个函数表,类似这样(节选)
|
|
这里解混淆的思路是显然的,通过正则或手动将被混淆的函数名直接替换为对应的操作。如:
|
|
搜索代码中全部的'bLOAn'
替换为^
,可以获得完整可读的代码逻辑。
这里通过常量识别可以发现是一个md5,没有任何魔改,哈希结果对应的即为默认输入flag{00000000000000000000000000000000}
。但是输出结果为Wrong!
,可以判断是nodejs被魔改。
nodejs 魔改点初步定位
尝试进行黑盒测试来猜测 nodejs 的魔改位置。我们先看一下整体 js 进行 flagcheck 的流程:
|
|
这里我们先假设 nodejs 没有对被运行的 js 进行完整性校验,即我们可以用他的引擎运行任意的脚本进行测试。如果测试中可以触发Right!
则可以判定不存在这种可能。
通过在脚本中进行console.log
,可以发现输入 flag 为 flag{00000000000000000000000000000000}
时,得到的执行路径是 pos1->pos2->pos3 (在上面代码中标出)
这里可以发现resultchecker
的执行到return 0x0
,但是主函数得到的返回值却是-0x1
矛盾。
我们来思考一下几种可能性并且用我们观察到的现象来验证一下:
resultchecker
中的某些运算符被重载或常量被替换:- 尝试在 pos1 处输出 e 和密文,发现他们没被更改,且
==
正常工作(返回true),否定这种可能性
- 尝试在 pos1 处输出 e 和密文,发现他们没被更改,且
resultchecker
被替换:- 在 pos1 处添加的输出语句能被执行,说明
resultchecker
内的逻辑有被执行,否定这种可能性
- 在 pos1 处添加的输出语句能被执行,说明
resultchecker
被某种方式 hook (可能的)Return
或其相关的字节码解析操作被改写(可能的)
注意到上面的3、4操作本质上是相同的。
hook 时机和条件定位
一般来说 hook 一个函数必须要先找到这个函数。虽然我不会 js,但是这些方法一般是通用的。寻找函数的方法大概如下(可能我经验不足接触得较少):
- 函数名称
- 函数签名
- 函数偏移
- 函数特征(一般是特征字符串的引用)
简单进行一下测试:
|
|
可以得出函数触发 hook 的条件为:
- 函数名中包含
resultchecker
字符串 - 返回0或-1(这里-1无法被验证)
- 只传入一个参数
这里很明显是通过函数名称来定位的函数,同时因为 hook 是否触发与返回值有关,因此 hook 时机一定在 return 之后。
hook 函数定位
由于没有发现可见字符串resultchecker
(详见“复盘”节《为什么说 Shino 是一个啥比》),尝试通过以下方法进行 hook 函数定位。这里就是记录一下,实际上没有能跑通的方法。
由于我对 nodejs 内核没有任何的了解,所以我的调试方法是添加console.log
语句并且监控write
系统调用(只要有输出必定有调用)来在 js 函数内部添加断点。
由于我的 Cheat Engine 爆炸了,并且没有合适的时机进行 Attach (这里我觉得是我技不如人),所以没有先把程序跑起来再在内存中搜可见字符串这种操作(这里我觉得是我技不如人,我觉得这个方法应该是可行的,等师傅们教我)
- 调用链 diff (忘了hook时机在 return 之后,弱智了)
- Bindiff
- 实际上由于题目的 nodejs 的编译环境和官方的完全不一样,导入的模块也不一样,可以说结果没有一点相同的
- 跟踪输入下读写断点
- 在 String 的 New 方法里找输入(没找到)
- 在 Argument Parse 的相关方法里找输入(没找到)
- 或者说我连上面那两个方法都没找到,好吧(其实我的ida完全不能完整加载这个node的binary(太大了),所以我没法进行函数搜索或者等等操作,一搜索就会卡死,该换电脑了)
- 跟踪字节码编译或 log 执行的字节码
- 这种情况仅限于 hook 代码也在 js 层,事实上 hook 代码是在引擎里的,所以这里的相关尝试都失败了
- 针对字节码魔改:
- diff 字节码表,使用 nodejs 自带的 log 来记录字节码记录顺序
在最后一步的尝试中我们确实知道了是在 Bytecode 的相关方法内进行了魔改,但是直到比赛结束也没有找到这个方法。
复盘
为什么说 Shino 是一个啥比
接下来揭秘一下我为什么没有找到可见字符串。
划重点:这种短字符串常量会被编译优化为上面这个样子,所以如果在 hex 里搜索resultch
(8位)是可以搜到的,但是resultchecker
不行。
菜死我了,长记性了(但凡打字慢一点就搜到了,我也搜过hex
hook 逻辑分析
修改点位于v8::internal::interpreter::BytecodeGenerator::VisitCall
下载nodejsv16.18.0找到源码:src/interpreter/bytecode-generator.cc
在 switch case 的 2 3 8,对应源码的三种调用方法:
|
|
判断函数名称中包含resultchecker
:
并且参数个数为2(还有一个this.context
):
满足以上条件的时候执行一些额外操作。
看起来操作是按位比较,每一位比较的逻辑大致如下:
|
|
比较关键的是这个:v8::internal::interpreter::BytecodeArrayBuilder::BinaryOperationSmiLiteral(v26,38,0x3E00000000LL,v39);
源码:src/interpreter/bytecode-array-builder.h
运算token定义:src/parsing/token.h
操作有38、47、48三种,分别对应:
- 38:BIT_XOR
- 47:ADD
- 48:SUB
算法流程为修改输入之后调用以下逻辑,以下逻辑执行完毕后修改回去。
|
|
调用了 475 号 Runtime 函数,参考https://zhuanlan.zhihu.com/p/431621841
|
|
通过上面的函数表可知调用了函数 v8::internal::Runtime_TypedArrayVerify,可以看到这个函数被改动过,包含一个check逻辑
逆向算法即可得到答案。
exp(有点难调)
|
|
flag{97170f6727bc6757e69eb04c045478be}