Window和window的

1
2
3
4
5
Object.getOwnPropertyDescriptor(Window, "name")
// => { value: "Window", writable: false, enumerable: false, configurable: true }

Object.getOwnPropertyDescriptor(window, "name")
// => { get: f, set: f, enumerable: true, configurable: true }

为什么两者的name属性不一样

1
2
Window.name 表示构造函数自己的名字(Window的name属性) → 数据属性
window.name 表示 window 对象的一个 web 标准 API → 访问器属性,可读可写

可以这样理解

1
2
3
4
5
6
7
8
9
10
11
12
13
// Window 是构造函数(类似类)
class Window {
constructor() { /* 创建 window */ }
static name = "Window" // 构造函数自己的名字
}

// window 是这个类的实例
const window = new Window()

Object.defineProperty(window, "name", {
get() { return "用于跨页面通信的值" },
set(val) { /* 做一些存储 */ }
})

__ proto__ 和prototype

首先来看一段代码

1
2
function test() {}
test.toString()

当调用test.toString()的时候发生了什么?

先说结论,当调用 test.toString() 时,其实内部走的是这个原型链:

1
test --> Function.prototype --> Object.prototype

由于这个原型链的存在,当调用test.toString的时候就会走如下逻辑

1
2
3
4
解释器会去 test 这个函数对象上找 toString 方法:
先在 test 自己身上找有没有 .toString 属性(找不到)
然后找它的原型:Function.prototype,这里就有 .toString
所以执行的是 Function.prototype.toString.call(test)

也就是说,test.toString() 实际调用的是 Function.prototype.toString

这就出现了一个什么现象呢?

1
2
3
4
function test() {}
test.toString()
delete test.prototype.toString
test.toString() //正常输出,不受影响

为什么这里删了不管用,因为应该删掉Function.prototype.toString,这样才是对的

现在进入正题,__proto__prototype的区别是什么?联系是什么?

首先我们要知道,一切皆对象,对于上面的test函数,其实也是一个对象,它既是一个函数,也是一个对象,它是别人new出来的,它也可以new出来别人,而这个test对象(函数)就是通过Function对象new出来的,同时,test作为一个对象,也可以new出来别人

test可以说既是一个类,也是一个实例,作为它可以new出来别人,它是一个类,但是它也是被Function对象new出来的实例

对于js中的大部分对象,都可以这样说,既是一个类,也是一个实例,对于函数对象,一定是这样,箭头函数除外。

然后再来看,为什么删掉delete test.prototype.toString 删除的不是 Function.prototype.toString

最重要的结论是,test.__proto__ !== test.prototype !!!

test.__proto__指的是test作为一个实例,它是从哪里来的,也就是test对象(实例)是被哪里new出来的,这里就是Function.prototypeFunction.prototype是所有函数对象的”工厂”,所以

1
Test.__proto__ === Function.prototype //true

test.prototype指的是test作为一个类,被它new出来的实例,这些实例是从哪里来的,所以

1
2
3
function Test() {}
const obj = new Test()
Test.prototype === obj.__proto__ //true

换个形象的类比:

1
2
function Test() {}
const baby = new Test();

我们就像在说:

1
2
3
4
5
Test是一个“工厂函数”,比如「老王造人厂」
Test.__proto__就是「老王造人厂」是谁开的 → 它的爹是 Function.prototype
Test.prototype是未来「老王」造出来的“人”要用的 DNA 模板 → 所有“人”都继承这个
baby.__proto__ === Test.prototype → baby 的原型是Test.prototype
所以 baby 继承了 Test.prototype 上的所有东西

那么就有如下结论

1
2
3
4
5
6
function Test() {}
const baby = new Test();

console.log(Test.__proto__ === Function.prototype); // true
console.log(baby.__proto__ === Test.prototype); // true
console.log(Test.prototype.constructor === Test); // true

延申出来可以这样说,如果一个对象,它没有prototype属性,也就是说它不能作为一个类去new别人!

没有prototype属性的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 箭头函数
const arrow = () => {};
console.log(arrow.prototype); //undefined
new arrow(); // TypeError: arrow is not a constructor

// 普通对象
// 通过字面量或 Object 创建出来的实例
const person = {
name: "Tom",
sayHi() { console.log("hi"); }
};
console.log(person.prototype); // undefined

// 实例对象
function Human() {}
const h = new Human();
console.log(h.prototype); // undefined

记住这么理解就行,如果一个对象可以new别人,就一定有prototype属性,如果是被别人new出来的,就有__proto__ 属性,对象一定有__proto__属性,但不一定有prototype属性

原型链的链式查找

先看一段代码

1
2
3
4
function Test() {}
const obj = new Test();
delete Test.prototype.toString;
console.log(obj.toString()); // 为什么不是报错?

注意Test.prototype其实是一个对象,就是{}那种对象,不是函数对象,它作为一个对象,它也是被别人new出来的,被别人生产出来的,生产它的就是object.prototype,所以Test.prototype.__proto__ === Object.prototype,不是Test.prototype.prototype === Object.prototype,普通对象是没有prototype 属性的,上面说过,也就是说把Test.prototype看成一个对象来说,它只能作为实例,不能作为类,不能new别人

所以说删掉Test.prototype这个”工厂”是不够的,还要删掉Object.prototype.toString这个”总工厂”

原型链如下

1
obj --> Test.prototype --> Object.prototype --> null

