Window和window的 1 2 3 4 5 Object .getOwnPropertyDescriptor (Window , "name" )Object .getOwnPropertyDescriptor (window , "name" )
为什么两者的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 class Window { constructor ( ) { } static name = "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.prototype
,Function.prototype
是所有函数对象的”工厂”,所以
1 Test .__proto__ === Function .prototype
而test.prototype
指的是test作为一个类,被它new出来的实例,这些实例是从哪里来的,所以
1 2 3 function Test ( ) {}const obj = new Test ()Test .prototype === obj.__proto__
换个形象的类比:
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 ); console .log (baby.__proto__ === Test .prototype ); console .log (Test .prototype .constructor === Test );
延申出来可以这样说,如果一个对象,它没有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 ); new arrow (); const person = { name : "Tom" , sayHi ( ) { console .log ("hi" ); } }; console .log (person.prototype ); function Human ( ) {}const h = new Human ();console .log (h.prototype );
记住这么理解就行,如果一个对象可以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 ( ) { const _mytoString=Function .prototype .toString const _symbol=Symbol () function _toString ( ) { if (typeof this ==="function" && this [_symbol]){ return this [_symbol] } return _mytoString.call (this ) } function setNative (func,key,value ) { Object .defineProperty (func,key,{ configurable :true , enumerable :false , writable :true , value :value }) } delete Function .prototype .toString setNative (Function .prototype ,"toString" ,_toString) setNative (Function .prototype .toString ,_symbol,'function toString() { [native code] }' ) 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 = function (...args ) { console .log ("hook 成功!参数是:" , args); return _atob.apply (this , args); };
此时去检测该函数,会返回篡改之后的函数体字符串
1 2 Function .prototype .toString .call (atob)
通过调用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
就是atob
,atob
是function
,并且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 )
JavaScript中检测函数被篡改的机制之检测name属性 再看一个demo,还是hook atob函数
1 2 3 atob = function fakeAtob ( ) { console .log ("hook 了" ); }
然后执行上面的sandBox.setNative(atob,'atob')
,此时会发现toString检测被成功绕过,但是当我们打印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 中,每一个构造函数(比如 Function
、Object
、Array
、Document
、Window
)都有一个 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 (); p2.sayHi ();
这个 sayHi
方法就定义在 Person.prototype
上,不是每个实例都有自己的 sayHi
,而是共享的。
那么,原型对象知道了,什么是原型对象的属性和方法呢?
原型对象当然也是一个对象,它也有自己的属性,属性值可以是任何东西(数值、字符串、函数)等等,原型对象方法,也就是说这个方法不是实例的,而是从它的原型(__proto__
)上继承来的
举个例子
1 2 3 const arr = [1 , 2 , 3 ];console .log (arr.push === Array .prototype .push );
arr
实例的push
方法根本不是它实例自己的,而是挂在原型对象Array.prototype
上的
再比如document.clear()
方法
1 console .log (document .clear === Document .prototype .clear );
也就是说,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
,而且没办法保留原来的属性描述符(比如 enumerable
、configurable
,至少在下面的sandBox.hook
的代码中是这样的
原型对象的访问器属性和数据属性 首先要明确一个问题,属性的“类型分类” 和属性值的“值的类型”要区分清楚,什么意思呢?比如document.clear()
和document.cookie
,clear()
我们平常理解就是Document.prototype
原型对象上的函数或者方法,cookie
是Document.prototype
原型对象上的属性,但是实际上这种理解是不对的。
对于一切皆对象的核心理念,clear
和cookie
都是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.cookie
,Object.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 );
这里的 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 ; user.age = 30 ;
hook代码在后面sandBox.hookObj(...)
关于浏览器全局对象 首先要明确,为什么需要在nodejs中模拟浏览器环境
因为在 Node.js 中,没有浏览器提供的 window
、document
、location
等全局对象、没有 DOM、BOM、事件系统,所以我们得用沙箱手动伪造 window
,使得运行在 Node.js 的反调试脚本或检测脚本“以为自己在浏览器中”。
DOM(Document Object Model)文档对象模型,是把HTML页面结构转换为 JavaScript 能操作的对象结构,比如document.getElementById('app').innerText = 'World';
,DOM 提供的对象有document
、Element
、Node
、Text
、HTMLDivElement
、document.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 环境,没有页面、没有标签、没有浏览器窗口,它不需要也不提供,没有 document
、window
,没有页面结构,也没有 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
,因为浏览器设计上就没太往上挂东西
第二,要区分普通函数和构造器函数,对于普通函数,比如atob
、alert
等等,这些函数是没办法被new
出新的实例的,即它们的.prototype
是undefined
,对于这种函数,可以直接调用sandBox.hook
去hook
就行了;但是对于构造器函数,比如window.XMLHttpRequest
等,这种就是可以new出来实例的,其原型上还挂载了很多别的属性,也就是说它的.prototype
是object
,可以new XMLHttpRequest()
,通过window.XMLHttpRequest.prototype
可以拿到原型上的open
、send
、abort
、setRequestHeader
等属性,对于构造器函数,把它当成原型去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" )); console .log (Person .prototype .hasOwnProperty ("speak" )); const p2 = new Person ();p2.walk = function ( ) { console .log ("I'm walking" ); }; console .log (p2.hasOwnProperty ("walk" )); console .log (Person .prototype .hasOwnProperty ("walk" ));
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) 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 ); proxyClear (); document .clear = new Proxy (document .clear , { apply (... ) { ... } });document .clear ();
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) 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.body
、window.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 ; } 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 } }; 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 sandBox.hookObj =function (obj,objName,propName,isDebug ) { let oldDescriptor=Object .getOwnPropertyDescriptor (obj,propName) let newDescriptor={} if (oldDescriptor.configurable ==false ){ return ; } newDescriptor.configurable =true newDescriptor.enumerable =oldDescriptor.enumerable ; if (oldDescriptor.hasOwnProperty ("writable" )){ newDescriptor.writable =oldDescriptor.enumerable } if (oldDescriptor.hasOwnProperty ("value" )){ let funcInfo={ objName :objName, funcName :propName } newDescriptor.value =sandBox.hook (oldDescriptor.value ,funcInfo,isDebug) } if (oldDescriptor.hasOwnProperty ("get" )){ let funcInfo={ objName :objName, funcName :`get ${propName} ` } newDescriptor.get =sandBox.hook (oldDescriptor.get ,funcInfo,isDebug) } if (oldDescriptor.hasOwnProperty ("set" )){ let funcInfo={ objName :objName, funcName :`set ${propName} ` } newDescriptor.set =sandBox.hook (oldDescriptor.set ,funcInfo,isDebug) } 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" ) ... 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' )