前端逆向学习之 crypto-js

涉及知识点

间接调用

在 JavaScript 中函数也是对象,与其他 JavaScript 对象一样,函数也有方法,其中有两个方法 call()apply(),可以用来间接调用函数,这两个方法允许我们指定调用时的this值,也就是说可以将任意函数作为任意对象的方法来调用,即使这个函数实际并不是该对象的方法

call 方法

1
2
3
4
call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)
  • thisArg:在调用函数时要使用的this值,如果函数不在严格模式下,nullundefined将被替换为全局对象,并且原始值将被转换为对象
  • arg1...argN:函数的参数
  • 返回指定的this值和参数调用函数后的结果
1
2
3
4
5
6
7
8
function greet(greeting, punctuation) {
console.log(greeting + ", I'm " + this.name + punctuation);
}

const user = { name: "Bob" };

greet.call(user, "Hi", "!");
// 输出: Hi, I'm Bob!

apply 方法

1
2
apply(thisArg)
apply(thisArg, argsArray)
  • thisArg:调用函数时要使用的this值,如果函数不处于严格模式,则nullundefined会被替换为全局对象,原始值会被替换为对象
  • argsArray:传给函数的参数以数组的形式提供
1
2
3
4
5
6
7
8
9
10
11
const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);

console.log(max);
// Expected output: 7

const min = Math.min.apply(null, numbers);

console.log(min);
// Expected output: 2

闭包

JavaScript 使用词法作用域,这意味着函数执行时,使用的是定义函数时生效的变量作用域,而不是调用函数时生效的变量作用域,为了实现词法作用域,JavaScript 函数对象的内部状态不仅要包含函数代码,还要包括对函数定义所在作用域的引用,这种函数对象与作用域(即一组变量绑定)组合起来解析函数变量的机制,在计算机科学文献中被成为闭包

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function (y) {
return x + y;
};
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

add5add10都创建了闭包,它们共享相同的函数体定义,但是保存了不同的词法环境,在add5的词法环境中,x为 5,在add10的词法环境中,x则为 10

原型

原型是 JavaScript 对象相互继承特性的机制

1
2
3
function Foo() {}
let obj = new Foo();
Object.getPrototypeOf(obj) === Foo.prototype // true

几乎所有的对象都有原型,在浏览器中通常可以通过__proto__访问,或者使用标准方法Object.getPrototypeOf(),通过Object.create(null)创建的对象是没有原型的,它的__proto__undefined

只有函数有ptototype属性,不包括箭头函数,因为箭头函数不能作为构造函数

arguments

arguments是一个类数组对象,可在函数内部访问,其中包含传递给该函数的参数值,arguments对象是所有非箭头函数中都可用的局部变量,可以使用arguments对象在函数中引用函数的参数,此对象包含传递给函数的每个参数,第一个参数在索引0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sum() {
// arguments 是一个类数组对象,包含所有传入的参数
console.log('接收到的参数:', arguments);

let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}

// 调用示例
console.log(sum(1, 2)); // 输出: 3
console.log(sum(1, 2, 3, 4, 5)); // 输出: 15
console.log(sum()); // 输出: 0

crypto

实例化流程

crypto-js是 javaScript 中的一个加密库,许多前端加密都是使用的这个库,其中core.js是这个加密库的核心,定义了基础架构、数据结构、编码转换以及算法运行的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var C_lib = C.lib = {};

var Base = C_lib.Base = (function () {

return {

extend: function (overrides) {
// Spawn
var subtype = create(this);

// Augment
if (overrides) {
subtype.mixIn(overrides);
}

// Create default initializer
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
subtype.init = function () {
subtype.$super.init.apply(this, arguments);
};
}

// Initializer's prototype is the subtype object
subtype.init.prototype = subtype;

// Reference supertype
subtype.$super = this;

return subtype;
},

create: function () {
var instance = this.extend();
instance.init.apply(instance, arguments);

return instance;
},

init: function () {
},


mixIn: function (properties) {
...
},

clone: function () {
...
}
};
}());

该 js 文件中的C.lib.Base是所有对象的基类,允许其他对象继承它,并提供类似类实例化的功能

  • extend(overrides):创建一个子类
  • create():创建类的实例
  • init():构造函数,用于初始化状态,默认为空,子类可以覆盖它
  • mixIn(properties):相当于Object.assign的早期实现,将属性复制到当前对象
  • clone():创建当前对象的深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var WordArray = C_lib.WordArray = Base.extend({

init: function (words, sigBytes) {
...
},

toString: function (encoder) {
return (encoder || Hex).stringify(this);
},

concat: function (wordArray) {
...
},

clamp: function () {
...
},

clone: function () {
...
},

random: function (nBytes) {
...
}
});

JavaScript(特别是在 ES6 TypedArray 普及之前)对二进制数据的处理非常薄弱,字符串是 UTF-16 的,不适合位运算。crypto-js创造了WordArray(字数组)来在 JS 中高效处理二进制数据

  • init(words, sigBytes):初始化
  • toString(encoder):将二进制数据转为字符串(默认转为十六进制字符串)
  • concat(wordArray):核心操作,将另一个 WordArray 拼接到当前数组后面
  • clamp():截断/清理
  • random(nBytes):生成指定字节长度的随机WordArray

