当前位置: 首页 > news >正文

JavaScript逆向爬虫教程-------基础篇之JavaScript混淆原理

目录

  • 一、常量的混淆原理
  • 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 混淆原理是非常有必要的,原因:

  1. 学好 AST 混淆和还原 JavaScript 代码的基础
  2. 招聘要求,越来越多的招聘要求爬虫人员懂 JavaScript 防护技术(逆向是越来越卷了)

1.1 对象属性的两种访问方式

示例代码:

	let person = { 'name': 'amo', 'age': 18, 'address': '重庆市红鼎国际', eat: function () { console.log(this.name + '~eating') } } // ①: person.name,name是一个标识符,必须明确出现在代码中,不能进行加密与拼接 console.log(person.name) person.eat() // ②: person['name'],该种方式name是一个字符串,既然是字符串,访问的时候就可以进行加密与拼接 // 在js混淆中一般会选择用这种方式来访问属性,操作空间更大 console.log(person['name']) person['eat']()

访问对象的方法也可以通过 **

	[]

** 的方式,因为对象的方法可以看作特殊的属性,它是一种值为函数的属性。将 **

	一、常量的混淆原理

** 中的示例代码可以转换为如下形式:

	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 = 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)); 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()); 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 window['Date']()['format']('yyyy-MM-dd')); //2024-05-01 console.log(new window['Date']()['getFullYear']()) //2024 console.log(new window['Date']()['getMonth']()) //4 当前月份为结果+1 //输出结果 2022-04-25

Date 是 JS 的内置对象,在 JS 中,很多内置对象都是 window 的属性 (上面的代码由于笔者是在 node.js 中运行的,故将 window 指向了 globalThis), 所以 JS 中的内置对象和客户端 JS 中的 DOM 对于 JS 的防护与逆向极为重要。在真实浏览器环境中,代码中定义的全局变量 (var) 都是全局对象 window 的属性,定义的全局函数都是全局对象 window 的方法,全局对象的属性或者方法在调用时,可以省略全局对象名,例如:

	window.btoa('a')

等同于

	btoa('a')

,如果要把

	btoa

变为字符串,前面就必须加 window,如上面的示例代码:**

	new window['Date']()

**

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'](/yyyy|YYYY/, this['\x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72']()); \u0057\u0065\u0065\u006b = \u0057\u0065\u0065\u006b['\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)); \u0057\u0065\u0065\u006b = \u0057\u0065\u0065\u006b['\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 \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']('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

在使用

	\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 当前月份为结果+1

1.5 字符串常量加密

字符串常量加密的核心思想是,先把字符串加密得到密文,然后在使用前调用对应的解密去解密得到明文,代码中仅出现解密函数和密文,当然也可以使用不同的加密方法去加密字符串,再调用不同的解密函数去解密。示例代码:

	Date.prototype.format = function (formatStr) { let str = formatStr; str = str['replace'](/yyyy|YYYY/, this['getFullYear']()); 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 = str[atob('cmVwbGFjZQ==')](/yyyy|YYYY/, this[atob('Z2V0RnVsbFllYXI=')]()); 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^b=c

,那么

	c^b=a

。以 sha1 算法中的 0xc3d2e1f0 常量为例,

	0xc3d2e1f0^0x12345678=0xd1e6b788

,那么在代码中可以用

	0xd1e6b788^0x12345678

来代替 0xc3d2e1f0,其中 0x12345678 可以理解成密钥,它可以随机生成。上述方法中两个数字进行位异或,实际上就是一个二项式。

小结:混淆方案不一定是单一使用,各种方案之间可以结合使用。

二、增加 JS 逆向者的工作量

一、常量的混淆原理 中介绍了一部分的混淆手段,现在我们应该对 JS 混淆有了一定的认识,但实际上只是处理了一些常量,防护力度并不高。混淆的目的是为了增加破解的难度和时间,因此本小节从这方面入手,继续介绍更加深入的内容。

2.1 数组混淆

之前的示例代码中,在改变对象属性的访问方式后,产生了很多原本没有的字符串。虽然在前面的介绍中,已经对它们做了一系列的处理,但是遇到有混淆逆向经验的逆向开发者,破解这里的混淆十分容易,本小节的方案是将所有的字符串都提取到一个数组中,然后在需要引用字符串的地方,全部都以数组下标的方式访问数组成员。例如:

	let bigArr = ['Date', 'getTime', 'log']; console[bigArr[2]](new window[bigArr[0]]()[bigArr[1]]()); 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]) bigArr[5]()