在查找的时候,如果obj本身没有toString()方法,就会去obj的上级工厂去找,就是Test.prototype.toSrting()存不存在?如果Test.prototype这个工厂没有toString()方法,就会去上上级工厂也就是Object.prototype去找,找到了就调用,找不到就报错,一直往上找

这里再说一下函数对象的原型链,还是上面的代码

1
2
Test.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype

注意这里Function.prototype会输出ƒ () { [native code] },并不是说链子已经到头了,而是这代表这是浏览器内置函数,是C++实现的,浏览器无法显示,其实本质也是一个特殊的对象

Object.prototype 是所有对象的祖先工厂(“老祖宗”), [native code] 只是外表,内部其实还是遵循 JS 的原型系统

JavaScript中检测函数被篡改的机制之toString

在现代的反调试、反hook技术中,最常见的方式是

1
Function.prototype.toString.call(被检测的函数)

这个方法返回该函数的“源码字符串”。

如果是原生函数,会返回:"function xxx() { [native code] }"

如果是用户定义的函数,会返回实际代码:"function xxx() { console.log('hello') }"

如果对函数做了 hook,它返回的字符串就变了,就会被检测到。

所以,要规避这种检测,要做的就是篡改Function.prototype.toString()函数,也就是说hook这个函数,让这个检测函数直接失效,当目标代码调用了这个函数的时候,调用的是我们篡改之后的假的Function.prototype.toString()方法

查看一下toString()方法的具体属性,执行Object.getOwnPropertyDescriptor(Function.prototype,"toString")

1
2
3
4
configurable: true
enumerable: false
value: ƒ toString()
writable: true

怎么篡改这个函数呢?先删掉原有的toString属性,把value的值给改成我们自己写的toString()就可以了,当目标js代码调用了Function.prototype.toString()的时候,我们的虚假toString()方法直接返回字符串function toString() { [native code] }

下面这段代码可以达到如下效果

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
sandBox={}
// 定义自执行函数
!function () {
// 把原生的Function.prototype.toString赋值给_mytoString
// 这样即使原来的toString被删除之后,还可以通过_mytoString去调用原生的toString方法
// _mytoString 是在删除之前就保存下来的函数引用,它是“被复制了一份”,所以删掉原生的toString不影响_mytoString的调用
const _mytoString=Function.prototype.toString
const _symbol=Symbol()

function _toString() {
if(typeof this==="function" && this[_symbol]){
return this[_symbol]
}
return _mytoString.call(this)
}

// 修改属性值,这里是修改func对象的key属性的value的值
function setNative(func,key,value) {
Object.defineProperty(func,key,{
configurable:true,
enumerable:false,
writable:true,
value:value
})
}
// 删除原生的Function.prototype.toString
delete Function.prototype.toString
// 把Function.prototype的toString属性设置为虚假的toString函数
setNative(Function.prototype,"toString",_toString)
// 给Function.prototype.toString这个函数对象添加一个Symbol属性,这个属性的值是一个伪装的字符串
setNative(Function.prototype.toString,_symbol,'function toString() { [native code] }')
// 通过sandBox把setNative函数暴露出去,供外部调用
// 供外部调用的setNative方法的作用就是把目标函数对象添加Symbol属性
sandBox.setNative=function (func,funcName){
setNative(func,_symbol,`function ${funcName || func.name}() { [native code] }`)
}
}()

这段代码怎么用的呢?执行的流程是什么?现在来过一下

比如针对原生的atob函数,我们hook了这个函数

1
2
3
4
5
6
7
8
// 保存原始的原生函数
const _atob = atob;

// 替换 atob 函数,加入 hook 行为
atob = function (...args) {
console.log("hook 成功!参数是:", args);
return _atob.apply(this, args); // 保留原始行为
};

此时去检测该函数,会返回篡改之后的函数体字符串

1
2
Function.prototype.toString.call(atob)
//'function (...args) {\n console.log("hook 成功!参数是:", args);\n return _atob.apply(this, args); // 保留原始行为\n}'

通过调用sandBox.setNative(atob,'atob'),会给atob函数对象加一个_symbol属性,属性值是'function atob() { [native code] }'

此时再调用检测Function.prototype.toString.call(atob);,会发生什么呢?

Function.prototype.toString.call(atob)被执行的时候,由于Function.prototype.toString其实被修改成了上面定义的虚假的_toString方法,所以其实调用的是_toString.call(atob),然后在函数内部执行如下逻辑

1
2
3
4
if(typeof this==="function" && this[_symbol]){
return this[_symbol]
}
return _mytoString.call(this)

this就是atobatobfunction,并且atob在上面已经被添加了_symbol属性,最终返回的就是的是this[_symbol]也就是atob[_symbol],返回的就是function atob() { [native code] }

那么这一句也很好理解了,setNative(Function.prototype.toString,_symbol,'function toString() { [native code] }')就是为了防止toString方法自己被检测的,可以尝试注释掉这一行代码,然后执行,会返回我们篡改之后的toString方法的函数体字符串

1
2
Function.prototype.toString.call(Function.prototype.toString)
//'function _toString() {\n if(typeof this==="function" && this[_symbol]){\n return this[_symbol]\n }\n return _mytoString.call(this)\n }'

JavaScript中检测函数被篡改的机制之检测name属性

再看一个demo,还是hook atob函数

1
2
3
atob = function fakeAtob() {
console.log("hook 了");
}

然后执行上面的sandBox.setNative(atob,'atob'),此时会发现toString检测被成功绕过,但是当我们打印atob.name属性的时候,就会暴露出虚假的函数名fakeAtob

