瑞数vmp-动态代码生成原理
刚开始就是很好奇,为啥给了一串字符串和一个数字就可以动态生成每次都不一样却都能正常运行的文本?为啥代码一格式化就会执行进入死循环?window.$_ts
的值在动态代码生成过程中会越来越多值,它的作用是什么?动态代码中的循环加判断又是怎么生成的?又为何删除了源码的debugger后代码就运行异常?
带着好奇我直接把动态代码生成的逻辑逆向了,核心代码经过逆向后就只有大概300多行,代码整理成项目已经上传到了github,地址:https://github.com/pysunday/rs-reverse/tree/main
,其中调试可以配合loopcode
内代码进行,loopcode
里面的文件就是源码循环文件执行的代码。
1. 3种不变的字符串
- 复制到cp值给动态代码运行用的,点击这里 🔗查看,顾名思义,cp0赋值给
window.$_ts.cp[0]
,cp2赋值给window.$_ts.cp[2]
用。 - 用于生成代码的,点击这里 🔗查看,globalText1用于生成第一段代码,globalText2用于生成第二段代码。
$_ts
带入的nsd和cd值。
2. nsd的作用及应用
nsd是一串数字,每次返回的nsd都是不同的,nsd的作用主要有3个:
- 用于交换数组,如数据
[1, 2, 3]
通过nsd
值多次交换后变成[2, 3, 1]
。 - 控制
globalText
生成的多段代码前面换行的数量。 - 生成变量数组,即
window.$_ts.cp[1]
的值,也是基于交换数组能力将固定数组打乱顺序。
nsd的处理对应代码 🔗
function getScd(scd) {
return function(look) {
scd = 15679 * (scd & 65535) + 2531011;
return scd
}
}
即初始化一次这个处理函数,生成包含变量scd的闭包函数,通过闭包内变量的不断赋值产生关联的数据值,这有点像斐波那契数列。
2.1. nsd控制数组乱序(交换数组)
结合代码不难理解,就是将数组经过多次交换操作以达到打乱数组顺序,其中交换的下标就是每次取一个新的scd值对数组长度取模。
function arraySwap(arr, scd) {
const knarr = [ ...arr ];
let len = knarr.length, idx;
const _scd = typeof scd === 'function' ? scd : getScd(scd);
while (len-- > 1) {
idx = _scd() % len;
const temp = knarr[len];
knarr[len] = knarr[idx];
knarr[idx] = temp;
}
return knarr;
}
2.2. 基于nsd生成变量数组
其中参数num表示变量数组长度,flag就是nsd值,首先生成num长度的固定值数组,然后通过前面定义的打乱顺序的方法arraySwap
进行打乱顺序操作。
function grenKeys(maxlen) {
const keys = "_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split('');
const ans = [];
for (let i = 0; i < keys.length; i ++) {
for (let j = 0; j < keys.length; j ++) {
ans.push('_$' + keys[i] + keys[j]);
if (ans.length === maxlen) return ans;
}
}
return ans;
}
function grenKeys(num, flag) {
const keynames = grenKeys(num);
return arraySwap(keynames, flag)
}
2.3. 控制换行数量
控制globalText1
生成多段代码,每段代码前面都有不大于5行的换行,这个换行数据就是通过nsd值控制的,由于scd的取值结果是根据调用次数动态生成的,因此每次产生的换行数量也是不同的。
const scd = getScd();
// 一段代码
'\n\n\n\n\n'.substring(0, scd() % 5);
// 一段代码
'\n\n\n\n\n'.substring(0, scd() % 5);
3. $_ts
部分值的初始化
点击查看代码 🔗可以看到对$_ts
的部分值进行初始化,其中可以看到一个多此一举的grenJf
方法返回结果会赋值给$_ts.jf
,这个属性值之前的博文有做分析,是用来判断源码是否经过格式化了,这里再补充下,代码如下:
function grenJf () {
const flags = [1, 0, 0];
// 格式化检测通过执行
const flag = --flags[1];
// 反码检测通过执行
return !flag;
}
其中const flag = --flags[1]
的前后都有两次检测,前面的是格式化检测
,正如前面博文介绍的一样检测是否格式化,后面的是序列化检测
,即源码通过某些操作后,如console.log
输出会将反码自动解码,如16进制的字符串自动转化为10进制的字符串,此时通过toString
后返回的结果也是10进制的,没有16进制字符串则检测不通过,所有格式化代码时需要注意下。
4. 定义处理globalText的方法
点击查看代码 🔗
代码的作用其实与getScd方法类似,需要区别的是这里的闭包变量是globalText的定位游标,游标在哪即取出globalText中对应的值,对游标的处理分为单个(getCode)的和一段(getList、getLine)的。
5. 生成动态代码
前面的相关方法知道作用后就可以看如何生成动态代码的原理了,生成动态代码的核心程序200行不到,具体看代码:点击查看代码 🔗
动态代码的生成分为两部分,即两个globalText分别生成不同的代码,其中基本是通过globalText1生成的。
由于代码变量具名没有意义且为了方便源码逆向时的定位,因此定义了mate
和data
两种数据类型,其中存储值的key与源码保持一致,可以结合文件夹https://github.com/pysunday/rs-reverse/blob/main/loopcode/
内的文件做分析
5.1. 入口代码分析
入口代码可以看文件https://github.com/pysunday/rs-reverse/blob/main/loopcode/run_%24a9_ENTER()_0_1.js
,其中_$f2(69)
即进入解析方法,该代码只执行一次。对应代码文件中的run()
方法。
5.2. 解析globalText1
对应代码文件中的parseGlobalText1
方法,optext
、opmate
和opdata
是三个辅助方法,解析前需要初始化(游标与数据重置)。
然后定义全局变量,对应文件https://github.com/pysunday/rs-reverse/blob/main/loopcode/run_%24f7_%24f2(69)_0_2.js
的47行至55行:
opmate.setMate('G_$e4', true);
opmate.setMate('G_$$c', true);
opmate.setMate('G_$dK', true);
opmate.setMate('G_$kv', true);
opmate.setMate('G_$cR', true);
定义keycodes
,keycodes
与前面grenKeys
生成的keynames
类似,keynames
是映射变量名,keycodes
是映射代码片段:
this.keycodes.push(...optext.getLine(optext.getCode() * 55295 + optext.getCode()).split(String.fromCharCode(257)));
opmate.setMate(); // 代码片段读取不是连续的,中间间隔了一个字符,需要注意
this.keycodes.push(optext.getLine(optext.getCode() * 55295 + optext.getCode()));
取出代码片段数量并循环生成生成对应数量的代码段,代码段数量也可以在网页动态代码执行前查看$_ts.aebi
数组数量:
opmate.setMate('G_$gG', true);
for (let i = 0; i < opmate.getMateOri('G_$gG'); i++) {
this.gren(i, codeArr);
}
this.gren
方法不难看出前部分继续解析globalText1
生成下标数字数组,从if (current) {
代码开始生成代码片段并推入this.codeArr
数组中,可以调试查看,需要关注代码const codelist = this.grenIfelse(0, opmate.getMateOri('_$bf'), []);
,其中方法grenIfelse
就是生成我们看到头痛的循环加if/else
代码。
这个代码的算法就比较有意思了,通过深度优先递归最终生成代码,通过净化代码,我们也可以生产这种代码,净化代码如下:
const grenIfelse = function (start, end, codeArr) {
const arr8 = [4, 16, 64, 256, 1024, 4096, 16384, 65536];
const key = 'key';
let text;
let diff = end - start;
if (diff == 0) {
return codeArr;
} else if (diff == 1) {
} else if (diff <= 4) {
text = "if(";
end--;
for (; start < end; start++) {
codeArr.push(text, key, "===", start, "){");
text = "}else if(";
}
codeArr.push("}else{");
codeArr.push("}");
} else {
const step = arr8[arr8.findIndex(it => diff <= it) - 1] || 0;
text = "if(";
for (; start + step < end; start += step) {
codeArr.push(text, key, "<", start + step, "){");
grenIfelse(start, start + step, codeArr);
text = "}else if(";
}
codeArr.push("}else{");
grenIfelse(start, end, codeArr);
codeArr.push("}");
}
return codeArr;
}
可以复制净化代码在浏览器运行,后运行调用方法grenIfelse(0, 10, []).join('')
,生成if/else
代码如:if(key<4){if(key===0){}else if(key===1){}else if(key===2){}else{}}else if(key<8){if(key===4){}else if(key===5){}else if(key===6){}else{}}else{if(key===8){}else{}}
5.3. 解析globalText2
globalText2
相比1会简单很多,重置op*
后只执行了几行代码,代码如下:
parseGlobalText2() {
const { opmate, opdata, optext, keynames, getCurr } = this;
optext.init(0, immutext.globalText2);
opdata.init();
opmate.init();
opmate.setMate('G_$ht', true);
const keycodes = optext.getLine(optext.getCode()).split(String.fromCharCode(257));
return this.special(optext.getList().data, keycodes, this.keynames).join('');
}
5.4. 生成$_ts.cp[3]
这个值也很关键,是我们将源码中的debugger删除后代码无法运行的关键,在动态代码中会将这个值拿去做运算,如果值不对就无法正确运行。原理与文件取hash值类似,即将动态代码中每隔100字符取出一个后计算ascii码,再将这些ascii码值相加生成一个代码标识值,代码如下:
let flag = 0;
for (let i = 0; i < codeStr.length; i += 100) {
flag += codeStr.charCodeAt(i)
}
this.$_ts.cp[3] = flag;
5.5. 生成$_ts.cp[4]
这个存的是生成代码所用的时间,盲猜会判断这个值的大小判断是不是代码被下过断点了,具体等逆向动态代码生成cookie的原理后看。
6. $_ts
的生成与作用整理
整理见下面的json代码,未注释说明的都是固定值
{
"cd": "原$_ts.cd值不变",
"cp": [
"源码固定值,即前文提到的固定文本cp0",
"变量名组成的数组",
"源码固定值,即前文提到的固定文本cp2",
"动态代码标识,类似hash值,值不对会造成动态代码运行异常",
"生成动态代码所用的毫秒时间",
undefined,
""
],
"jf": "布尔值,标记是否格式化,false表示未格式化,值不对会造成动态代码运行异常",
"aebi": "由多个数字数组组成的数组,数组长度与代码片段相关,数字数组由globalText1解析得出",
"scj": [],
"lcd": undefined,
"nsd": undefined
}
7. 总结
通过代码逆向,动态代码生成的原理也都一清二楚了。接下来我们可以格式化代码做逆向开发方便不少,也可以将while/if/else
的代码优化成方便调试的代码格式,导致程序执行异常的主要有$_ts.cp[3]
和$_ts.jf
,当然还有动态代码中对执行环境的检测已经动态代码本身的防格式化检测,需要在接下来继续逆向动态cookie的生成原理时发现。