因此,可以把代码中的一部分函数以及字符串提取到大数组中。为了安全,通常会对提取到数组中的字符串进行加密处理,把代码处理成字符串就可以进行加密了。对于之前格式化日期的函数,改写为以下形式:

	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=')](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[atob(bigArr[7])](/MM/, (this[atob(bigArr[8])]() + 1) > 9 ? (this[atob(bigArr[8])]() + 1) [atob(bigArr[9])]() : '0' + (this[atob(bigArr[8])]() + 1)); str = str[atob(bigArr[7])](/dd|DD/, this[atob(bigArr[10])]() > 9 ? this[atob(bigArr[10])]()[atob(bigArr[9])]() : '0' + this[atob(bigArr[10])]()); return str; } console.log(new \u0077\u0069\u006e\u0064\u006f\u0077[atob(bigArr[11])]()['\x66\x6f\x72\x6d\x61\x74'] ('\u0079\u0079\u0079\u0079\u002d\u004d\u004d\u002d\u0064\u0064')); //2024-05-02 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077[atob(bigArr[11])]() ['\x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72']()) //2024 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077[atob(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,

	!+[]

等同于

	!0

,JS 是一种弱类型的语言,弱类型并不是代表没有类型,是指 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 = str[atob(bigArr[7])](/dd|DD/, this[atob(bigArr[10])]() > 9 ? this[atob(bigArr[10])]()[atob(bigArr[9])]() : '0' + this[atob(bigArr[10])]()); 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=')](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 ] )) 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 = str[atob(bigArr[7])](/MM/, (this[atob(bigArr[8])]() + 1) > 9 ? (this[atob(bigArr[8])]() + 1) [atob(bigArr[9])]() : '0' + (this[atob(bigArr[8])]() + 1)); break; } } } console.log(new \u0077\u0069\u006e\u0064\u006f\u0077[atob(bigArr[11])]()['\x66\x6f\x72\x6d\x61\x74'] ('\u0079\u0079\u0079\u0079\u002d\u004d\u004d\u002d\u0064\u0064')); //2024-05-02 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077[atob(bigArr[11])]() ['\x67\x65\x74\x46\x75\x6c\x6c\x59\x65\x61\x72']()) //2024 console.log(new \u0077\u0069\u006e\u0064\u006f\u0077[atob(bigArr[11])]() ['\x67\x65\x74\x4d\x6f\x6e\x74\x68']() + 1) //5 当前月份为结果+1

JS 语法比较灵活,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)

第一行代码中,括号代表这是一个整体,也就是把

	(a=1000,a+=2000)

整体赋值给 a 变量,这个整体返回的结果和 return 语句是一样的,会先执行 a=1000,然后执行 a+=2000,再把结果赋值给 a 变量,最终 a 变量的值为 3000。明白了上述原理后,再介绍逗号运算符的混淆,以本节中的 test1 函数为例,处理思路如下:

	// ① 执行 a=1000,再执行 a+2000,代码可以改为 (a=1000,a+2000) // ② 接着赋值给b,代码可以改为 b=(a=1000,a+2000) // ③ 执行 b+3000,代码可以改为 (b=(a=1000,a+2000),b+3000) // ④ 接着赋值给 c,代码可以改为 c=(b=(a=1000,a+2000),b+3000) // ⑤ 执行 c+4000,代码可以改为 (c=(b=(a=1000,a+2000),b+3000),c+4000) // 以此类推....

处理后的代码为:

	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

的话,可以处理成

	(a=1000,obj.add)(a,3000)

或者

	(a=1000,obj).add(a,3000)

