抖音 bogus jsvmp纯算
根据代码进行调试可以发现J是vmp的装载器```
function J(t, r, e, n) {
return (
Z.length ||
(function (t) {
var r = (function (t) {
for (var r = atob(t), e = 0, n = 4; n < 8; ++n)
e += r.charCodeAt(n);
return { d: C(Uint8Array.from(r.slice(8), _, e % 256)), i: 0 };
})(t);
((Z.length = 0), (z.length = 0), V.clear());
for (var e = W(r), n = 0; n < e; ++n) Z.push(K(r));
var o = W(r);
for (n = 0; n < o; ++n) {
for (
var i = W(r),
u = Boolean(W(r)),
s = new Array(),
c = W(r),
a = 0;
a < c;
++a
)
s.push();
for (var f = new Array(), l = W(r), p = 0; p < l; ++p)
f.push(W(r));
z.push();
}
})(
"字符串",
),
X(z, r, e, n)
);
```
实际的执行器在function X(t, r, e, n),看代码首先内部会执行g函数进行初始化
o为指令流,i为严格模式判断,s是变量表,数组0是外层作用域,1是arguments,2开始拷贝参数
u,异常处理表,a,pc指针,f,异常处理状态,l异常对象
```
function g(t, r, e, n) {
var p = Math.min(e.length, t)
, v = {};
Object.defineProperty(v, "length", {
value: e.length,
writable: !0,
enumerable: !1,
configurable: !0
}),
o = t,
i = t,
u = t,
s = ;
for (var h = 0; h < p; ++h)
s.push(e);
if (i)
for (c = r,
h = 0; h < e.length; ++h)
v = e;
else {
c = null == r ? globalThis : Object(r);
var g = function(t) {
t < p ? Object.defineProperty(v, t, {
get: function() {
return s
},
set: function(r) {
s = r
},
enumerable: !0,
configurable: !0
}) : v = e
};
for (h = 0; h < e.length; ++h)
g(h)
}
a = 0,
f = 0,
l = void 0
}
```
初始化后开始进入d函数进行执行,这里vmp为了混淆判断分支流,用了大量的if,我们可以用ast拍平变成switch-case结构
```
// 根据指定 opcode,从原始 if/else 分发树里取出真正会执行的 handler。
function resolveStatementForOpcode(statementNode, opcodeValue) {
if (types.isIfStatement(statementNode)) {
let matched;
try {
matched = evaluateTest(statementNode.test, opcodeValue);
} catch {
// handler 内部也可能有普通业务判断,例如:
//
// if ("function" != typeof n) ...
//
// 这种判断和 opcode t 无关,不能静态选择分支,必须原样保留。
return ;
}
const next = matched ? statementNode.consequent : statementNode.alternate;
if (!next) {
return [];
}
return resolveStatementForOpcode(next, opcodeValue);
}
if (types.isBlockStatement(statementNode)) {
return statementNode.body.flatMap((item) =>
resolveStatementForOpcode(item, opcodeValue),
);
}
if (types.isExpressionStatement(statementNode)) {
return [
types.expressionStatement(
simplifyExpression(statementNode.expression, opcodeValue),
),
];
}
if (types.isReturnStatement(statementNode)) {
const cloned = types.cloneNode(statementNode, true);
if (cloned.argument) {
cloned.argument = simplifyExpression(cloned.argument, opcodeValue);
}
return ;
}
// 其他语句类型暂时原样克隆。
// 如果后续遇到不准确的 case,再针对那个结构补处理逻辑。
return ;
}
function evaluateTest(testNode, opcodeValue) {
if (!types.isBinaryExpression(testNode)) {
throw new Error(`暂不支持的条件类型:${testNode.type}`);
}
const left = testNode.left;
const right = testNode.right;
const operator = testNode.operator;
// 支持 t < number。
if (isOpcodeIdentifier(left)) {
const rightValue = numericValue(right);
if (rightValue === undefined) {
throw new Error(`条件右侧不是数字:${generator(testNode).code}`);
}
switch (operator) {
case "<":
return opcodeValue < rightValue;
case "<=":
return opcodeValue <= rightValue;
case ">":
return opcodeValue > rightValue;
case ">=":
return opcodeValue >= rightValue;
case "===":
case "==":
return opcodeValue === rightValue;
case "!==":
case "!=":
return opcodeValue !== rightValue;
default:
throw new Error(`暂不支持的操作符:${operator}`);
}
}
// 支持 number === t / number !== t。
if (isOpcodeIdentifier(right)) {
const leftValue = numericValue(left);
if (leftValue === undefined) {
throw new Error(`条件左侧不是数字:${generator(testNode).code}`);
}
switch (operator) {
case "<":
return leftValue < opcodeValue;
case "<=":
return leftValue <= opcodeValue;
case ">":
return leftValue > opcodeValue;
case ">=":
return leftValue >= opcodeValue;
case "===":
case "==":
return leftValue === opcodeValue;
case "!==":
case "!=":
return leftValue !== opcodeValue;
default:
throw new Error(`暂不支持的操作符:${operator}`);
}
}
throw new Error(`条件里没有 opcode 变量 t:${generator(testNode).code}`);
}
```
处理完成后得到了
```
function d() {
for (;;) {
// 读取当前 opcode,并让 pc 后移一位。
var t = o;
switch (t) {
case 0:
// opcode 0: handler 逻辑省略
break;
case 1:
// opcode 1: handler 逻辑省略
break;
case 2:
// opcode 2: handler 逻辑省略
break;
case 3:
// opcode 3: handler 逻辑省略
break;
case 4:
// opcode 4: handler 逻辑省略
break;
case 5:
// opcode 5: handler 逻辑省略
break;
case 6:
// opcode 6: handler 逻辑省略
break;
case 7:
// opcode 7: handler 逻辑省略
break;
// ...
case 75:
// opcode 75: handler 逻辑省略
break;
default:
// 未知 opcode / 默认处理逻辑
break;
}
}
}
```
可以看到一共75个opcode+fallback code,这个时候一定要替换文件测试一下,确保算法依然是正常的
关于ast处理vmp目前主要分为三类
1.纯静态生成,读取opcode+对应参数,直接脱离网页的代码和执行逻辑,静态decompile,给到夯
2.解释器改造为生成器,会处理opcode的时候直接修改原stack代码,实现还原,破坏了原来的基本流程,需要较高的代码能力去核对
3.采用旁路式,在执行代码的同时执行ast代码,更利于调试和逆向分析
这里采用旁路式,那我们需要在浏览器环境下引入一个ast的sdk
```
import * as types from "@babel/types";
import generateModule from "@babel/generator";
const generator = generateModule.default || generateModule;
globalThis.AST = {
types,
generator,
generate(node, options) {
return generator(node, options).code;
},
};
```
构建出ast_bundle.js,这里推荐制作一个脚本猫脚本来自动注入提供环境
构造完验证一下
```
window.__AST_SDK_INJECTED__
AST.generate(AST.types.stringLiteral("dfp"))
'"dfp"'
```
接下来处理opcode,大部分都是比较简单的,例如opcode 24是v = typeof v;,从堆栈取出来一个值进行typeof然后塞回去,我们就写一下对应的ast代码就可以
```
case 24:
// opcode 24
// 原逻辑:对当前栈顶做 typeof,结果覆盖当前栈顶。AST:生成 UnaryExpression 'typeof'。
v = typeof v;
if (__astTypes) {
var __astArg = __astStack;
__astStack = __astTypes.unaryExpression("typeof", __astArg, true);
}
break;
```
同样类似的还有
```
case 21:
// opcode 21
// 原逻辑:弹出右值 E,对当前栈顶做减法 -=。AST:生成 BinaryExpression '-'。
E = v, v -= E;
if (__astTypes) {
var __astRight = __astStack;
var __astLeft = __astStack;
__astStack = __astTypes.binaryExpression("-", __astLeft, __astRight);
}
break;
```
一些基础的CRUD的opcode大约40个,全部处理完还有30多个没处理,我们要靠debugger去分析对应代码,可以把没处理的顶部都加一个debugger,以便调试
这里有一个需要注意的就是case 63是一个递归调用,这里我们主要目的是跟踪,所以不进行处理,只制造一个空function表示,如果有需求后续考虑加强代码递归调用
```
E = D(o, s), v[++p] = E;
```
这些都是比较简单的,剩下的一些就是cfg bound的处理了
目前也是有两种处理方案
1.按看雪的https://bbs.kanxue.com/thread-276268-1.htm 文章,如果遇到if就开始创建一个block
同样结构是这样的
```
如果 xxx >10 为false则跳转语句B
语句A
跳转语句C
语句B
语句C
```
所以无论如何,读取跳转语句的last语句,即可直到该分支的结束位置,为了防止递归,最好用stack处理
2.运行时收集数据结构,生成完整pc状态机,后续可以识别各种分支以便进行美化,例如
```
function _vm_func_12(args_0, args_1) {
var pc = 0;
var stack = [];
var sp = -1;
while (true) {
switch (pc) {
case 0:
...
pc = 8;
break;
case 8:
...
if (cond) pc = 20;
else pc = 10;
break;
case 20:
...
return stack;
}
}
}
```
我们其实这里也可以更折中一点,PC状态机的方案可以无视各种分支流的处理,也具有可读性,所以直接在执行过程中生成pc状态机,但不追求构建完整状态机,仅保证最小可用,缺点就是不会去判断什么时候分支流程结束,但是因为我们目标是还原,而非完美还原,所以到这里已经可以满足需求
例如
```
var x = o,
S = v,
P = [];
for (var j in S) P.push(j);
s = ;
```
我们可以构造出来
```
var __astOpcodePc = a - 1;
if (__astTypes) {
__astBeginOpcode(__astOpcodePc);
}
// opcode 3
var x = o,
S = v,
P = [];
for (var j in S) P.push(j);
s = ;
if (__astTypes) {
--__astP;
}
if (__astTypes) {
__astRecordNormalExit(a);
}
```
其中
```
function __astBeginOpcode(pc) {
__astCurrentPc = pc;
__astGetBlock(pc).executed = true;
}
function __astRecordNormalExit(nextPc) {
if (__astCurrentPc < 0) return;
var block = __astGetBlock(__astCurrentPc);
var lastExit = block.exits;
if (!lastExit) {
block.exits.push({
type: "next",
to: nextPc
});
}
}
```
这里注意当处理状态机的时候,那所有的opcode都要加上对应的信息桩
```
while (true) {
switch (pc) {
case 0:
pc = 1;
break;
case 1:
var _s0_3 = this;
_s0_2 = false;
pc = 4;
break;
case 2:
var _s0_2 = 1;
_s0_4 = [];
_s0_4 = "=";
_s0_3 = "";
_s0_3 = [];
_s0_3 = [];
_s0_3 = [];
_s0_4 = [];
_s0_4 = "=";
pc = 4;
break;
case 3:
Object.defineProperty({}, "name", {
value: "Other",
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty({}, "name", {
value: "Other",
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty({}, "name", {
value: "Other",
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty({}, "name", {
value: "Other",
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty({}, "name", {
value: "Other",
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty({}, "name", {
value: "Other",
writable: true,
configurable: true,
enumerable: true
});
if (_s1_8) {
pc = 12;
} else {
--sp;
pc = 5;
}
break;
case 4:
_s0_2 = location.href;
_s0_2 = window.outerWidth;
_s0_2 = window.outerWidth;
if (navigator.storage) {
--sp;
pc = 6;
} else {
pc = 16;
}
break;
case 5:
if (null == _s0_3) {
pc = 16;
} else {
--sp;
pc = 7;
}
break;
case 1827:
var _s0_93 = _s2_1.apply(undefined, );
pc = 1830;
break;
case 1833:
controlState = 2;
controlValue = _s0_93;
return;
```
生成了这样的状态机代码,然后根据返回值判断可以确定是bogus,接下来根据_s2_1的函数来看,因为目前没有做opcode 63 vm递归生成的映射处理,所以我们需要再处理一下,这些都是纯体力话,就不多赘述了,我们可以利用之前的opcode的执行记录pc来回溯目前首条执行时到底时哪个stack来形成关联
目前还有许多空跳,以及对普通流进行合并,这里我的方法是把所有的pc指针的case都塞到一个map里,然后进行排序,此时如果某一pc有多入口,则视为非普通语句,存在控制流,如果存在单一入口则视为普通语句,属于比较基本的呆呆的CFG Bound处理方法,这里要注意不能把存在两个入口的case进行合并
```
case 1820:
pc = 1823;
break;
// frame z
// frame z
case 1823:
pc = 1825;
break;
// frame z
// frame z
case 1825:
pc = 0;
break;
// frame z
// frame z
```
全部处理完就得到了类似于
```
case 1745:
var _s0_90 = _s1_20.apply(undefined, );
pc = 0;
break;
// flow block 1788 ] merged=1788,1789,1792,1795
// frame z
case 1788:
pc = 0;
break;
// flow block 1797 ] merged=1797,1799,1801
// frame z
case 1797:
pc = 0;
break;
// flow block 1803 ] merged=1803,1806,1809,1812,1813,1816,1817,1820,1823,1825
// frame z
case 1803:
var _s0_91 = _s2_2.apply(undefined, ]), String.fromCharCode.apply.apply(String.fromCharCode, .concat.apply([], ), _s1_6.apply(undefined, )])])]);
var _s0_92 = _s0_89 + _s0_91;
pc = 0;
break;
// flow block 1827 ] merged=1827,1830
// frame z
case 1827:
var _s0_93 = _s2_1.apply(undefined, );
pc = 1833;
break;
// flow block 1833 ] merged=1833
// frame z
case 1833:
controlState = 2;
controlValue = _s0_93;
return;
break;
}
}
```
这样的格式,接下来开始处理cfg bound,结合一下,我们就可以得到,这里是一个纯耐心的工作,需要尝试自己找规律,建 CFG,算支配树,找段内回边,然后把原来的代码拼回块里,因为代码多到爆炸,所以这里就
```
function recovered_z150_structured() {
// block pc 0
var _s0_12 = 11;
// block pc 14
if (window.onwheelx) {
// block pc 16
// block pc 22
if (window.onwheelx._Ax) {
// block pc 24
xxxxxxx
// block pc 52
if (_s0_13 === void 0) {
// goto pc 54 (external frame)
} else {
// block pc 59
// block pc 66
if (_s0_13.writable === false) {
// block pc 68
_s0_12 = 3;
// block pc 77
var _s0_14 = Date.now.apply(Date, []);
var _s0_15 = new _s2_3();
_s0_16 = _s2_4.apply(undefined, []);
// block pc 114
if (_s2_5.track.mode !== 0) {
// goto pc 116 (external frame)
} else {
// block pc 120
xxxxx
// block pc 250
if (navigator.vendorSubs === null) {
// block pc 259
if (_s0_11 === void 0) {
// goto pc 261 (external frame)
} else {
// block pc 266
// block pc 271
if (_s0_11.ink) {
// block pc 275
var _s0_22 = _s0_11.ink;
var _s0_23 = ;
var _s0_24 = 41;
var _s0_25 = _s0_10.split.apply(_s0_10, ["."]).map.apply(_s0_10.split.apply(_s0_10, ["."]), );
var _s0_26 = (new Date().getTime.apply(new Date(), []) - 1721836800000) / 1000 / 60 / 60 / 24 / 14 >> 0;
var _s0_27 = _s1_21.apply(undefined, []);
var _s0_28 = 2;
// block pc 365
if (_s1_3 > 0) {
// block pc 367
_s0_28 = _s0_14 - _s1_3 + 3 & 255;
// block pc 383
var _s0_29 = _s0_14 & 255;
var _s0_30 = _s0_14 >> 8 & 255;
var _s0_31 = _s0_14 >> 16 & 255;
var _s0_32 = _s0_14 >> 24 & 255;
var _s0_33 = _s0_14 / 256 / 256 / 256 / 256 & 255;
var _s0_34 = _s0_14 / 256 / 256 / 256 / 256 / 256 & 255;
var _s0_35 = _s0_16 % 256 & 255;
var _s0_36 = _s0_16 / 256 & 255;
var _s0_37 = _s2_13.apply(undefined, );
var _s0_38 = _s0_37["4"] & 255;
var _s0_39 = _s0_37["4"] >> 8 & 255;
xxxxxx
var _s0_50 = 3;
var _s0_51 = _s0_18;
// loop header pc 639, back-edge from pc 663
while (_s0_51 === 11) {
// block pc 647
// block pc 661
if (_s0_50 < _s0_19.length) {
// block pc 663
_s0_51 = _s0_18;
continue; // -> pc 639
} else {
break; // -> pc 675
}
}
// block pc 682
// block pc 690
if (_s0_37["4"] & 2) {
// goto pc 692 (external frame)
} else {
// block pc 697
var _s0_52 = _s0_19["10"];
var _s0_53 = _s0_19["19"];
var _s0_54 = 4;
var _s0_55 = _s0_19;
// block pc 734
if (_s0_55 === 8) {
// goto pc 736 (external frame)
} else {
// block pc 771
// block pc 779
if (_s0_37["4"] & 4) {
// goto pc 781 (external frame)
} else {
// block pc 786
var _s0_56 = _s0_21["11"];
var _s0_57 = _s0_21["21"];
var _s0_58 = 5;
var _s0_59 = _s0_21;
// block pc 823
if (_s0_59 === 12) {
// goto pc 825 (external frame)
} else {
// block pc 860
// block pc 868
if (_s0_37["4"] & 8) {
// goto pc 870 (external frame)
} else {
// block pc 875
var _s0_60 = _s0_22 & 255;
var _s0_61 = _s0_22 >> 8 & 255;
var _s0_62 = _s0_22 >> 16 & 255;
var _s0_63 = _s0_22 >> 24 & 255;
var _s0_64 = _s0_22 / 256 / 256 / 256 / 256 & 255;
var _s0_65 = _s0_22 / 256 / 256 / 256 / 256 / 256 & 255;
var _s0_66 = _s0_12;
xxxxxx
var _s0_75 = {};
var _s0_76 = _s1_13.apply(undefined, );
var _s0_77 = _s1_12.apply(undefined, );
var _s0_78 = _s0_77.length;
var _s0_79 = _s0_78 & 255;
var _s0_80 = _s0_78 >> 8 & 255;
var _s0_81 = (_s0_14 + 3 & 255) + ",";
var _s0_82 = _s1_12.apply(undefined, );
var _s0_83 = _s0_82.length;
var _s0_84 = _s0_83 & 255;
var _s0_85 = _s0_83 >> 8 & 255;
var _s0_86 = _s2_37.apply(undefined, ), _s0_2]);
var _s0_87 = _s0_86["0"] ^ _s0_86["1"] ^ _s0_86["2"] ^ _s0_86["3"] ^ _s0_86["4"] ^ _s0_86["5"] ^ _s0_86["6"] ^ _s0_86["7"] ^ _s0_24 ^ _s0_26 ^ _s0_27 ^ _s0_28 ^ _s0_29 ^ _s0_30 ^ _s0_31 ^ _s0_32 ^ _s0_33 ^ _s0_34 ^ _s0_35 ^ _s0_36 ^ _s0_38 ^ _s0_39 ^ _s0_40 ^ _s0_41 ^ _s0_42 ^ _s0_43 ^ _s0_44 ^ _s0_45 ^ _s0_56 ^ _s0_47 ^ _s0_48 ^ _s0_49 ^ _s0_51 ^ _s0_52 ^ _s0_53 ^ _s0_55 ^ _s0_56 ^ _s0_57 ^ _s0_59 ^ _s0_60 ^ _s0_61 ^ _s0_62 ^ _s0_63 ^ _s0_64 ^ _s0_65 ^ _s0_66 ^ _s0_67 ^ _s0_68 ^ _s0_69 ^ _s0_70 ^ _s0_71 ^ _s0_72 ^ _s0_73 ^ _s0_74 ^ _s0_79 ^ _s0_80 ^ _s0_84 ^ _s0_85;
var _s0_88 = .concat.apply(, ), _s1_6.apply(undefined, ), ]);
var _s0_89 = String.fromCharCode.apply.apply(String.fromCharCode, )])]);
var _s0_90 = _s1_20.apply(undefined, );
var _s0_91 = _s2_2.apply(undefined, ]), String.fromCharCode.apply.apply(String.fromCharCode, .concat.apply([], ), _s1_6.apply(undefined, )])])]);
var _s0_92 = _s0_89 + _s0_91;
var _s0_93 = _s2_1.apply(undefined, );
// block pc 1833
controlState = 2;
controlValue = _s0_93;
return;
}
}
}
}
}
} else {
// goto pc 383 (already emitted)
}
} else {
// goto pc 273 (external frame)
}
}
} else {
// block pc 252
// goto pc 259 (already emitted)
}
}
} else {
// goto pc 72 (external frame)
}
}
} else {
// block pc 45
// goto pc 52 (already emitted)
}
} else {
// goto pc 77 (already emitted)
}
} else {
// goto pc 22 (already emitted)
}
}
```
类似对其他函数进行同样的处理,在合适的地方下断点进行分析,最后将所有代码聚集到一起进行分析
可以知道bogus采用了SM3 + 字节抽样 → 拼环境字节 → 随机展开 → RC4 → s4 base64 → 188 字符
实现bogus纯算
!(data/attachment/forum/202608/03/002521u55kcmmg46xklp4f.png) 天不生我李恒道,逆道万古如长夜 王一之 发表于 2026-8-3 09:33
天不生我李恒道,逆道万古如长夜
安排!
页:
[1]