1
2
atob.name
'fakeAtob'

可以通过Object.getOwnPropertyDescriptor(atob,"name")查看函数的name属性的相关信息

1
2
3
4
5
6
7
Object.getOwnPropertyDescriptor(atob,"name")
{value: 'atob', writable: false, enumerable: false, configurable: true}
configurable: true
enumerable: false
value: "atob"
writable: false
[[Prototype]]: Object

所以这个value属性也是需要修改的,把value属性的属性值设置为原生的函数名

代码如下

1
2
3
4
5
6
7
8
sandBox.reName=function (func,funcName) {
Object.defineProperty(func,"name",{
configurable:true,
enumerable:false,
writable:false,
value:funcName
})
}

hook实例方法和原型对象方法的区别

这个需要理解到位,因为是后续写hook原型对象方法代码的前提

首先需要理解,什么是实例方法,什么是原型对象方法?

(当然这里提原型对象方法不严谨,具体见后面原型对象的访问器属性和数据属性)

原型对象,在 JavaScript 中,每一个构造函数(比如 FunctionObjectArrayDocumentWindow)都有一个 prototype 属性,它指向一个“原型对象”,原型对象的作用就是:让同类的实例共享一些属性或方法,举一个通俗的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}

Person.prototype.sayHi = function () {
console.log("Hi, I'm " + this.name);
};

const p1 = new Person("张三");
const p2 = new Person("李四");

p1.sayHi(); // Hi, I'm 张三
p2.sayHi(); // Hi, I'm 李四

这个 sayHi 方法就定义在 Person.prototype 上,不是每个实例都有自己的 sayHi,而是共享的。

那么,原型对象知道了,什么是原型对象的属性和方法呢?

原型对象当然也是一个对象,它也有自己的属性,属性值可以是任何东西(数值、字符串、函数)等等,原型对象方法,也就是说这个方法不是实例的,而是从它的原型(__proto__)上继承来的

举个例子

1
2
3
const arr = [1, 2, 3];

console.log(arr.push === Array.prototype.push); // true

arr实例的push方法根本不是它实例自己的,而是挂在原型对象Array.prototype上的

再比如document.clear()方法

1
console.log(document.clear === Document.prototype.clear); // true

也就是说,document.clear()方法,不是挂在document实例上的,而是挂在Document类的prototype原型对象上的

那么 atob()document.clear() 有什么区别?

atob是挂在window实例上的,而不是Window.prototype这个原型对象,clear方法是挂在Document.prototype原型对象上的

为什么要讲这个区别呢?

因为在实际hook中,对于实例方法(atob)的hook,采用的是下面的sandBox.hook方法(代码在后面的章节),而对于原型对象的属性和方法的hook,采用的是下面sandBox.hookObj方法

为什么原型对象的方法的hook不能采用sandBox.hook函数呢?同样都是function,为什么不是通用的呢?

比如使用如下代码去达到hook的效果

1
document.clear = sandBox.hook(document.clear)

看似达到了效果,实际上没有达到,为什么这么说?因为上面的做法实际上是在document实例上添加了一个clear属性(这个clear属性原先在document实例上是不存在的),在执行完这一句之后,后面再调用的document.clear的时候,调用的就是实例上的clear方法了,而不是原型对象上的clear方法,因为存在调用链,当你调用 document.clear(),浏览器会这样找:

1
2
3
4
1. 先查document实例本身有没有clear属性
2. 如果没有,再沿着__proto__去找,也就是查Document.prototype
3. 如果再没有,就继续往上找Node.prototype、EventTarget.prototype...
4. 最后到Object.prototype

最后再补充一下,可以直接使用hook原型对象的方法达到hook的效果

1
Document.prototype.clear = sandBox.hook(Document.prototype.clear)