,第2种方法是调用表达式在等号右边的情况,例如 test 函数中的第3条语里面的

	b+obj.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 (c) { return c.toString(36) }; if ('0'.replace(0, e) == 0) { while (c--) r[e(c)] = 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(c) + '\\b', 'g'), k[c]); return p }('7.prototype.8=function(a){b 2=a;b Week=[\'日\',\'一\',\'二\',\'三\',\'四\',\'五\',\'六\'];' + '2=2.4(/c|YYYY/' + ',3.getFullYear());2=2.4(/d/,(3.5()+1)>9?(3.5()+1).e():\'0\'+(3.5()+1));' + '2=2.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')](Math['round'](Math['random']())); _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 内存爆破 小节中的内存爆破代码加入其中,检测到格式化就跳转到内存爆破代码中执行,程序会崩溃。

原文地址

相关文章:

JavaScript逆向爬虫教程-------基础篇之JavaScript混淆原理

目录 一、常量的混淆原理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 …...

qt移植到讯为rk3568,包含一些错误总结

qt移植到arm报错动态库找不到 error while loading shared libraries: libAlterManager.so.1: cannot open shared object file: No such file or directory 通过设置环境变量 LD_LIBRARY_PATH就行了。 LD_LIBRARY_PATH是一个用于指定动态链接器在运行时搜索共享库的路径的环…...

使用阿里云快速搭建 DataLight 平台

使用阿里云快速搭建 DataLight 平台 本篇文章由用户 “闫哥大数据” 分享&#xff0c;B 站账号&#xff1a;https://space.bilibili.com/357944741?spm_id_from333.999.0.0 注意&#xff1a;因每个人操作顺序可能略有区别&#xff0c;整个部署流程如果出现出入&#xff0c;以…...

ubuntu设置自启动

1. 把要启动的程序或者脚本(比如A.sh、A1)放在 /usr/sbin 目录中。比如我的 A.sh 只是启动 A1 程序&#xff1a; #!/bin/bash/usr/sbin/A1echo "A1 finish!!!" 需要注意的是&#xff0c;脚本和程序都要有可执行的权限才行 2. 在 /etc/systemd/system 目录中创建 .…...

Paddle分布式训练报NCCL错

应该是没有装NCCL&#xff0c;但是通过NVIDIA官网方式用apt安装报错&#xff0c;说nccl签名有问题 打开官网查找对应版本的nccl&#xff1a;https://developer.nvidia.com/nccl/nccl-legacy-downloads 这里不下载local Ubuntu选项&#xff0c;下载O/S agnostic local install…...

PD3.1快充对我们到底有没有必要?

在科技飞速发展的今天&#xff0c;各种智能设备和电子产品已经渗透到了我们生活的方方面面。随之而来的&#xff0c;是对充电速度和效率的不断追求。正是在这样的背景下&#xff0c;USB联盟于2021年6月发布了最新的快充协议——PD3.1。那么&#xff0c;PD3.1快充协议对我们到底…...

Android OpenGL ES详解——立方体贴图

目录 一、概念 二、如何使用 1、创建立方体贴图 2、生成纹理 3、设置纹理环绕和过滤方式 4、激活和绑定立方体贴图 三、应用举例——天空盒 1、概念 2、加载天空盒 3、显示天空盒 4、优化 四、应用举例——环境映射:反射 五、应用举例——环境映射:折射 六、应用…...

Bugku CTF_Web——字符?正则?

Bugku CTF_Web——字符&#xff1f;正则&#xff1f; 进入靶场 <?php highlight_file(2.php); $keyflag{********************************}; $IM preg_match("/key.*key.{4,7}key:\/.\/(.*key)[a-z][[:punct:]]/i", trim($_GET["id"]), $match); if…...

C# 中Math.Round 和 SQL Server中decimal(18,2) 不想等的问题

首先了解Math.Round方法的默认舍入规则 在C#中&#xff0c;Math.Round方法使用的是“银行家舍入法”&#xff08;也叫四舍六入五成双&#xff09;。这种舍入规则是&#xff1a;当要舍弃的数字小于5时直接舍去&#xff1b;当要舍弃的数字大于5时进位&#xff1b;当要舍弃的数字正…...

