涉及知识点
间接调用
在 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值,如果函数不在严格模式下,null和undefined将被替换为全局对象,并且原始值将被转换为对象
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", "!");
|
apply 方法
1 2
| apply(thisArg) apply(thisArg, argsArray)
|
thisArg:调用函数时要使用的this值,如果函数不处于严格模式,则null和undefined会被替换为全局对象,原始值会被替换为对象
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);
const min = Math.min.apply(null, numbers);
console.log(min);
|
闭包
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)); console.log(add10(2));
|
add5和add10都创建了闭包,它们共享相同的函数体定义,但是保存了不同的词法环境,在add5的词法环境中,x为 5,在add10的词法环境中,x则为 10
原型
原型是 JavaScript 对象相互继承特性的机制
1 2 3
| function Foo() {} let obj = new Foo(); Object.getPrototypeOf(obj) === Foo.prototype
|
几乎所有的对象都有原型,在浏览器中通常可以通过__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() { console.log('接收到的参数:', arguments);
let total = 0; for (let i = 0; i < arguments.length; i++) { total += arguments[i]; } return total; }
console.log(sum(1, 2)); console.log(sum(1, 2, 3, 4, 5)); console.log(sum());
|
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) { var subtype = create(this);
if (overrides) { subtype.mixIn(overrides); }
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { subtype.init = function () { subtype.$super.init.apply(this, arguments); }; }
subtype.init.prototype = subtype;
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]") { 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