但是这样会存在一个问题,你得自己判断这个属性是 value 还是 get/set,而且没办法保留原来的属性描述符(比如 enumerableconfigurable,至少在下面的sandBox.hook的代码中是这样的

原型对象的访问器属性和数据属性

首先要明确一个问题,属性的“类型分类” 和属性值的“值的类型”要区分清楚,什么意思呢?比如document.clear()document.cookieclear()我们平常理解就是Document.prototype原型对象上的函数或者方法,cookieDocument.prototype原型对象上的属性,但是实际上这种理解是不对的。

对于一切皆对象的核心理念,clearcookie都是Document.prototype原型对象的属性!

对象的属性分为数据属性(data property)和访问器属性(accessor property),所有的属性都是这两种之一,而函数、字符串、对象等,是属性的值,只不过类型不同。

也就是说,数据属性的值可以是函数,例如document.clear 就是数据属性,值是函数。

访问器属性,也可以返回函数,比如document.cookie 的 getter 返回的是字符串,但也可以返回函数。

例如对于document.clear

1
Object.getOwnPropertyDescriptor(Document.prototype, "clear")

结果

1
2
3
4
5
6
{
value: ƒ clear(), // 说明是数据属性,值是函数
writable: true,
enumerable: true,
configurable: true
}

说明

1
2
3
它是数据属性
它的value是一个函数
它不是访问器属性(因为没有 get / set)

再看 document.cookieObject.getOwnPropertyDescriptor(Document.prototype, "cookie")

结果

1
2
3
4
5
6
{
get: ƒ cookie(), // 访问器属性
set: ƒ cookie(),
enumerable: true,
configurable: true
}

说明

1
2
3
它是访问器属性
getter 和 setter 本身是函数
访问这个属性时,不是直接拿 value,而是调用 get cookie() 函数返回值

也就是说了一个结论

1
2
3
函数不是一种属性类型,只是值类型。
属性类型只有两种:数据属性(有 value)和访问器属性(有 get/set)。
document.clear 本质上是:一个值为函数的数据属性。

哪怕是自己定义的一个对象person,里面有一个function speak方法,这个方法也是属性,值是函数

1
2
3
4
5
6
const person = {
name: "Alice",
speak() {
console.log("Hello!");
}
};

运行console.log(Object.getOwnPropertyDescriptor(person, "speak"));

1
2
3
4
5
6
{
value: ƒ speak(),
writable: true,
enumerable: true,
configurable: true
}

继续往后说明第二个问题,对于get/set属性,他们的属性里有name字段

1
2
3
get: ƒ cookie()
length: 0
name: "get cookie"

这个name字段,默认值一般都是 get + 属性名 或者 set + 属性名,而且一般没啥用,只是我们在模拟浏览器对象的时候需要设置一下,这样模拟的更真实

第三个问题,get/set所定义的函数什么时候触发?比如cookie,当访问 document.cookie 时,触发的是 getter,浏览器会返回当前页面的所有cookie;当设置 document.cookie = "a=1" 时,触发的是 setter

最后补充一个语法,但是不算很重要,js的访问器属性(accessor property)语法

1
2
3
4
5
6
7
const obj = {
get something() {
return 123;
}
};

console.log(obj.something); // 输出 123

这里的 something 不是一个函数名,而是一个“属性名”,但给它设置了一个 getter —— 每次访问这个属性,都会执行 get 后面那个函数。

1
2
get: 定义当访问某个属性时应该执行的函数
set: 定义当给某个属性赋值时应该执行的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const user = {
_age: 18,

get age() {
console.log("访问 age 属性");
return this._age;
},

set age(val) {
console.log("设置 age 属性为:", val);
this._age = val;
}
};

user.age; // 触发getter
user.age = 30; // 触发setter

hook代码在后面sandBox.hookObj(...)

关于浏览器全局对象

首先要明确,为什么需要在nodejs中模拟浏览器环境

因为在 Node.js 中,没有浏览器提供的 windowdocumentlocation 等全局对象、没有 DOM、BOM、事件系统,所以我们得用沙箱手动伪造 window,使得运行在 Node.js 的反调试脚本或检测脚本“以为自己在浏览器中”。

DOM(Document Object Model)文档对象模型,是把HTML页面结构转换为 JavaScript 能操作的对象结构,比如document.getElementById('app').innerText = 'World';,DOM 提供的对象有documentElementNodeTextHTMLDivElementdocument.querySelector()createElement()

BOM(Browser Object Model)浏览器对象模型,提供与浏览器窗口、导航器、地址栏、历史记录等交互的接口,BOM 提供的对象有window(全局对象)、navigator(浏览器信息)、location(地址栏)、history(浏览历史)、screen(屏幕信息)、alert(), confirm(), setTimeout()(其实也是 window 上的)

事件系统(DOM Events),浏览器提供的一整套事件机制,比如监听点击、输入、加载、键盘、鼠标移动等,比如document.getElementById("btn").addEventListener

对于nodejs而言,Node 是运行在服务端的 JavaScript 环境,没有页面、没有标签、没有浏览器窗口,它不需要也不提供,没有 documentwindow,没有页面结构,也没有 DOM API,没有浏览器历史、导航栏、弹窗

这也是为什么需要在node中模拟的原因。

对于window全局对象的hook,先看代码再来解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sandBox.hookGlobal=function (isDebug){
for(const key in Object.getOwnPropertyDescriptors(window)){
if(typeof window[key]==='function'){
if(typeof window[key].prototype==='object'){
sandBox.hookProto(window[key],isDebug)
}
else if (typeof window[key].prototype==='undefined'){
let funcInfo={
objName:"globalThis",
funcName:key
}
sandBox.hook(window[key],funcInfo,isDebug)
}
}
}
log("{hook|globalThis}")
}

第一,要知道window对象上有数千种属性,而且前面也提到过,这些属性都是挂在window实例上,而不是Window.prototype上的,这也是为什么前面要用Object.getOwnPropertyDescriptors(window)去获取全局对象的所有属性,不去遍历Window.prototype,因为浏览器设计上就没太往上挂东西

第二,要区分普通函数和构造器函数,对于普通函数,比如atobalert等等,这些函数是没办法被new出新的实例的,即它们的.prototypeundefined,对于这种函数,可以直接调用sandBox.hookhook就行了;但是对于构造器函数,比如window.XMLHttpRequest等,这种就是可以new出来实例的,其原型上还挂载了很多别的属性,也就是说它的.prototypeobject,可以new XMLHttpRequest(),通过window.XMLHttpRequest.prototype可以拿到原型上的opensendabortsetRequestHeader等属性,对于构造器函数,把它当成原型去hook就行了,通过sandBox.hookProto去hook

第三,说明一下为什么会存在原型上没有但是实例上有的属性,比如如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
speak() {
console.log("I'm speaking");
}
}

const p1 = new Person();
console.log(p1.hasOwnProperty("speak")); // false
console.log(Person.prototype.hasOwnProperty("speak")); // true
//说明:speak() 是定义在原型上的,实例 p1 是通过 [[Prototype]] 继承获得的。