lab2:docker基础实战

一、实验目的 1.通过本次实验&#xff0c;完成Docker主机的安装和配置、镜像的搜索和下载、容器生命周期的基本管理、容器网络的管理。 2.通过Dockerfile来构建nginx镜像&#xff0c;了解Dockerfile镜像构建过程。 二、实验内容与实验要求 1.完成Docker的安装和配置。 2.完…...

Druid 1.2 源码导读

Druid 是阿里巴巴开源的数据库连接池和监控组件&#xff0c;广泛用于 Java 应用程序中。Druid 1.2 版本提供了高性能的连接池和丰富的监控功能。以下是对 Druid 1.2 源码的导读&#xff0c;帮助你理解其架构和设计。 1. 源码获取 从 GitHub 上获取 Druid 1.2 的源码&#xff…...

千益畅行,共享旅游卡市场乱象解析与未来展望

在当今旅游市场蓬勃发展的大背景下&#xff0c;共享旅游卡作为一种新兴的旅游消费模式&#xff0c;受到了广泛关注。然而&#xff0c;伴随着其快速发展&#xff0c;市场乱象也层出不穷。作为千益畅行共享旅游卡的全国运营总监&#xff0c;我深感有必要对这些问题进行深入剖析&a…...

Dubbo源码解析-服务导出(四)

一、服务导出 当我们在某个接口的实现类上加上DubboService后&#xff0c;就表示定义了一个Dubbo服务&#xff0c;应用启动时Dubbo只要扫描到了DubboService&#xff0c;就会解析对应的类&#xff0c;得到服务相关的配置信息&#xff0c;比如&#xff1a; 1. 服务的类型&…...

浅谈React的虚拟DOM

React的虚拟DOM&#xff1a;揭秘高效渲染的秘密 在React中&#xff0c;虚拟DOM&#xff08;Virtual DOM&#xff09;是一个核心概念&#xff0c;它是React能够提供高效渲染和更新的关键。虚拟DOM是一个轻量级的JavaScript对象&#xff0c;表示真实的DOM树。通过使用虚拟DOM&am…...

linux上海康SDK安装并设置环境变量

将HCNetSDK下linux部分复制到客户端电脑/usr/lib/HCNetSDK下:sudo cp -r H /usr/lib/HCNetSDK H是我的文件夹&#xff0c;要把这个文件夹的内容复制到/usr/lib/HCNetSDK路径里。 编辑&#xff1a;vi ~/.bashrc 找到export,按 i 插入换行添加 export LD_LIBRARY_PATH$LD_LIB…...

【计算机网络】UDP网络程序

一、服务端 1.udpServer.hpp 此文件负责实现一个udp服务器 #pragma once#include <iostream> #include <string> #include <cstdlib> #include <cstring> #include <functional> #include <strings.h> #include <unistd.h> #incl…...

什么是全域电商?有哪些电商代运营公司能做全域电商代运营?

什么是全域电商&#xff1f;有哪些电商代运营公司能做全域电商代运营&#xff1f; 随着电商行业的迅猛发展&#xff0c;传统的单一平台运营模式已经无法满足品牌多元化发展的需求。在此背景下&#xff0c;全域电商作为一种新兴的运营方式应运而生&#xff0c;成为越来越多品牌在…...

微信小程序上传pdf和显示

引用&#xff1a;https://blog.csdn.net/qq_54027065/article/details/129854339 loadResume(){let that thisuni.showLoading({title:"下载中"})wx.downloadFile({url:url,success:(res)>{console.log(res,"res11111")if (res.statusCode 200){setTi…...

MongoDB分布式集群搭建----副本集----PSS/PSA

MongoDB分布式集群 Replication 复制、Replica Set 复制集/副本集 概念 一、 副本集的相关概念 1.概念 “ A replica set is a group of mongod instances that maintain the same data set. ” 一组MongoDB服务器&#xff08;多个mongod实例&#xff09;&#xff08;有不…...

