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

一、常量的混淆原理

示例代码:

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 内存爆破 小节中的内存爆破代码加入其中,检测到格式化就跳转到内存爆破代码中执行,程序会崩溃。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amo Xiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值