const p2 = new Person();
p2.walk = function () {
console.log("I'm walking");
};

console.log(p2.hasOwnProperty("walk")); // true
console.log(Person.prototype.hasOwnProperty("walk")); // false
//说明:walk() 是定义在实例 p2 自身上的,和原型无关

proxy hook各种方法

Proxy是什么?

1
new Proxy(target, handler)
1
2
3
4
5
6
7
target:你要代理的原始对象。
handler:一个对象,里面包含一系列“拦截器函数”,用来拦截各种操作,比如:
get:读取属性
set:设置属性
apply:调用函数
construct:用 new 构造实例
ownKeys:Object.keys 或 for...in

hook get方法

先看一段demo

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
sandBox.proxy=function (obj,objName) {
if(!sandBox.config.proxy){
return obj
}
let handler={
get(target,p,receiver){
let result;
try{
result=Reflect.get(target,p,receiver)
let type=sandBox.getType(result)
if(result instanceof Object){
console.log(`{obj|get:[${objName}] -> prop:[${p.toString()}] -> type:[${type}]}`)
result=sandBox.proxy(result,`${objName}.${p.toString()}`)
}else if(typeof result==="symbol"){
console.log(`{obj|get:[${objName}] -> prop:[${p.toString()}] -> result:[${result.toString()}]}`)
}else{
console.log(`{obj|get:[${objName}] -> prop:[${p.toString()}] -> result:[${result}]}`)
}
}catch (e) {
console.log(`{obj|get:${objName} -> prop:${p.toString()} -> error:${e.message}}`)
}
return result
}
}
return new Proxy(obj,handler)
}

第一,对get(target, p, receiver) 参数的解释

1
2
3
target: 原始被代理的对象
p:被访问的属性名(可以是字符串或 symbol)
receiver:通常是代理本身(proxy 本体)

第二,调用原有的函数,result=Reflect.get(target,p,receiver)

第三,为什么要调用 sandBox.getType(result)获取返回值的类型?这一步是为了知道取出来的值的类型,方便调试日志打印。如果是对象,说明可以继续代理嵌套属性;如果是函数、字符串、数字等,直接打印结果;如果是 symbol,打印 symbol 的字符串形式,避免 [object Symbol] 看不清

第四,为什么对象要递归 proxy,而非对象不需要?

举个例子,假设有如下结构

1
2
3
4
5
6
7
8
let obj = {
inner: {
name: "test",
sayHi() {
console.log("hi");
}
}
}

调用 sandBox.proxy(obj, 'obj'),只对 obj 做了代理。如果不递归处理,那么

1
2
obj.inner.name     不会触发 proxy,因为inner本身没有被代理
proxyObj.inner.name 只触发一次 get(proxyObj, 'inner'),拿到原始对象

第四,嵌套hook的执行流程是什么?当然这个不重要,作为了解即可

以上面只hook get的代码为例,hook上面的obj对象

1
2
3
4
5
6
7
8
9
let obj = {
inner: {
name: "test",
sayHi() {
console.log("hi");
}
}
}
let proxiedObj = sandBox.proxy(obj, "obj");

当触发访问proxiedObj.inner.name;的时候,执行流程逐步拆解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
proxiedObj.inner.name;

第一步:访问 proxiedObj.inner
1.proxiedObj是Proxy(obj, handler)创建的代理对象;
2.访问.inner,进入get(target, p, receiver) 拦截器;
3.target是obj,p是 "inner";
4.执行Reflect.get(obj, "inner", proxyObj)得到原始对象{name:"test",sayHi(){}};
5.判断是 instanceof Object,于是调用:
result = sandBox.proxy(result, 'obj.inner');
6.sandBox.proxy(innerObj, 'obj.inner') 被调用,创建一个新的 Proxy(innerObj, handler),返回的是proxyInner
注意:此时不会进入 proxyInner 的拦截逻辑,因为你只是创建了这个 Proxy,并没有对它执行任何操作。

第二步:访问 proxyInner.name
7.继续回到刚才的get拦截器中,它会return result,也就是 proxyInner;
8.现在,外部代码继续执行.name,其实是proxyInner.name;
9.再次进入 get(innerObj, 'name', proxyInner) 拦截器;
10.获取值 "test",不是对象,不用递归代理;
11.最终返回 "test"

嵌套代理本质上是:每次访问属性时,发现值是对象,就提前为它“安装代理”,下一次访问这个返回值的属性时,新的 Proxy 接管逻辑。

hook getOwnPropertyDescriptor方法

再看另一段demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getOwnPropertyDescriptor(target, p) {
let result;
try{
result=Reflect.getOwnPropertyDescriptor(target,p)
let type=sandBox.getType(result)
// if(typeof result!=="undefined"){
// result=sandBox.proxy(result,`${objName}.${p.toString()}.property`)
// }
console.log(`{obj|getOwnPropertyDescriptor:[${objName}] -> prop:[${p.toString()}] -> result:[${JSON.stringify(result)}]}`)
}catch (e) {
console.log(`{obj|getOwnPropertyDescriptor:${objName} -> prop:${p.toString()} -> error:${e.message}}`)
}
return result
}

这也是 Proxy 的一个拦截器,用来拦截对目标对象属性描述符的访问行为,等价于Object.getOwnPropertyDescriptor(obj, prop)

什么时候被触发呢?它在访问某个属性的属性描述符时被触发,常见情况包括

