吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2148|回复: 38
上一主题 下一主题
收起左侧

[Web逆向] 某红书-4.3.2-绕过ob直面JSVMP-mns0301-详细分析

  [复制链接]
跳转到指定楼层
楼主
LiXieZengHui 发表于 2026-3-21 23:41 回帖奖励
本帖最后由 LiXieZengHui 于 2026-3-28 09:06 编辑

某红书-4.3.2-JSVMP-mns0301详细分析

接口关键字:dXNlcl9wb3N0ZWQ=

碎碎念:

由于这是笔者的第二次完整分析(为什么是第二次呢,这就说来话长了),所以简单的内容不再赘述。本文的重点在于不使用AST对ob混淆进行还原,直面ob混淆下的JSVMP,通过日志插桩的方式,分析出mns0301的生成逻辑。

不出意外,应该还会有一篇文章讲一下通过AST还原最外层JS文件的ob混淆,时间待定,希望能在下周提交2333。不过其实逆向完了发现解混淆确实不是必须的,只要明白了原理,混不混淆意义不大,更多的是一种心理上的压力。

文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口均已做脱敏处理。严正声明禁止用于商业和非法用途,否则由此产生的一切后果与作者本人无关。

下面的文章是“形”,这里讲一下VMP的内核⬇️
先说插桩位置
最简单vmp=>apply,然后依靠经验(连蒙带猜)
中级vmp=>apply+运算符,然后分析日志(查看结构性运算)
难度再增加,apply+运算符+全局栈,然后仔细分析日志+玄学

Q&A
Q:为什么要对VMP进行插桩?为什么对apply和运算符进行插桩?
A:VMP的目的在于代码混淆 + 虚拟化
当原始代码为
function encrypt(a, b) {
return a b + 1;
}
VMP会将其变为
h[r[++p]] = h[r[++p]].apply(h[r[++p]], l);
此时原始的调用关系已经完全不可见了。
这个函数调用现在变成了字节码中的一段字节,
而对于函数中的运算=>a
b + 1
在运行过程中根据 opcode 跳转到对应的运算分支(乘法分支、加法分支)逐条执行。
因此,在常规代码中我们可以直接阅读函数体来理解其功能;
而在 VMP 中,我们看不到任何函数结构,只能观察到:

  1. opcode 的变化
  2. PC的跳转
  3. 栈顶指针的移动
  4. 栈顶元素的修改
    某次执行了乘法运算,某次执行了加法运算
    这意味着我们无法静态分析,只能通过动态插桩 + 日志的方式,记录运行时的行为,再从日志中反推出原始函数的功能。
    所以,选择运算符处进行插桩,是为了捕获每一次算术/逻辑运算的操作数和结果,还原计算过程。
    又因为
    有一部分底层操作仍然依赖原生 JS 函数(如 Math.random、Array.prototype.push 等)。
    这些原生函数的调用会通过 apply 发起,因此在 apply 处插桩可以捕获所有对外部原生函数的调用,补全日志中缺失的关键信息。

再多一句嘴,对于较复杂的VMP,不要一次性插全部的桩或者说一次性全点位输出日志,
先搞清楚大致流程
再分段分块插桩会好很多
同时巧用条件断点,对一段流程多次调试生成多份日志,对照分析;
Q:我有好几份日志,但是VMP中拿时间戳或者rand进行初始化,我每次获得的日志不一样啊!?
A:通过console注入,hook rand和Date.now()或者具体使用到的函数。如果多次取用时间戳,like ts_1\ts_2\ts_3这种的
在进入关键分许区域断住,然后此时开始hook,如此可以清晰的看到哪个点位调用了ts_i,ts_i+1

在分析过程中要给自己增加信心!直面困难,见招拆招~

arr_144生成方式分析

arr_20

arr_4

window中取得_dsn对应的值,并将其转换为ASCII码


arr_16 = func(arr_24)

又是拼接而成的

[0-7]

uint32_to_LE_list(TIMESTAMP **&** 0xFFFFFFFF)