PDF编辑的好东西

1.Eage浏览器 直接拖到浏览器中就ok了&#xff0c;这样读书的话是非常爽的&#xff0c;然后的话最近&#xff0c;也不知道学啥&#xff0c;vue开发网站&#xff0c;一开始的配置&#xff0c;也是给我难到了&#xff0c;所以没有办法&#xff0c;就随便找点书看看吧&#xff0c…...

块设备的两种访问方法的区别

概述 1.当我们运行类似于“dd if/dev/sdb1ofsdb1.img”的命令把整个/dev/sdb1裸分区复制到sdb1.img的时候&#xff0c;内核走的是def_blk_fops这个file_operations 2.另外一种方法是通过文件系统来访问块设备&#xff0c;file_operations的实现则位于文件系统内&#xff0c;文…...

java 泛型中的 ?

在 Java 泛型中&#xff0c;? 被称为通配符&#xff08;wildcard&#xff09;&#xff0c;它代表了未知的类型。使用通配符可以增加代码的灵活性&#xff0c;允许在不知道具体类型的情况下操作泛型类或接口。通配符主要有以下几种形式&#xff1a; 无界通配符&#xff08;Unbo…...

如何在jupyter notebook切换python环境

目录 参考链接 首先确保conda已经正常安装 conda --version 或者conda -V 以下请将“myenv”替换成自己的命名&#xff01;&#xff01;&#xff01; 1-查看虚拟环境目录 conda env list 2-创建虚拟环境命令 conda create -n myenv 或者 conda create --name myenv 3-激活虚拟环…...

用Python将Word文档转换为Markdown格式

Markdown作为一种轻量级标记语言&#xff0c;以其简洁的语法和广泛的兼容性&#xff0c;特别适合用于博客、技术文档和版本控制系统中的内容管理。而Word文档则因其强大的排版功能&#xff0c;常常成为文档制作的首选。然而&#xff0c;直接使用Word格式在某些平台上可能显得过…...

CSV 文件

CSV&#xff0c;全称为 Comma-Separated Values&#xff09;&#xff08;逗号分隔值&#xff09;&#xff0c;是一种常用的文本文件格式&#xff0c;用于存储表格数据&#xff0c;如电子表格或数据库。它采用纯文本形式&#xff0c;以逗号作为字段之间的分隔符&#xff0c;每行…...

SpringCloud核心组件(五)

文章目录 Gateway一. 概述简介1. Gateway 是什么2. 什么是网关?3.Gateway 和 Nginx 两个网关的区别什么是流量入口&#xff1f; 4.Gateway 能干嘛5.gateway 三大核心概念6.运行方式 二. 入门案例a.创建gateway模块&#xff0c;在pom.xml中引入依赖b.创建启动类GatewayApplicat…...

TCP为什么需要三次握手和四次挥手,有哪些需要注意的地方?

TCP&#xff08;传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议。为了确保数据能够准确无误地从一端发送到另一端&#xff0c;TCP设计了一系列机制来保证通信的可靠性&#xff0c;其中包括连接建立和断开的过程。 三次握手&#xff08;Three-…...

机器学习(基础2)

特征工程 特征工程:就是对特征进行相关的处理 一般使用pandas来进行数据清洗和数据处理、使用sklearn来进行特征工程 特征工程是将任意数据(如文本或图像)转换为可用于机器学习的数字特征,比如:字典特征提取(特征离散化)、文本特征提取、图像特征提取。 特征工程API 实例化…...

Cpolar 内网穿透使用

Cpolar登录地址&#xff1a;cpolar - secure introspectable tunnels to localhost 使用固定公网TCP连接ssh ssh -p端口号 用户名公网地址...

ThreadLocal 提供线程局部变量

ThreadLocal作用 相当于建立一个独立的空间&#xff0c;可以把使用频率高的任何类型的数据放到里面&#xff0c;方便调用用来存取数据&#xff1a;set()/get()使用ThreadLocal存储的数据&#xff0c;线程安全 ThreadLocal工具类 /*** ThreadLocal 工具类*/ SuppressWarnings(…...