操作 是否触发 getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor(obj, "foo")
Object.keys(obj) / for...in 否(触发 ownKeys
Object.defineProperty(...) 有可能会触发
Object.getOwnPropertyDescriptors(obj) 是(底层遍历调用)
Reflect.getOwnPropertyDescriptor(obj, p)
Object.prototype.hasOwnProperty.call(obj, p)

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
apply(target, thisArg, argArray) {
let result;
try{
result=Reflect.apply(target, thisArg, argArray)
let type=sandBox.getType(result)
//获取参数
let args = argArray.map(arg => {
if (typeof arg === "function") {
return `function ${arg.name||''}`;
} else if (arg instanceof Object) {
try {
return JSON.stringify(arg);
} catch (e) {
return arg.toString();
}
} else if (typeof arg === "symbol") {
return arg.toString();
} else {
return arg;
}
});
if(result instanceof Object){
console.log(`{func|apply:[${objName}] -> type:[${type}] ->args:[${args}]}`)
}else if(typeof result ==="symbol"){
console.log(`{func|apply:[${objName}] -> result:[${result.toString()}]} ->args:[${args}]`)
}else{
console.log(`{func|apply::[${objName}] -> result:[${result}] ->args:[${args}]}`)
}

}catch (e) {
console.log(`{func|apply:${objName} -> error:${e.message}}`)
}
return result
}

参数解释

1
2
3
target: 被代理的原始函数
thisArg: 调用时的 this 上下文
argArray: 调用时传入的参数列表(是个数组)

第一,为什么要判断typeof arg === "function"也就是参数是函数的情况?因为可以把一个函数作为参数传给另一个函数,所以 hook 的函数可能接收到函数作为参数,必须特殊处理,不能直接 JSON.stringify()。比如

1
2
3
document.addEventListener("click", function handleClick(e) {
console.log("clicked");
});

第二,什么时候触发?当代理的对象是函数,函数被调用的时候触发。

如果只有上面的一段hook逻辑(没有hook get/set这些)的话,想要触发,只有显式去hook目标方法才能触发,以document.clear为例

1
2
3
4
5
6
7
8
9
10
11
12
let proxyClear = new Proxy(document.clear, {
apply(target, thisArg, argArray) {
console.log("apply trap called:", argArray);
return Reflect.apply(target, thisArg, argArray);
}
});

proxyClear.call(document); // 会触发 apply
proxyClear(); // 会触发 apply
// 或者
document.clear = new Proxy(document.clear, { apply(...) { ... } });
document.clear(); // 会触发 apply

proxy hook各种方法

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
sandBox.proxy=function (obj,objName) {
if(!sandBox.config.proxy){
return obj
}
let handler={
get(target,p,receiver){
let result;
try{
result=Reflect.get(target,p,receiver)
let type=sandBox.getType(result)
if(result instanceof Object){
console.log(`{obj|get:[${objName}] -> prop:[${p.toString()}] -> type:[${type}]}`)
result=sandBox.proxy(result,`${objName}.${p.toString()}`)
}else if(typeof result==="symbol"){
console.log(`{obj|get:[${objName}] -> prop:[${p.toString()}] -> result:[${result.toString()}]}`)
}else{
console.log(`{obj|get:[${objName}] -> prop:[${p.toString()}] -> result:[${result}]}`)
}
}catch (e) {
console.log(`{obj|get:${objName} -> prop:${p.toString()} -> error:${e.message}}`)
}
return result
},
set(target, p, newValue, receiver){
let result;
try{
result=Reflect.set(target, p, newValue, receiver)
let type=sandBox.getType(newValue)
if(newValue instanceof Object){
console.log(`{obj|set:[${objName}] -> prop:[${p.toString()}] -> newValue_type:[${type}]}`)
}else if(typeof newValue==="symbol"){
console.log(`{obj|set:[${objName}] -> prop:[${p.toString()}] -> result:[${newValue.toString()}]}`)
}else{
console.log(`{obj|set:[${objName}] -> prop:[${p.toString()}] -> newValue:[${newValue}]}`)
}
}catch (e) {
console.log(`{obj|set:${objName} -> prop:${p.toString()} -> error:${e.message}}`)
}
return result
},
getOwnPropertyDescriptor(target, p) {
let result;
try{
result=Reflect.getOwnPropertyDescriptor(target,p)
let type=sandBox.getType(result)
// if(typeof result!=="undefined"){
// result=sandBox.proxy(result,`${objName}.${p.toString()}.property`)
// }
console.log(`{obj|getOwnPropertyDescriptor:[${objName}] -> prop:[${p.toString()}] -> result:[${JSON.stringify(result)}]}`)
}catch (e) {
console.log(`{obj|getOwnPropertyDescriptor:${objName} -> prop:${p.toString()} -> error:${e.message}}`)
}
return result
},
defineProperty(target, p, attributes) {
let result;
try{
result=Reflect.defineProperty(target,p,attributes)
console.log(`{obj|defineProperty:[${objName}] -> prop:[${p.toString()}] -> result:[${JSON.stringify(attributes)}]}`)
}catch (e) {
console.log(`{obj|defineProperty:${objName} -> prop:${p.toString()} -> error:${e.message}}`)
}
return result
},
apply(target, thisArg, argArray) {
let result;
try{
result=Reflect.apply(target, thisArg, argArray)
let type=sandBox.getType(result)
//获取参数
let args = argArray.map(arg => {
if (typeof arg === "function") {
return `function ${arg.name||''}`;
} else if (arg instanceof Object) {
try {
return JSON.stringify(arg);
} catch (e) {
return arg.toString();
}
} else if (typeof arg === "symbol") {
return arg.toString();
} else {
return arg;
}
});
if(result instanceof Object){
console.log(`{func|apply:[${objName}] -> type:[${type}] ->args:[${args}]}`)
}else if(typeof result ==="symbol"){
console.log(`{func|apply:[${objName}] -> result:[${result.toString()}]} ->args:[${args}]`)
}else{
console.log(`{func|apply::[${objName}] -> result:[${result}] ->args:[${args}]}`)
}

}catch (e) {
console.log(`{func|apply:${objName} -> error:${e.message}}`)
}
return result
},
construct(target, argArray, newTarget) {
let result;
try{
result=Reflect.construct(target, argArray, newTarget)
let type=sandBox.getType(result)
console.log(`{obj|construct:[${objName}] -> type:[${type}]}`)
}catch (e) {
console.log(`{obj|construct:${objName} -> error:${e.message}}`)
}
return result
},
has(target, p) {
let result;
try{
result=Reflect.has(target,p)
console.log(`{obj|has:[${objName}] -> prop:[${p.toString()}] -> has:[${result}]}`)
}catch (e) {
console.log(`{obj|has:${objName} -> error:${e.message}}`)
}
return result
},
deleteProperty(target, p) {
let result;
try{
result=Reflect.deleteProperty(target,p)
console.log(`{obj|deleteProperty:[${objName}] -> prop:[${p.toString()}] -> result:[${result}]}`)
}catch (e) {
console.log(`{obj|deleteProperty:${objName} -> error:${e.message}}`)
}
return result
},
getPrototypeOf(target) {
let result;
try{
result=Reflect.getPrototypeOf(target)
console.log(`{obj|getPrototypeOf:[${objName}]}`)
}catch (e) {
console.log(`{obj|getPrototypeOf:${objName} -> error:${e.message}}`)
}
return result
},
setPrototypeOf(target, v) {
let result;
try{
result=Reflect.setPrototypeOf(target,v)
console.log(`{obj|setPrototypeOf:[${objName}]}`)
}catch (e) {
console.log(`{obj|setPrototypeOf:${objName} -> error:${e.message}}`)
}
return result
},
preventExtensions(target){
let result = Reflect.preventExtensions(target);
try{
console.log(`{obj|preventExtensions:[${objName}]}`);
}catch (e) {
console.log(`{obj|preventExtensions:${objName} -> error:${e.message}}`)
}
return result;
},
isExtensible(target){
let result = Reflect.isExtensible(target);
try{
console.log(`{obj|isExtensible:[${objName}]}`);
}catch (e) {
console.log(`{obj|isExtensible:${objName} -> error:${e.message}}`)
}
return result;
},
ownKeys: function (target){
let result = Reflect.ownKeys(target);
try{
console.log(`{obj|ownKeys:[${objName}]}`);
}catch (e) {
console.log(`{obj|ownKeys:${objName} -> error:${e.message}}`)
}
return result
},
}
return new Proxy(obj,handler)
}

proxy hook代码的执行流程

前面说过,如果只hook了apply方法的话,此时只有显式代理document.clear = new Proxy(document.clear, { apply(...) { ... } });的时候才会触发,那么上面的代码,hook了一堆方法,此时只代理document对象document = sandBox.proxy(document, "document");,然后调用document.clear方法,会触发到apply吗?

答案是会的,当然前提是必须先访问了 document.clear 属性,这才会进入的 get 方法的hook逻辑,从而返回一个被包了一层 proxy 的函数(也就是 apply 能接管的函数)。

流程分析为什么这样能hook到apply,因为代码中定义了get方法的hook

1
2
3
4
5
6
7
8
9
get(target, p, receiver) {
result = Reflect.get(target, p, receiver);
...
if (result instanceof Object) {
result = sandBox.proxy(result, `${objName}.${p.toString()}`);
}
...
return result;
}

所以当我们执行:

1
2
document = sandBox.proxy(document, "document");
document.clear();

会发生以下步骤:

1
2
3
4
5
6
7
8
9
1.document.clear会触发get
get的hook逻辑被触发:参数是 target = document,p = "clear"。
得到 result = document.clear(是个函数),而result instanceof Object为真

2.走到
if (result instanceof Object) {
result = sandBox.proxy(result, `${objName}.${p.toString()}`); // 就是 "document.clear"
}
逻辑,又对这个函数进行了再次proxy,而这个proxy又实现apply的hook。

最后再强调一下,只要目标是对象(Object),任何对其属性的读取都会先触发 get 的hook,无论这个属性值是基本类型(数字、字符串、布尔)、函数(普通函数、构造函数、箭头函数)、对象(比如 document.bodywindow.XMLHttpRequest

当然,函数也是Object

sanBox封装的其他关键功能函数

hook实例方法

这里是传了6个参数,但是实际在调用的时候不需要传这么多,因为有些参数在没有获取到传参的时候会自动设置一个默认值

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
52
53
54
55
56
sandBox.hook=function (func,funcInfo,isDebug,onEnter,onLeval,isExec) {
if(typeof func!='function'){
return func
}
if(funcInfo===undefined){
funcInfo={
objName:"globalThis",
funcName:func.name || ''
}
}
if(isDebug===undefined){
isDebug=false
}
if(!onEnter){
onEnter=function (obj){
log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]函数执行前传入的参数:${JSON.stringify(obj.args)}}`)
}
}
if(!onLeval){
onLeval=function (obj) {
log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]函数执行后生成的结果:${obj.result?obj.result.toString():[]}}`)
}
}
if(isExec===undefined){
isExec=true
}