uint32_to_LE_list(math**.**floor(TIMESTAMP **/** 0x100000000))

[8-23]

[int(md5_str[i **:** i **+** 2]**,** 16) **for** i **in** range(0**,** len(md5_str) **-** 1, 2)]

arr_24→arr_16

定位计算位置

我们先定位到运算位置

点击函数,发现带我们来到了一个VM中

那么想必转换就是在这里完成的

这一看就是一个JSVMP,而且还是带有ob混淆的

那么接下来肯定逃不过插桩,但是我们现在所处的位置是VM

根本无法修改和打点

显然我们接下来需要找到这个源代码所在位置

确定代码来源

我们先取消代码格式化,看看它的本来面目

第一行非常有辨识度,我们直接copy然后全局搜索

发现结果是来自于一个网络请求

嗯哼,那么我们直接替换就行(没想到google居然把Mock的功能做进了Chrome中)

日志插桩(不依靠AST解混淆)

在Mock之前我们还需要把日志点插入到代码中

但是这里存在ob混淆。。。我们又没有自动化解混淆脚本

虽然之前写的那个改一改能用,但是额,总归还是觉得心累(其实是懒2333)

我们想一想能不能在不依靠AST的情况下进行插桩呢?

思考。。。

小红书的ob混淆很有特征,基本上赋值就是stack[stack_top_pointer] = stack[stack_top_pointer] operator value

那么理论上我们可以通过全局搜索进行打点

能够做到以上前提的是我们能够识别出stack[stack_top_pointer]

我们先观察整个代码

那么我们如何识别栈呢???这个其实特别好办,因为在进入VMP之前,需要先初始化栈,我们我们只要在VMP开始的位置进行寻找即可

OK,那么我们现在还欠缺什么?

运算相关的日志我们已经可以打了,但是函数调用,也就是apply,这个我们还没有找到点位。。。

咋办呢?这里我们稍微借用一下AST的知识

我们知道ob混淆会将所有的字符串放进一个大数组中,然后在初始化的时候有一个自执行的函数负责旋转大数组,等到要调用了就通过解密函数去大数组里调用。

根据这个思路,我们可以先动态还原出apply的真实索引,然后看谁调用了这个索引,这样就能顺藤摸瓜找到apply的位置了。

理论可行,开始实战

嗯哼,apply的真实index对应50,也就是0x32

我们在混淆后的代码中搜索,搜索到了7个结果,不多,我们可以一个一个看过去

但是更聪明的做法是,根据ob混淆的尿性,肯定不可能是大数组[0x32]这种形式,大概率是一个解密函数[0x32],而且这个解密函数一般是在当前代码块中var _0x??????? = 全局解密函数,然后在调用的时候形式为_0x???????[0x32],我们特别关注一下这种就行。

我们当前代码全局解密函数为_0x1769

OK,那么当opcode == 1f的时候,就是apply调用的位置

接下来按照惯例打上日志即可

。。。

OK,日志添加完成,我们将JS压缩为一行然后替换响应体

嗯,替换完了,但是运行了之后没效果。。。

发现存在另一个js文件

神奇的是代码一样,那么我们同步替换ds.js即可

替换完成刷新之后,我们可以看到我们要的日志来了

分析日志

在最开始先初始化了4个值

每次从arr_24中取4个元素

将这4个元素视为4个字节,按照小段序拼接为4个字节,对应一个数字,也就是uint32

value = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)

我们看第一个拼装,最后的结果是[11][233][84][156]

这其实是一个小段序拼装

在还原过程中建议像我这样输出,如果之后的运算用到了之前的值,可以很方便的使用

之后的分析过程就是依葫芦画瓢,不在此赘述,嫌麻烦直接给AI,AI会说是一个类似chacha20的算法

经过一系列莫名其妙的运算之后(其实感觉是有规律的,我菜,看不出来)

我们又发现了结构性运算

这里面必然有什么奥妙

我们我们猜测,可能是取了4个Uint32,然后把每个Uint32拆分成4个Uint8,这样就能够生成16位Uint8的数组了

我们验证一下

第一组取的是1210961166

转换成16进制为0x482DCD0E

按照两个字节长度进行拆分,[48] [2D] [CD] [0E]

转换成十进制为[72] [45] [205] [14]

嗯哼,如果是小端序存储的话,我们还要进行反序,最后还原一组为[14, 205, 45, 72]

嗯哼,针不戳,一模一样

OK,继续分析,接下来是

接下来的步骤就相当的简单了

我们可以知道103是根据随机数计算而来的

final

arr_20 = arr_4 + arr_16

arr_124

arr_124 = arr_108 + arr_16

arr_108

arr_108 = arr_97 + arr_11

arr_97

arr_97 = arr_44 + arr_53

arr_44

这里的arr_44是由多个长度或4或8的数组拼接而成

[0-3]

固定魔术头[121, 104, 96, 41]

[4-7]

[8-15]

[15-23]

有一点要注意,这里的时间戳比上面的要小。。。

[24-27]

不清晰来自哪里。。。

[28-31]

[32-35]

[35-43]

嗯,我们搜索了半天,发现了这个神似MD5的字符串的来源,是外界传入的参数

至于是不是MD5,我们一会儿研究

这个的计算逻辑是先把MD5字符串对半劈开,取前二分之一,一个8字节,每字节作为一个单位,然后将一个字节看作是一个完整的16进制数,再将这个数与之前计算使用的RAND&0xFF相异

arr_53/arr_a1

最后在开头拼接上长度

arr_11

arr_16

arr_16 = arr_4 + arr_12

arr_4

arr_10

之后的10个值也是固定运算得到的

arr_2

同为固定运算

OK,那么到此为止我们arr_144已经全部分析完成了,接下来我们关注一下接下来的内容

魔改RC4

既然长度没有发生改变,但是内容却发生了变化

排除掉简单的顺序变化,自然想到的就是RC4

但是经过测试,发现这并不是一个标准的RC4

var _0x57c9e7 = function _0x30ce91() {
    var _0x9eca1a = _0x5edc27
      , _0x50f290 = arguments;
    return _0x30ce91[_0x9eca1a(0x53)] > 0x0 || _0x30ce91['ΙII']++,
    _0x31ad27(_0x30754b, _0x30ce91[_0x9eca1a(0x7b)], _0x30ce91[_0x9eca1a(0x5b)], _0x50f290, _0x30ce91[_0x4d21fc[_0x9eca1a(0x42)]], this, null, 0x0);
};

我们跳转到_0x30ce91进行查看整个函数组成

开始是一个赋值语句,紧接着就是return

return中是逗号运算,最后是一个函数调用,这才是我们需要关注的

我们在这个位置打上断点

根据已有经验,当生成位mns0301、mns0101时,会出现这个

所以我们有选择的开闭断点

进来之后我们发现这特么的又是个ob+jsvmp

要是时间充裕的话自然可以先还原再插桩

但是我们不妨先跟着看看,毕竟我们现在只是在追踪RC4

并不是要还原所有的逻辑

这个地方已经相当接近算法位置了,应该跟一下能看到一些眉目

嗯哼,跟踪了一会儿,果然还是要被绕晕了

难道真的又要来一遍ob解混淆???补药啊,我没有自动化脚本啊呜呜呜呜

我们再仔细观察,我们刚才是在func(arr_144)的日志处发现了这个函数,

现在在这里被断住了,但不一定就是func(arr_144)啊,可能是这个虚拟机里的其他函数调用啊

毕竟根据我们刚才跟栈的时候,这里是vmp初始化的地方,我们再看一下日志

此时并没有在func(arr_144),说明我们还没有到位置,让我们一边关注日志信息,一边追踪

放开了几次,直到出现如上图

我们发现arr_144已经生成了,而外层虚拟机已经取到了_1619d69735e1d480a72d7e01c4a40b7f

感觉差不多就是这个函数了,我们此时应该开始跟踪了

一直跟一直跟,发现了

这个while循环似曾相识啊,哦,和外层一样,也是在初始化虚拟机

那么这个虚拟机初始化完成后,会是什么呢

我们继续跟踪

没跟几步,这个执行顺序已经被解密出来了

完成初始化之后就发现了一个全新的函数

丢给AI一询问,嗯哼,有点意思,AI认为是RC4

看来我们找到了加密函数了嘻嘻

为什么AI认为这个就是一个RC4呢?

因为有一个256的sboxc

然后每轮都会更新状态,每次又会向一个数组内push进当前轮的运算结果的一部分

我们可以打个日志观察一下

我们拿着日志让AI分析一下,然后生成代码即可

完美,这一部分验收通过~

base64魔改

我们可以看到在经过RC4转换之后变成了一个字符串

众所周知,当流式加密结束后输出的也是字节流而非字符串

那么一般来说,我们将字节→字符到时候,都会用到什么?

答对了,base64

那么大厂一般会怎样?

答对了,魔改编码表

我们再来定位一下,发现被跳转到了一个新的js文件中

啧啧啧,又是ob混淆+JSVMP

我们来跟一下栈

看看会发生什么

嗯哼,基本上可以确定就是这个位置

我们开始跟一下栈

嗯哼,没戏,跳转了几下就开始头晕了。。。

我们还是老老实实日志插桩吧

因为我们现在高度怀疑这个base64只是更换了编码表,根据以往的经验,我们只需要对apply位置进行插桩即可。

转换成16进制就是87,我们查找一下对87的应用

很好,就5项,我们分析一下

大概率就是这里,我们直接插桩试一下

PS:非要验证一下也没问题

嗯哼,ez,就是这儿了

依照之前的方法,我们找到apply的调用位置,然后替换代码,刷新页面,观察一下输出日志

嗯哼,就这么水灵灵的出来了~~~

只能说这玩意真就是越练越熟悉,之前弄抖音的时候插桩插的半死不活,再接着是分析小红书外层的JSVMP,只能步履蹒跚的学习AST解ob混淆(这个现在还不熟练),到现在随手找到apply位置,简单插桩,扫一眼日志,轻轻松松(狗头.jpg)

进入函数观察一下,发现这个base64的编码表挺抽象的,一半是硬编码。。。剩下一半看我们的传入

校验通过,至此我们完成了整个JSVMP的逆向,剩下的就是函数参数的来源

免费评分

参与人数 13吾爱币 +12 热心值 +10 收起 理由
Kcode + 1 我很赞同!
junjia215 + 1 + 1 用心讨论,共获提升!
bnm11 + 1 + 1 我很赞同!
fengbolee + 1 + 1 我很赞同!
GSSby + 1 + 1 非常详细,谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
soyiC + 1 + 1 用心讨论,共获提升!
aihetianshui + 1 + 1 nb
Fourseasons + 1 厉害了大佬
Yao2903 + 1 + 1 狠人
lulanlan + 1 我很赞同!
wwb66668 + 1 + 1 谢谢@Thanks!
Bizhi-1024 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| LiXieZengHui 发表于 2026-3-23 13:59 |楼主
Bizhi-1024 发表于 2026-3-23 11:47
可以看一下你的插桩是如何插的嘛?(如何判断插桩位置)

先说插桩位置
最简单vmp=>apply,然后依靠经验(连蒙带猜)
中级vmp=>apply+运算符,然后分析日志(查看结构性运算)
难度再增加,apply+运算符+全局栈,然后仔细分析日志+玄学

PS:我自问自答一下
Q:为什么要对VMP进行插桩?为什么对apply和运算符进行插桩?
A:VMP的目的在于代码混淆 + 虚拟化
当原始代码为
function encrypt(a, b) {
    return a * b + 1;
}
VMP会将其变为
h[r[++p]] = h[r[++p]].apply(h[r[++p]], l);
此时原始的调用关系已经完全不可见了。
这个函数调用现在变成了字节码中的一段字节,
而对于函数中的运算=>a * b + 1
在运行过程中根据 opcode 跳转到对应的运算分支(乘法分支、加法分支)逐条执行。
因此,在常规代码中我们可以直接阅读函数体来理解其功能;
而在 VMP 中,我们看不到任何函数结构,只能观察到:
1. opcode 的变化
2. PC的跳转
3. 栈顶指针的移动
4. 栈顶元素的修改
某次执行了乘法运算,某次执行了加法运算
这意味着我们无法静态分析,只能通过动态插桩 + 日志的方式,记录运行时的行为,再从日志中反推出原始函数的功能。
所以,选择运算符处进行插桩,是为了捕获每一次算术/逻辑运算的操作数和结果,还原计算过程。
又因为
有一部分底层操作仍然依赖原生 JS 函数(如 Math.random、Array.prototype.push 等)。
这些原生函数的调用会通过 apply 发起,因此在 apply 处插桩可以捕获所有对外部原生函数的调用,补全日志中缺失的关键信息。

再多一句嘴,对于较复杂的VMP,不要一次性插全部的桩或者说一次性全点位输出日志,
先搞清楚大致流程
再分段分块插桩会好很多
同时巧用条件断点,对一段流程多次调试生成多份日志,对照分析;能够在分析过程中给自己增加信心

点评

建议把这部分也放到正文中  详情 回复 发表于 2026-3-26 19:46
3#
Bizhi-1024 发表于 2026-3-23 11:47
可以看一下你的插桩是如何插的嘛?(如何判断插桩位置)
4#
wapj3076 发表于 2026-3-23 12:27
5#
 楼主| LiXieZengHui 发表于 2026-3-23 14:02 |楼主
感谢所有在互联网上无私分享知识的前辈们,正是因为你们的慷慨与开放,才让后来者的我得以站在巨人的肩膀上继续前行。
6#
crownT 发表于 2026-3-23 22:27
膜拜大佬
7#
xhlbudd 发表于 2026-3-24 00:06
不明觉厉,收藏起来慢慢学习~~
8#
xixicoco 发表于 2026-3-24 00:43
大佬是真有耐心,我是看一会儿这debug就眼花了
9#
 楼主| LiXieZengHui 发表于 2026-3-24 08:27 |楼主
xixicoco 发表于 2026-3-24 00:43
大佬是真有耐心,我是看一会儿这debug就眼花了

一杯茶,一包瓜子,一个混淆看一天哈哈哈哈
10#
jefflookmeair 发表于 2026-3-24 10:23
好长的帖,老实说,太复杂了看不懂
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-11 20:29

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表