网站设计心的网站建设 网络推广
- 作者: 五速梦信息网
- 时间: 2026年04月20日 07:29
当前位置: 首页 > news >正文
网站设计心的,网站建设 网络推广,wordpress数据列表模板,电子商务网站建设与管理教材目录 一、常量的混淆原理1.1 对象属性的两种访问方式1.2 十六进制字符串1.3 Unicode字符串1.4 字符串的ASCII码混淆1.5 字符串常量加密1.6 数值常量加密二、增加 JS 逆向者的工作量2.1 数组混淆2.2 数组乱序2.3 花指令2.4 jsfuck三、代码执行流程的防护原理3.1 流程平坦化3.2 …目录 一、常量的混淆原理1.1 对象属性的两种访问方式1.2 十六进制字符串1.3 Unicode字符串1.4 字符串的ASCII码混淆1.5 字符串常量加密1.6 数值常量加密二、增加 JS 逆向者的工作量2.1 数组混淆2.2 数组乱序2.3 花指令2.4 jsfuck三、代码执行流程的防护原理3.1 流程平坦化3.2 逗号表达式混淆四、其他代码防护方案4.1 eval加密4.2 内存爆破4.3 检测代码是否格式化 一、常量的混淆原理 示例代码 Date.prototype.format function (formatStr) { let str formatStr; let Week [日, 一, 二, 三, 四, 五, 六]; str str.replace(/yyyy|YYYY/, this.getFullYear()); // console.log(str); str str.replace(/MM/, (this.getMonth() 1) 9 ? (this.getMonth() 1) .toString() : 0 (this.getMonth() 1)); // console.log(str); str str.replace(/dd|DD/, this.getDate() 9 ? this.getDate().toString() : 0 this.getDate()); // console.log(str); return str; } // console.log(new Date()); // 2022-04-25T05:25:36.856Z console.log(new Date().format(yyyy-MM-dd)); //2024-05-01 console.log(new Date().getFullYear()) //2024 console.log(new Date().getMonth()) //4 当前月份为结果1 //输出结果 2022-04-25上面的代码用于格式化时间这段代码整体上来讲逻辑简单清晰即在 Date 的原型对象上增加了一个 format 方法当实例化一个 Date 对象后就可以直接调用从 Date 原型对象上继承过来的 format 方法。上面代码没有经过任何处理任何查看脚本的开发者 js0基础的不算嘿嘿 都可以清楚地理解本段代码内容假如这是某网站开发人员编写的一段关键代码那么在代码发布后很容易被第三方破解利用从而引发安全问题因此学习 JS 代码的防护技术就显得格外重要了。 ps学习 JavaScript 混淆原理是非常有必要的原因 学好 AST 混淆和还原 JavaScript 代码的基础招聘要求越来越多的招聘要求爬虫人员懂 JavaScript 防护技术逆向是越来越卷了 1.1 对象属性的两种访问方式 示例代码 let person { name: amo, age: 18, address: 重庆市红鼎国际, eat: function () { console.log(this.name ~eating) } } // ①: person.namename是一个标识符必须明确出现在代码中不能进行加密与拼接 console.log(person.name) person.eat() // ②: person[name]该种方式name是一个字符串既然是字符串访问的时候就可以进行加密与拼接 // 在js混淆中一般会选择用这种方式来访问属性操作空间更大 console.log(person[name]) personeat访问对象的方法也可以通过 ** []** 的方式因为对象的方法可以看作特殊的属性它是一种值为函数的属性。将 ** 一、常量的混淆原理** 中的示例代码可以转换为如下形式 let window globalThis; // Date.prototype.format function (formatStr) { Date[prototype][format] function (formatStr) { let str formatStr; let Week [日, 一, 二, 三, 四, 五, 六]; // str str.replace(/yyyy|YYYY/, this.getFullYear()); str strreplace; // console.log(str); // str str.replace(/MM/, (this.getMonth() 1) 9 ? (this.getMonth() 1) // .toString() : 0 (this.getMonth() 1)); str strreplace; // console.log(str); // str str.replace(/dd|DD/, this.getDate() 9 ? this.getDate().toString() : 0 this.getDate()); str strreplace; // console.log(str); return str; } // console.log(new Date()); // 2022-04-25T05:25:36.856Z console.log(new windowDateformat); //2024-05-01 console.log(new windowDategetFullYear) //2024 console.log(new windowDategetMonth) //4 当前月份为结果1 //输出结果 2022-04-25Date 是 JS 的内置对象在 JS 中很多内置对象都是 window 的属性 上面的代码由于笔者是在 node.js 中运行的故将 window 指向了 globalThis 所以 JS 中的内置对象和客户端 JS 中的 DOM 对于 JS 的防护与逆向极为重要。在真实浏览器环境中代码中定义的全局变量 var 都是全局对象 window 的属性定义的全局函数都是全局对象 window 的方法全局对象的属性或者方法在调用时可以省略全局对象名例如 window.btoa(a)等同于 btoa(a)如果要把 btoa变为字符串前面就必须加 window如上面的示例代码** new windowDate** 1.2 十六进制字符串 改变对象属性的访问方式后代码的阅读性仍然较高要继续进行复杂化处理。因为 JS 中的字符串支持以十六进制形式表示所以可以用十六进制形式代替原有的字符串。如 amo可以表示成 \x61\x6d\x6f其中字符 a转换为字节再用十六进制表示就是 61字符 a的 Hex 形式的 ASCII 码 。ps在 JavaScript 中使用 \x十六进制来定义一个十六进制的字符串字面量。 可以使用以下代码完成十六进制字符串的转换。 function charToHex(characters) { let hexString for (let i 0; i characters.length; i) { // ①: charCodeAt()方法用来取出字符串中对应索引字符的ASCII码 // ②: toString(16)转换为十六进制 const hexCode characters[i].charCodeAt(0).toString(16); // ③与\x进行拼接 hexString \x\({hexCode} } return hexString; } // 示例代码 const codes amoGood; const hexString charToHex(codes); console.log(hexString)将 ** 1.1 对象属性的两种访问方式** 中的代码转换为如下形式 let window globalThis; // Date.prototype.format function (formatStr) { // Date[prototype][format] function (formatStr) { Date[\x70\x72\x6f\x74\x6f\x74\x79\x70\x65][\x66\x6f\x72\x6d\x61\x74] function (formatStr) { let str formatStr; let Week [日, 一, 二, 三, 四, 五, 六]; // str str.replace(/yyyy|YYYY/, this.getFullYear()); // str str[replace](/yyyy|YYYY/, this[getFullYear]()); str str[\x72\x65\x70\x6c\x61\x63\x65](/yyyy|YYYY/, this[\x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72]()); // console.log(str); // str str.replace(/MM/, (this.getMonth() 1) 9 ? (this.getMonth() 1) // .toString() : 0 (this.getMonth() 1)); // str str[replace](/MM/, (this[getMonth]() 1) 9 ? (this[getMonth]() 1) // [toString]() : 0 (this[getMonth]() 1)); str str[\x72\x65\x70\x6c\x61\x63\x65](/MM/, (this[\x67\x65\x74\x4d\x6f\x6e\x74\x68]() 1) 9 ? (this[\x67\x65\x74\x4d\x6f\x6e\x74\x68]() 1) [\x67\x65\x74\x4d\x6f\x6e\x74\x68]() : 0 (this[\x67\x65\x74\x4d\x6f\x6e\x74\x68]() 1)); // console.log(str); // str str.replace(/dd|DD/, this.getDate() 9 ? this.getDate().toString() : 0 this.getDate()); // str str[replace](/dd|DD/, this[getDate]() 9 ? this[getDate]()[toString]() : 0 // this[getDate]()); str str[\x72\x65\x70\x6c\x61\x63\x65](/dd|DD/, this[\x67\x65\x74\x44\x61\x74\x65]() 9 ? this[\x67\x65\x74\x44\x61\x74\x65]()[\x74\x6f\x53\x74\x72\x69\x6e\x67]() : 0 this[\x67\x65\x74\x44\x61\x74\x65]()); return str; } // console.log(new Date()); // 2022-04-25T05:25:36.856Z console.log(new window[\x44\x61\x74\x65]()[\x66\x6f\x72\x6d\x61\x74](yyyy-MM-dd)); //2024-05-01 console.log(new window[\x44\x61\x74\x65]()[\x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72]()) //2024 console.log(new window[\x44\x61\x74\x65]()[\x67\x65\x74\x4d\x6f\x6e\x74\x68]()) //4 当前月份为结果1 //输出结果 2022-04-25这种混淆方式很容易被还原不会大量应用只用在无法加密的字符串上。十六进制字符串的还原方法很简单把字符串放到控制台中输出即可。 1.3 Unicode字符串 在 JavaScript 中可以使用 Unicode 编码来定义字符串。Unicode 编码通常以 \u开头后跟四位十六进制数不足四位的补0。例如表示字母 A的 Unicode 编码是 \u0041。以下是使用 Unicode 编码定义字符串的示例 let unicodeString1 \u0061\u006d\u006f; // 定义一个包含amo的字符串 console.log(unicodeString1); // 输出: amo let unicodeString2 \u91cd\u5e86\u5e02\u7ea2\u9f0e\u56fd\u9645; //定义一个包含重庆市红鼎国际的字符串 console.log(unicodeString2); // 输出: 重庆市红鼎国际可以使用以下代码完成 Unicode 转换 function charToUnicode(characters) { let unicodeString for (let i 0; i characters.length; i) { // ①: charCodeAt()方法用来取出字符串中对应索引字符的ASCII码 // ②: toString(16)转换为十六进制 const hexCode characters[i].charCodeAt(0).toString(16); // ③与\x进行拼接 unicodeString hexCode.length 4 ? \\u00\){hexCode} : \u${hexCode} } return unicodeString; }JS 中的标识符也支持 Unicode 形式表示因此之前代码中的 format、Week、str、formatStr、window 等都支持以 Unicode 形式表示将 将 ** 1.2 十六进制字符串** 中的代码转换为如下形式 只处理部分代码 let window globalThis; Date[\u0070\u0072\u006f\u0074\u006f\u0074\u0079\u0070\u0065] [\x66\x6f\x72\x6d\x61\x74] function (formatStr) { let \u0073\u0074\u0072 \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072; let \u0057\u0065\u0065\u006b [\u65e5, \u4e00, \u4e8c, \u4e09, \u56db, \u4e94, \u516d]; \u0057\u0065\u0065\u006b \u0073\u0074\u0072\x72\x65\x70\x6c\x61\x63\x65; \u0057\u0065\u0065\u006b \u0057\u0065\u0065\u006b\x72\x65\x70\x6c\x61\x63\x65; \u0057\u0065\u0065\u006b \u0057\u0065\u0065\u006b\x72\x65\x70\x6c\x61\x63\x65; return \u0057\u0065\u0065\u006b; } // console.log(new Date()); // 2022-04-25T05:25:36.856Z console.log(new \u0077\u0069\u006e\u0064\u006f\u0077 \x44\x61\x74\x65\x66\x6f\x72\x6d\x61\x74); //2024-05-01 console.log(new window\x44\x61\x74\x65\x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72) //2024 console.log(new window\x44\x61\x74\x65\x67\x65\x74\x4d\x6f\x6e\x74\x68) //4 当前月份为结果1 //输出结果 2022-04-25在使用 \u0073\u0074\u0072定义变量后依然能够使用对应的 str 来引用变量。在实际 JS 混淆应用中标识符一般不会替换成 Unicode 形式因为要还原它十分容易。通常的混淆方式是替换成没有语义但看上去又很相似的名字如 _0x278843,_0x278844和 _0x257799或是由大写字母 O、小写字母 o、以及数字 0 组成的名字Oo00Oo0、Oo00O0o 和 oO000Oo注意标识符不允许以数字开头与十六进制字符串一样把字符串放到控制台中输出即可还原。 1.4 字符串的ASCII码混淆 使用以下代码将一个字符串转换为字节数组 function stringToBytes(str) { const encoder new TextEncoder(); // 创建TextEncoder实例 return encoder.encode(str); // 将字符串转换为字节 } // 使用例子 const str amo; const bytes stringToBytes(str); console.log(bytes)yyyy-MM-dd字符串转换为字节数组是 [121,121,121,121,45,77,77,45,100,100]因此代码中的 yyyy-MM-dd可以表示为 //String.fromCharCode()方法将Unicode值转换为字符 接受的是可变长度的数值类型的参数 //String.fromCharCode()方法接收的参数类型并非数组如果想要传递数组可以使用String.fromCharCode.apply String.fromCharCode.apply(null,[121, 121, 121, 121, 45, 77, 77, 45, 100, 100]))ASCII 码混淆不仅用来做字符串混淆还可以用来做代码混淆。以下面这段代码为例 Date.prototype.format function (formatStr) { let str formatStr; let Week [日, 一, 二, 三, 四, 五, 六]; // str str.replace(/yyyy|YYYY/, this.getFullYear()); // 字符串的ASCII码混淆 等同于上面的代码 str str.replace(/yyyy|YYYY/, this.getFullYear()); eval(String.fromCharCode.apply(null, [ 115, 116, 114, 32, 61, 32, 115, 116, 114, 46, 114, 101, 112, 108, 97, 99, 101, 40, 47, 121, 121, 121, 121, 124, 89, 89, 89, 89, 47, 44, 32, 116, 104, 105, 115, 46, 103, 101, 116, 70, 117, 108, 108, 89, 101, 97, 114, 40, 41, 41, 59 ] )) // 由于str str.replace(/yyyy|YYYY/, this.getFullYear());变成了字符串故执行需要依赖于eval函数 str str.replace(/MM/, (this.getMonth() 1) 9 ? (this.getMonth() 1) .toString() : 0 (this.getMonth() 1)); // console.log(str); str str.replace(/dd|DD/, this.getDate() 9 ? this.getDate().toString() : 0 this.getDate()); // console.log(str); return str; } // console.log(new Date()); // 2022-04-25T05:25:36.856Z console.log(new Date().format(yyyy-MM-dd)); //2024-05-01 console.log(new Date().getFullYear()) //2024 console.log(new Date().getMonth()) //4 当前月份为结果11.5 字符串常量加密 字符串常量加密的核心思想是先把字符串加密得到密文然后在使用前调用对应的解密去解密得到明文代码中仅出现解密函数和密文当然也可以使用不同的加密方法去加密字符串再调用不同的解密函数去解密。示例代码 Date.prototype.format function (formatStr) { let str formatStr; str strreplace; console.log(str) } new Date().format(yyyy); //2024将上述代码中的所有字符串进行加密此处仅为了演示故采用最简单的 Base64 编码如下 console.log(btoa(replace)) // cmVwbGFjZQ console.log(btoa(getFullYear)) // Z2V0RnVsbFllYXI console.log(btoa(yyyy)) // eXl5eQ处理后的代码为 Date.prototype.format function (formatStr) { let str formatStr; // 字符串加密后需要把对应的解密函数也放入代码中才能正常运行 // btoa: 用来编码atob: 用来解码 这里使用的是node.js中自带的在实际的混淆应用中还是自己实现比较好 str stratob(cmVwbGFjZQ); console.log(str) } new Date().format(atob(eXl5eQ)); //2024在实际混淆应用中标识符必须处理成没有语义的不然很容易就定位到关键代码。此外建议减少使用系统自带的函数自己去实现相应的函数因为不管如何混淆最终执行过程中系统函数的名字是固定的通过 Hook 技术极易定位到关键代码。根据写法的不同代码中有一些字符串常量没法加密和拼接如以下代码 let person { // name: amo, //正确写法 // \x6e\x61\x6d\x65: amo, //正确写法 // \x6e\x61\x6d\x65: amo, //正确写法 \u006e\u0061\u006d\u0065: amo, //正确写法 // atob(bmFtZQ): amo, //直接报错 age: 18, address: 重庆市红鼎国际, eat: function () { console.log(this.name ~eating) } } console.log(person.name) console.log(btoa(name)) let person2 {} let str na person2[str me] Amo console.log(person2.name) // 用这种方式给对象增加属性属性名可以加密和拼接1.6 数值常量加密 算法加密过程中会使用一些固定的数值常量如 MD5 中的常量 0x67452301、0xefcdab89、0x98badcfe 和 0x10325476以及 sha1 中的常量 0x67452301、0xefcdab89、0x98badcfe、0x10325476 和 0xc3d2e1f0。因此在标准算法逆向中会通过搜索这些数值常量来定位代码关键位置或者确定使用的是哪个算法。当然在代码中不一定会写十六进制形式如 0x67452301在代码中可能会写成十进制的 1732584193。为了安全起见可以把这些数值常量也进行简单加密。可以利用位异或的特性来加密。例如如果 a^bc那么 c^ba。以 sha1 算法中的 0xc3d2e1f0 常量为例 0xc3d2e1f0^0x123456780xd1e6b788那么在代码中可以用 0xd1e6b788^0x12345678来代替 0xc3d2e1f0其中 0x12345678 可以理解成密钥它可以随机生成。上述方法中两个数字进行位异或实际上就是一个二项式。 小结混淆方案不一定是单一使用各种方案之间可以结合使用。 二、增加 JS 逆向者的工作量 在 一、常量的混淆原理 中介绍了一部分的混淆手段现在我们应该对 JS 混淆有了一定的认识但实际上只是处理了一些常量防护力度并不高。混淆的目的是为了增加破解的难度和时间因此本小节从这方面入手继续介绍更加深入的内容。 2.1 数组混淆 之前的示例代码中在改变对象属性的访问方式后产生了很多原本没有的字符串。虽然在前面的介绍中已经对它们做了一系列的处理但是遇到有混淆逆向经验的逆向开发者破解这里的混淆十分容易本小节的方案是将所有的字符串都提取到一个数组中然后在需要引用字符串的地方全部都以数组下标的方式访问数组成员。例如 let bigArr [Date, getTime, log]; consolebigArr[2]; console.log(new window.Date().getTime()) // 1714585619000这里展示的代码阅读难度已经大大增加。当代码为上千行数组提取的字符串也有上千个。在代码中要引用字符串时全都以 bigArr[1001]和 bigArr[1002]访问就会大大增加理解难度不容易建立对应关系。在其他静态编程语言中同一个数组只能存放同一种类型。但是 JavaScript 语法灵活同一个数组中可以同时存放各种类型如布尔值、字符串、数值、数组、对象和函数等。例如 let bigArr [ false, Amo, 1314520, [13, 14, 520], {name: amo, age: 18}, function () { console.log(hello) } ] console.log(bigArr[0]) console.log(bigArr[1]) console.log(bigArr[2]) console.log(bigArr[3]) console.log(bigArr[4]) bigArr5因此可以把代码中的一部分函数以及字符串提取到大数组中。为了安全通常会对提取到数组中的字符串进行加密处理把代码处理成字符串就可以进行加密了。对于之前格式化日期的函数改写为以下形式 let window globalThis; let bigArr [\u65e5, \u4e00, \u4e8c, \u4e09, \u56db, \u4e94, \u516d, cmVwbGFjZQ, Z2V0TW9udGg, dG9TdHJpbmc, Z2V0RGF0ZQ, RGF0ZQ, [constructor][fromCharCode]]; Date.\u0070\u0072\u006f\u0074\u006f\u0074\u0079\u0070\u0065[\x66\x6f\x72\x6d\x61\x74] function (formatStr) { let \u0073\u0074\u0072 \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072; let Week [bigArr[0], bigArr[1], bigArr[2], bigArr[3], bigArr[4], bigArr[5], bigArr[6]]; eval(bigArr[12]atob(YXBwbHk)) str stratob(bigArr[7]); str stratob(bigArr[7]); return str; } console.log(new \u0077\u0069\u006e\u0064\u006f\u0077atob(bigArr[11])\x66\x6f\x72\x6d\x61\x74); //2024-05-02 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077atob(bigArr[11]) \x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72) //2024 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077atob(bigArr[11]) \x67\x65\x74\x4d\x6f\x6e\x74\x68 1) //5 当前月份为结果1这段代码在不使用动态调试也不使用 AST 的情况下可读性非常差但是 JS 代码混淆仍可继续。 2.2 数组乱序 观察 ** 2.1 数组混淆** 小节中处理后的代码数组成员与被引用的地方是一一对应的。如引用 bigArr[12]的地方需要的是 String.fromCharCode函数而该数组中下标为 12 的成员也是这个函数。将数组顺序打乱可以解决这个问题不过在数组顺序混乱后本身的代码也引用不到正确的数组成员。此处的解决方案是在代码中内置一段还原顺序的代码。可以使用以下代码打乱数组顺序 let bigArr [\u65e5, \u4e00, \u4e8c, \u4e09, \u56db, \u4e94, \u516d, cmVwbGFjZQ, Z2V0TW9udGg, dG9TdHJpbmc, Z2V0RGF0ZQ, RGF0ZQ, [constructor][fromCharCode]]; (function (arr, num) { let foo function (nums) { while (–nums) { // 弹出数组的最后一个元素并将其追加到数组的首位 arr.unshift(arr.pop()); } } foo(num); })(bigArr, 0x20); console.log(bigArr)在这段代码中有一个自执行的匿名函数实参部分传入的是数组和一个任意数值在这个函数内部通过对数组进行弹出和压入操作来打乱顺序除此之外只要控制台输出Unicode 处理后的字符串就变成原来的中文这就是之前说的十六进制字符串和 Unicode 都很容易被还原。 String.fromCharCode函数被移动到了下标为 5的地方但代码处引用的仍是 bigArr[12]所以需要把还原数组顺序的函数放入代码中还原数组顺序的代码逆向编写即可如下所示 (function (arr, num) { let foo function (nums) { while (–nums) { // 移除数组的第一个元素并将其追加到数组的尾部 arr.push(arr.shift()); } } foo(num); })(bigArr, 0x20); console.log(bigArr)ps还原数组顺序中的函数用到的字符串不能再提取到 bigArr 中。 2.3 花指令 添加一些没有意义却可以混淆视听的代码是花指令的核心。这里介绍一种比较简单的花指令实现方式举个例子 str str.replace(/MM/, (this.getMonth() 1) 9 ? (this.getMonth() 1) .toString() : 0 (this.getMonth() 1));把 this.getMonth() 1这个二项式改为如下形式 function _0x20ab1fxe1(a, b) { return a b; } // _0x20ab1fxe1(this.getMonth(), 1) str str.replace(/MM/, _0x20ab1fxe1(this.getMonth(), 1) 9 ? _0x20ab1fxe1(this.getMonth(), 1) .toString() : 0 _0x20ab1fxe1(this.getMonth(), 1));本质是把二项式拆开成三部分二项式的左边、二项式的右边和运算符。二项式的左边和右边作为另外一个函数的两个参数二项式的运算符作为该函数的运行逻辑。这个函救本身是没有意义的但它能瞬间增加代码量从而增加 JavaScript 逆向者的工作量。二项式转变为函数时进行多级嵌套代码如下 function _0x20ab1fxe1(a, b) { return a b; } function _0x20ab1fxe2(a, b) { return _0x20ab1fxe1(a, b); } // _0x20ab1fxe2(this.getMonth(), 1) str str.replace(/MM/, _0x20ab1fxe2(this.getMonth(), 1) 9 ? _0x20ab1fxe2(this.getMonth(), 1) .toString() : 0 _0x20ab1fxe2(this.getMonth(), 1));这个案例较为简单但是在实际混淆中代码可能有几千行函数定义部分与调用部分往往相差甚远。另外具有相同运算符的二项式并不是一定要调用相同的函数如把 0(this.getMonth()1)这个二项式改为如下所示代码 function _0x20ab1fxe1(a, b) { return a b; } function _0x20ab1fxe2(a, b) { return _0x20ab1fxe1(a, b); } function _0x20ab1fxe3(a, b) { return a b; } function _0x20ab1fxe4(a, b) { return _0x20ab1fxe3(a, b); } str str.replace(/MM/, _0x20ab1fxe2(this.getMonth(), 1) 9 ? _0x20ab1fxe2(this.getMonth(), 1) .toString() : _0x20ab1fxe4(0, _0x20ab1fxe1(this.getMonth(), 1)));上面介绍的是二项式转变为函数的花指令其实函数调用表达式也可以处理成类似的花指令。代码如下 function _0x20ab1fxe7(a, b, c) { return a.apply(b, c); } str _0x20ab1fxe7(str.replace, str, [ /MM/, (this.getMonth() 1) 9 ? (this.getMonth() 1) .toString() : 0 (this.getMonth() 1)]);花指令的生成方案并不是只有这些。文章后续还会演示另外一种插入花指令的方式。 2.4 jsfuck 样例参考https://jsfuck.com/ jsfuck 也可以算是一种编码它能把 JS 代码转化成只用 6 个字符就可以表示的代码并可以正常执行这 6 个字符分别是 (、、!、[、]、)。转换后的 JS 代码难以阅读可作为简单的保密措施如数值常量 8转成 jsfuck 后为 [][(![][])[]![]![]![][[]]]([][(![][])[]![]![]![][[]]][])![]![]![]![][[]]![]![]![]![][]![][]![]![]![][]![][[]][![]]()([![]![]![]![]![]![]![]![]][])接下来介绍 jsfuck 的基本原理 是 JS 中的一个算术运算符当它作为一元运算符使用时代表强转为数值类型 []在 JS 中表示空数组因此 []等于0 ![]等同于 !0JS 是一种弱类型的语言弱类型并不是代表没有类型是指 JS 引擎会在适当的时候自动完成类型的隐式转换。 !是 JS 中的取反这时需要一个布尔值在 JS 中七种值为假值其余均为真值这七种值分别是 false,undefined,null,0,-0,NaN,。因此 0转换为布尔值为 false再取反就是 true也就是 ** ![]true** 。又如 !![]数组转换成布尔值为 true然后两次取反依旧等于 true。JS 中的 作为二元运算符时假如有一边是字符串就代表着拼接两边都没有字符串就代表着数值相加true 转换为数值等于1剩余的部分原理相同不再赘述。在实际开发中jsfuck 的应用有限只会应用于 JS 文件中的一部分代码主要原因是它的代码量非常庞大且还原它较为容易例如把上述代码直接输入控制台运行就会输出 ** 8** 。一些网站之所以用它进行加密是因为个别情况下把整段 jsfuck 代码输入控制台运行会报错尤其是当它跟别的代码混杂时。 ps,半淘汰加壳器系列 AAEncode、JJEncode、jsfuck关于 AAEncode、JJEncode、jsfuck 具体的还原方式笔者会在后续实战的文章中进行详细演示这里就不再进行赘述。 三、代码执行流程的防护原理 经过 一、常量的混淆原理 和 二、增加 JS 逆向者的工作量 两节的处理虽然代码已经被混淆得 面目全非了但是执行流程还是跟原先一样。因此本节从代码的执行流程入手介绍更深入的代码防护方案。 3.1 流程平坦化 在一般的代码开发中会有很多的流程控制相关代码即代码中有很多分支这些分支会具有一定的层级关系在流程平坦化混淆中会用到 switch 语句因为 switch 语句中的 case 块是平级的而且调换 case 块的前后顺序并不影响代码原先的执行逻辑。为了方便理解这里举一个简单的例子代码如下 function test1() { var a 1000; var b a 2000; var c b 3000; var d c 4000; var e d 5000; var f e 6000; return f; } console.log(test1());混淆 test1 函数中的代码代码如下 function test3() { // ①构造一个分发器里面记录了代码执行的真实顺序。并把字符串通过split分割成一个数组 var arr z|t|y|u|a|d|7|c.split(|); var index 0; // ② 因为switch语句一次只能计算一次故需要一个循环 while (!![]) { // ③ index作为计数器每次递增按顺序引用数组中的每一个成员 // switch中把表达式的值与每个case的值进行对比(这里是的匹配不进行类型转换) switch (arr[index]) { case a: var e d 5000; break; case t: var b a 2000; break; case y: var c b 3000; break; case d: var f e 6000; break; case 7: var g 100000; g g a b c d; break; case c: return f; case z: var a 1000; break; case u: var d c 4000; break; } } } console.log(test3());在了解了简单的案例后对 ** 2.1 数组混淆** 一节中的代码做进一步混淆处理后的代码如下 let window globalThis; let bigArr [\u65e5, \u4e00, \u4e8c, \u4e09, \u56db, \u4e94, \u516d, cmVwbGFjZQ, Z2V0TW9udGg, dG9TdHJpbmc, Z2V0RGF0ZQ, RGF0ZQ, [constructor][fromCharCode]]; Date.\u0070\u0072\u006f\u0074\u006f\u0074\u0079\u0070\u0065[\x66\x6f\x72\x6d\x61\x74] function (formatStr) { // 定义分发器 let arr z|t|y|u|a|d|7|c.split(|); let index 0; let str while (!![]) { //需要多次计算故使用循环 switch (arr[index]) { // 依次引用数组中的每一个成员 case a: str stratob(bigArr[7]); break; case t: let Week [bigArr[0], bigArr[1], bigArr[2], bigArr[3], bigArr[4], bigArr[5], bigArr[6]]; break; case y: eval(bigArr[12]atob(YXBwbHk)) break; case d: console.log(~amo) break; // case 7: case 7: console.log(~jerry) break; case c: return str; case z: str \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072; break; case u: str stratob(bigArr[7]); break; } } } console.log(new \u0077\u0069\u006e\u0064\u006f\u0077atob(bigArr[11])\x66\x6f\x72\x6d\x61\x74); //2024-05-02 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077atob(bigArr[11]) \x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72) //2024 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077atob(bigArr[11]) \x67\x65\x74\x4d\x6f\x6e\x74\x68 1) //5 当前月份为结果1JS 语法比较灵活case 后面跟的值可以是字符/字符串也可以是数值还可以是对象或者数组。 3.2 逗号表达式混淆 逗号运算符的主要作用是把多个表达式或语句连接成一个复合语句。** 3.1 流程平坦化** 中的 test1() 函数等价于 function test1() { let a, b, c, d, e, f; return a 1000, b a 2000, c b 3000, d c 4000, e d 5000, f e 6000, f; } console.log(test1())return 语句后通常只能跟一个表达式它会返回这个表达式计算后的结果但是逗号运算符可以把多个表达式连接成一个复合语句因此上述代码中return 语句的使用也是没有问题的它会返回最后一个表达式计算后的结果但是前面的表达式依然会执行。上述案例只是单纯的连接语句没有混淆力度。下面再介绍一个案例代码如下 var a (a 1000, a 2000); // 使用let会报错 console.log(a)第一行代码中括号代表这是一个整体也就是把 (a1000,a2000)整体赋值给 a 变量这个整体返回的结果和 return 语句是一样的会先执行 a1000然后执行 a2000再把结果赋值给 a 变量最终 a 变量的值为 3000。明白了上述原理后再介绍逗号运算符的混淆以本节中的 test1 函数为例处理思路如下 // ① 执行 a1000再执行 a2000代码可以改为 (a1000,a2000) // ② 接着赋值给b代码可以改为 b(a1000,a2000) // ③ 执行 b3000代码可以改为 (b(a1000,a2000),b3000) // ④ 接着赋值给 c代码可以改为 c(b(a1000,a2000),b3000) // ⑤ 执行 c4000代码可以改为 (c(b(a1000,a2000),b3000),c4000) // 以此类推….处理后的代码为 function test2() { let a, b, c, d, e, f; return f (e (d (c (b (a 1000, a 2000), b 3000), c 4000), d 5000), e 6000) } console.log(test2())这段代码有一个声明一系列变量的语句这个语句很多余可以放到参数列表上这样就不需要 let声明了。另外既然逗号运算符连接多个表达式只会返回最后一个表达式计算后的结果那么可以在最后一个表达式之前插入不影响结果的花指令。最终处理后的代码如下 function test2(a, b, c, d, e, f) { // return f (e (d (c (b (a 1000, a 50, b 60, c 70, a 2000), d 80, b 3000), e 90, c 4000), f 100, d 5000), e 6000) } console.log(test2())a 50, b 60, c 70,d 80,e 90,f 100这些花指令并无实际意义不影响原先的代码逻辑test2() 虽有 6 个参数但是不传参也可以调用只不过各参数的初始值为 undefined。逗号表达式混淆不仅能处理赋值表达式还能处理调用表达式、成员表达式等。考虑下面这个案例 let obj { name: amo, add: function (a, b) { return a b; } } function sub(a, b) { return a - b; } function test() { let a 1000; let b sub(a, 3000) 1; let c b obj.add(b, 2000); return c obj.name } console.log(test());上述案例中的代码可以处理成如下形式 let obj { name: amo, add: function (a, b) { return a b; } } function sub(a, b) { return a - b; } function test(a, b, c) { return c (b (a 1000, sub)(a, 3000) 1, b (0, obj).add(b, 2000)), c (0,obj).name; } console.log(test());首先提升变量声明到函数参数中 b (a 1000, sub)(a,3000) 1中的 (a 1000,sub)可以整体返回 sub 函数然后直接调用计算的结果加 1 后赋值给 b(等号的运算符优先级很低)。同理如果 sub 函数改为 obj.add的话可以处理成 (a1000,obj.add)(a,3000)或者 (a1000,obj).add(a,3000)第2种方法是调用表达式在等号右边的情况例如 test 函数中的第3条语里面的 bobj.add(b,2000)可以对 obj.add进行包装处理成 b(0,obj.add)(b,2000)或者 b(0,obj).add(b,2000)括号中的0可以是其他花指令。 最后介绍逗号表达式混淆的还原技巧在逗号表达式混淆中通常需要使用括号来分组定位到最里面的那个括号一般就是第一条语句然后从里到外一层层地根据括号对应关系还原语句顺序如果用 AST 还原逗号表达式混淆就不用这么麻烦地找对应关系几行代码就可以解决问题在后续的文章中笔者会对 AST 进行详细地介绍。 四、其他代码防护方案 4.1 eval加密 加密的代码格式化后如下所示 eval(function (p, a, c, k, e, r) { e function © { return c.toString(36) }; if (0.replace(0, e) 0) { while (c–) r[e©] k[c]; k [function (e) { return r[e] || e } ]; e function () { return [2-8a-f] }; c 1 } ; while (c–) if (k[c]) p p.replace(new RegExp(\b e© \b, g), k[c]); return p }(7.prototype.8function(a){b 2a;b Week[\日\,\一\,\二\,\三\,\四\,\五\,\六]; 22.4(/c|YYYY/ ,3.getFullYear());22.4(/d/,(3.5()1)9?(3.5()1).e():\0(3.5()1)); 22.4(/f|DD/,3.6()9? 3.6().e():\0\3.6());return 2};console.log(new 7().8(\c-d-f));, [], 16, ||str|this|replace|getMonth|getDate|Date|format||formatStr|var|yyyy|MM|toString|dd .split(|), 0, {}));这段代码的一个 eval() 函数它用来把一段字符串当作 JS 代码来执行也就是说传给 eval() 的参数是一段字符串。但在上述代码中传给 eval() 函数的参数是一个自执行的匿名函数这说明这个匿名函数执行后会返回一段字符串并且用 eval() 执行这段字符串执行效果与 eval 加密前的代码效果等同那就可以把这个匿名函数理解成是一个解密函数了由此可见eval 加密其实和 eval 关系不大eval 只是用来执行解密出来的代码。 再来观察传给这个匿名函数的实参部分观察第1个实参p和第4个实参k可以看出处理方式很简单提取原始代码中的一部分标识符然后用它自己的符号占位最后再对应替换回去就解密了最后介绍 eval 解密这个比较容易既然这个自执行的匿名函数就是解密函数把上述代码中的 eval 删去剩余代码在控制台中执行就得到原始代码。 4.2 内存爆破 内存爆破是在代码中加入死代码正常情况下这段代码不执行当检测到函数被格式化或者函数被 Hook就跳转到这段代码并执行直到内存溢出浏览器会提示 Out of Memory 程序崩溃。内存爆破的代码如下所示 let d [0x1, 0x1, 0x1] function b() { for (let i 0x0, c d.length; i c; i) { d.push(Math.round(Math.random())); c d.length; } }这段代码中的 for 循环是一个死循环但它的形式不像 while(true) 这样明显尤其是代码混淆以后更具有迷惑性这段代码其实是从以下这段代码简化而来 const _0x447a [push, length]; const _0x3774 function (_0x447aa4, _0x377412) { _0x447aa4 _0x447aa4 - 0x0; let _0x2a002f _0x447a[_0x447aa4]; return _0x2a002f; }; let d [0x1, 0x1, 0x1]; function b() { for (let _0x514f9d 0x0, _0x1c3f88 d[_0x3774(0x1)]; _0x514f9d _0x1c3f88; _0x514f9d) { d_0x3774(0x0); _0x1c3f88 d[length]; } }for 循环的结束条件是 _0x514f9d _0x1c3f88其中 _0x1c3f88的初始化值是数组的长度看着像是一个遍历数组的操作但是在循环中又往数组中 push 了成员接着又重新给 _0x1c3f88赋值为数组的长度这时这段代码就永远不会结束了直到内存溢出。 4.3 检测代码是否格式化 检测的思路很简单在 JS 中函数是可以转为字符串的因此可以选择一个函数转为字符串然后跟内置的字符串对比或者用正则匹配函数转为字符串很简单代码如下 function add(a, b) { return a b; } console.log(add ); console.log(add.toString()) // 未格式化:function add(a, b) {return a b;} // 格式化: // function add(a, b) { // return a b; // }在 Chrome 开发者工具中把代码格式化后会产生一个后缀为 :formatted的文件之后在这个文件中设置断点触发断点后会停在这个文件中但是这时把某个函数转为字符串取到的依然是格式化之前的代码。上述检测方法检测不到这种情况那么上述检测方法的应用场景是什么在算法逆向中分析完算法为了得到想要的结果就需要实现这个算法简单的算法一般可以直接调用现成的加密库复杂的算法就会选择直接修改原文件然后运行得到结果把格式化后的代码保存成一个本地文件这时某个函数转为字符串取到的就是格式化后的结果了是否触发格式化检测关键是看原文件中是否有格式化接着把 4.2 内存爆破 小节中的内存爆破代码加入其中检测到格式化就跳转到内存爆破代码中执行程序会崩溃。 原文地址
- 上一篇: 网站设计小技巧响应式网站建设原则
- 下一篇: 网站设计行业吃香么建站报价表
相关文章
-
网站设计小技巧响应式网站建设原则
网站设计小技巧响应式网站建设原则
- 技术栈
- 2026年04月20日
-
网站设计销售好做吗百度教育官网
网站设计销售好做吗百度教育官网
- 技术栈
- 2026年04月20日
-
网站设计项目建设内容优秀高端网站建设服务商
网站设计项目建设内容优秀高端网站建设服务商
- 技术栈
- 2026年04月20日
-
网站设计行业吃香么建站报价表
网站设计行业吃香么建站报价表
- 技术栈
- 2026年04月20日
-
网站设计行业资讯有没有做定制衣服的网站
网站设计行业资讯有没有做定制衣服的网站
- 技术栈
- 2026年04月20日
-
网站设计需求方案学院网站怎么做的
网站设计需求方案学院网站怎么做的
- 技术栈
- 2026年04月20日