hookFunc=function hookFunc() {
if(isDebug){
debugger;
}
// obj对象用来存放函数的参数
let obj={};
obj.args=[]
for(let i=0;i<arguments.length;i++){
obj.args[i]=arguments[i]
}
if(isExec){
//函数执行前
onEnter.call(this,obj)
//函数执行中
let result=func.apply(this,obj.args)
obj.result=result
//函数执行后
onLeval.call(this,obj)
return result
}
};
//保护代码
//funcInfo不是必要传的参数
//因为在上面的逻辑中,funcInfo.funcName会被默认设置为传入的func.name属性(原生的正确的name属性)
sandBox.setNative(hookFunc,funcInfo.funcName);
sandBox.reName(hookFunc,funcInfo.funcName);

return hookFunc
}

如果想测试这段函数,可以直接运行atob = sandBox.hook(atob)

hook原型对象属性

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
// obj是原型对象,objName是原型对象字符串,propName是原型对象的属性
sandBox.hookObj=function (obj,objName,propName,isDebug) {
// 获取原型对象属性的原有属性
let oldDescriptor=Object.getOwnPropertyDescriptor(obj,propName)
let newDescriptor={}
// 如果原型对象的属性的configurable是false,也就是不能被删除、不能被重定义
// 不能再用delete obj.prop删除,不能用Object.defineProperty()去重新定义这个属性的特征
if(oldDescriptor.configurable==false){
return;
}
// 给configurable和enumerable字段赋值
newDescriptor.configurable=true
newDescriptor.enumerable=oldDescriptor.enumerable;
if(oldDescriptor.hasOwnProperty("writable")){
newDescriptor.writable=oldDescriptor.enumerable
}
if(oldDescriptor.hasOwnProperty("value")){
// 如果有value属性,意味着是一个函数,就调用上面的sandBox.hook去hook
// 上面讲hook实例方法和原型对象方法也说过了,可以这样hook
let funcInfo={
objName:objName,
funcName:propName
}
// .value属性值是一个函数,就可以hook这个函数
newDescriptor.value=sandBox.hook(oldDescriptor.value,funcInfo,isDebug)
}
if(oldDescriptor.hasOwnProperty("get")){
let funcInfo={
objName:objName,
funcName:`get ${propName}`
}
// 如果有get属性,就hook get方法
// .get属性值是一个函数,就可以hook这个函数
newDescriptor.get=sandBox.hook(oldDescriptor.get,funcInfo,isDebug)
}
if(oldDescriptor.hasOwnProperty("set")){
let funcInfo={
objName:objName,
funcName:`set ${propName}`
}
// .set属性值是一个函数,就可以hook这个函数
newDescriptor.set=sandBox.hook(oldDescriptor.set,funcInfo,isDebug)
}
// 把新的属性值赋值给原型对象的属性,此时的属性值就是hook之后的被篡改函数
Object.defineProperty(obj,propName,newDescriptor)
}