对称加密

下面的时序图,是调用AES.encrypt方法进行加密时,内部的整个流程

当调用AES.encrypt时会调用到CryptoJS.lib.SerializableCipher.encrypt,该方法中存在加解密需要用到的密钥等数据

CryptoJS.lib.CipherParams继承自CryptoJS.lib.Base,当调用create方法时,会调用Base.create,内部调用了apply方法,该方法是 JavaScript 内置方法

Hook_JS 中的 Hook_CryptoJS.js 脚本便是 hook 了apply方法,来获得加密密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
if (arguments.length === 2 && arguments[0] && arguments[1] && typeof arguments[1] === 'object' && arguments[1].length === 1 && hasEncryptProp(arguments[1][0])) {
if (Object.hasOwn(arguments[0], "$super") && Object.hasOwn(arguments[0], "init")) {
if (this.toString().indexOf('function()') !== -1 || /^\s*function(?:\s*\*)?\s+[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/.test(this.toString())) {
console.log(...arguments);

let encrypt_text = arguments[0].$super.toString.call(arguments[1][0]);
if (encrypt_text !== "[object Object]") {
// debugger;
console.log("对称加密后的密文:", encrypt_text);
} else {
console.log("对称加密后的密文:由于toString方法并未获取到,请自行使用上方打印的对象进行toString调用输出密文。");
}

let key = arguments[1][0]["key"].toString();
if (key !== "[object Object]") {
console.log("对称加密Hex key:", key);
} else {
console.log("对称加密Hex key:由于toString方法并未获取到,请自行使用上方打印的对象进行toString调用输出key。");
}

let iv = arguments[1][0]["iv"];

if (iv) {
if (iv.toString() !== "[object Object]") {
console.log("对称加密Hex iv:", iv.toString());
} else {
console.log("对称加密Hex iv:由于toString方法并未获取到,请自行使用上方打印的对象进行toString调用输出iv。");
}
} else {
console.log("对称加密时未用到iv")
}
if (arguments[1][0]["padding"]) {
console.log("对称加密时的填充模式:", arguments[1][0]["padding"]);
}
if (arguments[1][0]["mode"] && Object.hasOwn(arguments[1][0]["mode"], "Encryptor")) {
console.log("对称加密时的运算模式:", arguments[1][0]["mode"]["Encryptor"]["processBlock"]);
}
if (arguments[1][0]["key"] && Object.hasOwn(arguments[1][0]["key"], "sigBytes")) {
console.log("对称加密时的密钥长度:", get_sigBytes(arguments[1][0]["key"]["sigBytes"]));
}
console.log("%c---------------------------------------------------------------------", "color: green;");
}
}
  • arguments[0]对应instance
  • arguments[1]对应init.apply中的arguments,所以arguments[1][0]对应传入到create中的参数

哈希/HMAC

下面是SHA256生成实例和计算哈希的时序图

计算哈希是通过finalize方法

当创建SHA256实例时,也就是调用hasher.init方法时,会调用subtype.$super.init.apply,所以 Hook_CryptoJS.js 脚本依然是 hook apply方法,通过SHA256实例的原型链找的finalize并 hook,这样便能拿到计算哈希的原始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
else if (arguments.length === 2 && arguments[0] && arguments[1] && typeof arguments[0] === 'object' && typeof arguments[1] === 'object') {
if (arguments[0].__proto__ && Object.hasOwn(arguments[0].__proto__, "$super") && Object.hasOwn(arguments[0].__proto__, "_doFinalize") && arguments[0].__proto__.__proto__ && Object.hasOwn(arguments[0].__proto__.__proto__, "finalize")) {
// 这个判断是为了
if (arguments[0].__proto__.__proto__.finalize.toString().indexOf('哈希/HMAC') === -1) {
let temp_finalize = arguments[0].__proto__.__proto__.finalize;

arguments[0].__proto__.__proto__.finalize = function () {
if (!(Object.hasOwn(this, "init"))) {
let hash = temp_finalize.call(this, ...arguments);
console.log("哈希/HMAC 加密 原始数据:", ...arguments);
console.log("哈希/HMAC 加密 密文:", hash.toString());
console.log("哈希/HMAC 加密 密文长度:", hash.toString().length);
console.log("注:如果是HMAC加密,本脚本是hook不到密钥的,需自行查找。")
console.log("%c---------------------------------------------------------------------", "color: green;");
return hash;
}
return temp_finalize.call(this, ...arguments)
}
}
}
}

C.HmacSHA256 = Hasher._createHmacHelper(SHA256),对于HmacSHA256这里传入的依然是SHA256

由代码可知,最后依然是调用的Hasher.finalize,所以上面的 hook 脚本依然有效

参考链接

https://mp.weixin.qq.com/s/N27MJI7aqUuph_mbu1veAg

https://mp.weixin.qq.com/s/3rANOLlZyYgqa73pZs65jA


前端逆向学习之 crypto-js
http://www.weijin.ink/2026/01/24/前端逆向学习之-crypto-js/
作者
未尽
发布于
2026年1月24日
许可协议