测试的话,sandBox.hookObj(Document.prototype,"Document.prototype","cookie")

hook原型对象

枚举原型对象下的所有属性,然后调用hookObj去hook

1
2
3
4
5
6
7
sandBox.hookProto=function (obj,isDebug) {
let protoObj=obj.prototype
for(const key in Object.getOwnPropertyDescriptors(protoObj)){
sandBox.hookObj(protoObj,obj.name,key,isDebug)
}
log(`{hookProto|${protoObj}}`)
}

hook全局对象window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sandBox.hookGlobal=function (isDebug){
for(const key in Object.getOwnPropertyDescriptors(window)){
if(typeof window[key]==='function'){
if(typeof window[key].prototype==='object'){
sandBox.hookProto(window[key],isDebug)
}
else if (typeof window[key].prototype==='undefined'){
let funcInfo={
objName:"globalThis",
funcName:key
}
sandBox.hook(window[key],funcInfo,isDebug)
}
}
}
log("{hook|globalThis}")
}

proxy hook代码

上面有

使用sandBox实现补浏览器环境

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
window=globalThis;
window.xjb=null;

window.location={
host:'y.qq.com'
}
window.navigator={
userAgent:"Python3.0"
}
window.document={}

sandBox={}
sandBox.config={}
sandBox.config.proxy=true

sandBox.getType=function (obj) {
return Object.prototype.toString.call(obj)
}

sandBox.proxy=function (obj,objName) {
//...
}

// 代理对象
window=sandBox.proxy(window,"window")
location=sandBox.proxy(location,"location")
document=sandBox.proxy(document,"document")
navigator=sandBox.proxy(navigator,"navigator")

//webpack代码
...
//调用加密函数
var i, o = window.xjb(350).default;
console.log(o('{"comm":{"cv":4747474,"ct":24,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"yqq.json","needNewCode":1,"uin":"1152921504968805528","g_tk_new_20200303":377412991,"g_tk":377412991},"req_1":{"method":"DoSearchForQQMusicDesktop","module":"music.search.SearchCgiService","param":{"remoteplace":"txt.yqq.center","searchid":"67988778323096897","search_type":0,"query":"奢香1","page_num":1,"num_per_page":10}}}'))
console.log('zzb1f669e9dac6rzcxrocuptb9bpibs3q923ed882')