02--JS02--高级
JavaScript02: 高级进阶
一. 变量声明
1.1 变量提升
// 以下代码,或多或少会有些问题的
function fn(){
console.log(name);
var name = '大马猴';
}
fn()
// 问题:
name变量先使用,再定义 这么写代码,在其他语言里. 绝对是不允许的
但是在js里,不但允许,还能执行,为什么呢?
因为在js执行的时候,它会首先检测整体代码,发现在代码中会有name使用
则运行时,就会自动变成这样的逻辑:
// 变量提升的逻辑:
function fn(){
var name;
console.log(name);
name = '大马猴';
}
fn()
console.log(a); // undefined
// 看到了么,实际运行的时候和写代码的顺序可能会不一样
这种 把变量 提升到代码块第一部分运行 的逻辑,被称为变量提升
1.2 let 声明变量
结论一:用let声明变量,是新版本javascript ES6 提倡的一种声明变量的方案
// ES6提出 用let 声明变量,防止变量提升的逻辑
function fn(){
console.log(name); // 直接报错, let变量不可以变量提升
let name = '大马猴';
}
fn()
结论二:在同一个作用域内. let声明的变量只能声明一次,其他使用上和var没有差别
function fn(){
// console.log(name); // 直接报错, let变量不可以变量提升.
// let name = '大马猴';
var name = "周杰伦";
var name = "王力宏"; // 不会报错
console.log(name);
}
fn()
// var本意是声明变量,同一个变量,被声明两次(都是函数体内部,局部变量),显然也是不合理的
ES6规定:let声明的变量,在同一个作用域内,只能声明一次
function fn(){
// console.log(name); // 直接报错, let变量不可以变量提升.
// let name = '大马猴';
let name = "周杰伦";
console.log(name);
let name = "王力宏"; // 报错,同一个作用域 let 声明同一个变量,只能一次
console.log(name);
}
fn()
// 注意: 报错是发生在代码检查阶段. 所以上述代码,根本就执行不了
二. 闭包函数
先看一段代码
let name = "周杰伦"
function chi(){
name = "吃掉"
}
chi();
console.log(name); // "吃掉"
// 发现没有: 在函数内部修改,外部的变量是十分容易的一件事. 尤其是全局变量. 这是非常危险的.
// 试想 写了一个函数. 要用到name, 结果被别人写的某个函数给修改掉了...
接下来,看一个案例:
准备两个工具人. 来编写代码. 分别是js01和js02.
// 1号工具人
var name = "alex"
// 定时器,5000秒后执行
setTimeout(function(){
console.log("一号工具人:" + name) // 一号工具人还以为是alex呢, 但是该变量是不安全的.
}, 5000);
// 2号工具人
var name = "周杰伦"
console.log("二号工具人", name);
html:
<script src="js01.js"></script>
<script src="js02.js"></script>
此时运行的结果:
很明显, 虽然各自js在编写时是分开的. 但是在运行时, 是在同一个空间内执行的. 他们拥有相同的作用域
此时的变量势必是非常非常不安全的. 那么如何来解决呢?
注意:在js里 变量是有作用域的. 也就是说一个变量的声明和使用是有范围的,不是无限的
// 验证: 变量是有作用域的
function fn(){
let love = "爱呀"
}
fn()
console.log(love)
// 直接就报错了 也就是说. 在js里 变量作用域是有全局和局部 的概念
直接声明在最外层的变量,就是全局变量。所有函数、所有代码块都可以共享的
在函数内和代码块内声明的变量,尤其是函数内,声明出来的变量它是一个局部变量,外界是无法进行访问的
我们就可以利用这一点,来给每个工具人创建一个局部空间. 就像这样:
// 1号工具人 都是自运行函数
(function(){
var name = "alex";
setTimeout(function(){
console.log("一号工具人:"+name)
}, 5000);
})()
// 二号工具人 都是自运行函数
!function(){
var name = "周杰伦"
console.log("二号工具人", name);
}()
运行结果
这样,虽然解决了变量的冲突问题
但是想想. 如果在外面,需要函数内部的一些东西,来进行相关操作,怎么办?
比如 一号工具人要提供一个功能(加密),外界要调用, 怎么办?
// 1号工具人
// 局部函数中,对外接口 方式一: return 返回到 全局变量中 ===> 闭包函数
// 1.首先:全局要使用,那js文件中,就不能是自运行函数,要设置一个名字
let jiami = (function(){
let key = "10086" // 假装我是秘钥
// 我是一个加密函数
let mi = function(data){ // 数据
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+key);
console.log("数据:"+data);
// 返回密文
return "我要返回密文";
}
// 2.其次:外面可能需要用到该功能. 故 需要该变量返回(暴露到全局空间). 返回加密函数
return mi;
})();
// 局部函数中,对外接口 方式二:借助于window对象,将返回的变量 直接 赋值到全局变量中
(function(){
let key = "10086" // 假装我是秘钥
// 我是一个加密函数
let mi = function(data){ // 数据
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+key);
console.log("数据:"+data);
// 返回密文
return "我要返回密文";
}
// 对外的接口
window.mi = mi;
})();
注意:如果封装一个加密js包的时候,就还得准备出解密的功能
并且, 不可能一个js包就一个功能吧。 那也太痛苦了(js文件起名字),那怎么办?
可以返回一个对象,对象里面可以存放好多个功能
而一些不希望外界触碰的功能. 就可以很好的保护起来.
// 1号工具人.
let jiami = (function(){
let key = "10086" // 加装我是秘钥
// 该函数只属于该模块内部,外界无法访问. 就不返回
let n = {
abc:function(){
console.log("我是abc. 你叫我干什么?")
}
}
// 外面需要用到的功能,就进行返回.
return {
rsa_jiami: function(data){
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+this.get_rsa_key() + key);
n.abc();
console.log("数据:"+data);
return "我要返回密文";
},
aes_jiami: function(data){
console.log("接下来, 我要加密了,aes哦. 很厉害的")
console.log("秘钥:"+this.get_aes_key());
n.abc();
console.log("秘钥:"+key);
console.log("数据:"+data);
return "我要返回密文";
},
get_rsa_key: function() {
return this.rsa_key = "rsa的key", this.rsa_key
},
get_aes_key: function() {
return this.rsa_key = "aes的key", this.rsa_key
}
}
})();
html里面使用时:
<script>
miwen = jiami.rsa_jiami("吃你的糖葫芦吧");
console.log(miwen);
</script>
OK. 至此. 何为闭包? 上面这个就是闭包
相信你百度一下就会知道,什么内层函数使用外层函数变量、什么让一个变量常驻内存等等
其实细看,它之所以称之为闭包,它是一个封闭的环境。在内部. 自己和自己玩儿
避免了对该模块内部的冲击和改动. 避免的变量之间的冲突问题
闭包的特点:
- 内层函数对外层函数变量的使用
- 会让变量常驻于内存
这俩玩意就不解释了, 和python的闭包是一个意思。不懂没关系,能看懂它的执行过程就好
三. JS中的各种操作(非交互)
3.1 定时器
在JS中, 有两种设置定时器的方案
// 延时器:经过xxx时间后, 执行xxx函数
t = setTimeout(函数, 时间)
// 5000毫秒 5秒后打印我爱你
t = setTimeout(function(){
console.log("我爱你")
}, 5000);
clearTimeout(t) // 停止一个定时器
// window.clearTimeout(t)
// 定时器:每隔 xxx时间, 执行一次xxx函数
t = setInterval(函数, 时间)
// 每隔5秒钟, 打印`我爱你`
t = setInterval(function(){
console.log("我爱你")
}, 5000)
window.clearInterval(t) // 停止一个定时器
for(let i = 0; i <= 9999; i++)window.clearInterval(i); // 清理掉所有定时器
// 定时器 关于js逆向,常遇到的:
// 1.心跳检测 一般是用来监测用户 是否掉线了,也可以用来监测用户浏览器环境是否健康
// 这个就是 服务端 主动向 浏览器端 发送请求检测
http是被动响应的 服务器端只能不停的间隔发送检测请求,才能时刻监测客户端某个元素的状态(是否点击 或 扫描二维码)等
js逆向中:心跳检测
就是正常浏览器是 不停的心跳检测 且有数据返回,
js逆向代码时,可能只发一次,服务端就给做反爬 禁IP什么的
// 2.无限debugger
// 无限debugger的核心
setInterval(function(){
debugger; // 设置断点
}, 1000)
网页页面中,会正常显示其他的html代码,但是一旦F12调试,就会一直处理断点中
// 解决原理:
在source源代码中,setInderval 这一行(没进入定时器体内之前),左键点击行号(设置断点)
刷新页面,页面会调试暂停到 设置断点这一行
再在控制台(console)中,将定时器干掉 (重置定时器为普通的空函数 setInterval=function(){}; )
3.2 关于时间
eg:http://www.baidu.com/s?word=jfdsaf&t=1640090719637 参数t就是时间戳
var d = new Date(); // 获取当前系统时间
var d = new Date("2018-12-01 15:32:48"); // YYYY-MM-DD HH:mm:ss得到一个具体时间
// 时间格式化
year = d.getFullYear(); // 年份
month = d.getMonth() + 1; // 月份. 注意月份从0开始
date = d.getDate(); // 日期
hour = d.getHours(); // 小时
minute = d.getMinutes(); // 分钟
seconds = d.getSeconds(); // 秒
format_date = year + "-" + month + "-" + date + " " + hour + ":" + minute + ":" + seconds;
// 时间戳 表示从1970-1-1 00:00:00 到现在一共经过了多少毫秒
var d = new Date();
console.log(d.getTime())
// 注意1:python的时间戳 单位是秒
import time
print( int(time.time() * 1000) ) # 扩大一千倍,再取整
// 注意2: 有些时候,前端实例化日期对象时,会少一个调用括号
var d = new Date; // 坑人写法,也可以
console.log(d.getTime())
3.3 eval函数(必须会)
http://tools.jb51.net/password/evalencode 一个在线JS处理eval的网站. 大多数的eval加密. 都可以搞定了.
// eval:
可以动态的把字符串,当成js代码运行 // 从功能上讲非常简单,和python里面的eval是一样的
s = "console.log('我爱你')";
eval(s);
// 重点:eval加密
拓展使用 --> 前端利用eval的特性来完成反爬操作
// 解决核心:
eval函数,里面传递的应该是 即将要执行的 代码(字符串)
// eg:
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('0.1(\'我爱你\')',62,2,'console|log'.split('|'),0,{}))
// 记住eval()里面是字符串 !!!
这一坨看起来, 肯定很不爽. 怎么变成看着很舒服的样子呢? 想看看这个字符串长什么样?
就把eval()里面的东西拷贝出来
执行一下 // 注意:一般是个自运行函数,直接粘贴会报错 故加个括号 eg: (eval中的代码)
// 或 和下面截图一样 赋值给变量,再打印变量
最终一定会得到一个字符串,要不然eval()执行不了的
3.4 prototype 原型
prototype 原型,作用是 给类增加功能扩展 的一种模式
写个面向对象来看看.
function People(name, age){
this.name = name; // this 相当于python的self
this.age = age;
this.run = function(){
console.log(this.name+"在跑")
}
}
p1 = new People("张三", 18);
p2 = new People("李四", 19);
p1.run();
p2.run();
现在代码写完了. 突然之间, 感觉好像少了个功能. 人不应该就一个功能. 光会吃是不够的. 还得能够ooxx. 怎么办?
直接改代码? 可以,但不够好. 如果这个类不是我写的呢? 随便改别人代码是很不礼貌的,也很容易出错. 怎么办?
可以在我们自己代码中,对某个类型动态增加功能。此时就用到了prototype
function People(name, age){
this.name = name;
this.age = age;
this.run = function(){
console.log(this.name+"在跑")
}
}
// 通过prototype,可以给People 类(构造器) 增加功能 属性或方法
People.prototype.xxoo = function(){
console.log(this.name+"还可以xxoo");
}
p1 = new People("张三", 18);
p2 = new People("李四", 19);
p1.run();
p2.run();
p1.xxoo();
p2.xxoo();
// eg:给日期对象 增加常用的格式化功能
Date.prototype.fromat = function(){
let Y = this.getFullYear();
let M = this.getMonth() + 1;
let d = this.getDate();
let h = this.getHours();
let m = this.getMinutes();
let s = this.getSeconds();
return Y + "-" + M + "-" + d + " " + h + ":" + m + ":" + s;
}
// 使用
let d = new Date();
console.log(d.fromat())
几个重要概念
3.4.1 构造器
构造一个对象的函数. 叫构造器. 是用于 初始化对象
function People(name, age){ // People 就是构造器 constractor
this.name = name; // 带有this,99%都是构造器了,而不是定义函数了
this.age = age;
}
var p1 = new People(); // new 构造器 调用构造器,p1 是返回的对象
var p2 = People(); // 没有new,就是普通函数调用, p2 是函数的返回值
// 调用 对象的隐藏属性 构造器
p.constractor == People; // true
3.4.2 原型对象
每一个js对象中,都有一个隐藏属性__proto__
,指向该对象的 原型对象
在执行该对象的方法或者查找属性时:
首先, 对象自己(构造器中声明的 或 对象自己赋值的 )是否存在该属性或者方法
如果存在, 就执行自己的. 如果自己不存在. 就去找 原型对象
function Friend(){
this.chi = function(){
console.log("我的朋友在吃");
}
};
// 指定Friend 的原型对象 是全部重写 原型对象
Friend.prototype = {
chi: function(){
console.log("我的原型在吃")
}
};
// 或者这种写法 原型对象 增加某个方法或属性
Friend.prototype.chi = function(){
console.log("我的原型在吃")
};
f = new Friend();
f.chi(); // 运行结果: 我的朋友在吃
// 属性查找顺序:
先查找该对象( 构造器中声明的 或 对象自己赋值的 )中 是否有chi这个方法
再找,它的原型对象上( f.__proto__ ) 是否有chi这个方法
// 总结: !!!
Friend // 构造器
f // 对象
f.__proto__ <===> Friend.prototype // 构造器的prototype属性 和 对象的 __proto__,都是指向f对象 的 原型对象
3.4.3 原型链
原型链(prototype chain):是属性查找的方式
当调用一个对象的属性时,如果对象没有该属性,从对象的原型对象上去找该属性,
如果原型上也没有该属性,那就去找原型的原型,直到最后返回null为止,null没有原型。
// 前提:
每个对象身体里. 都隐藏着 __proto__属性 也就是它的 原型对象
同时 原型对象 也是对象, 也就是说 原型对象 也有 __proto__ 属性
类似于.....这样:
f.__proto__.__proto__ ===> Friend.prototype.__proto__ ===> Object.prototype // Object的原型对象
// 再多一层,就到null了
f.__proto__.__proto__.__proto__ ===> Object.prototype.__proto__ ===> null // Object的原型对象 的原型对象 就是null
打印出来的效果是这样的:
// 故:在执行 f.toString() 的时候不会报错. 可以正常运行的原因,就在这里
执行过程:
先找 f对象 中是否有 toString 没有
找它的 原型对象,原型对象 中没有
继续找 原型对象的原型对象
直至找到Object的原型对象为止,如果还没有,就报错了.
f.hahahahahahah() // 报错
// 综上:
原型链是js 方法查找的路径指示标
3.4.4 原型链的延伸使用
用原型链能做什么? 网站反爬(恶心): 有些页面网站,会让你F12时,一直无限debug ---> 反调试
看一段神奇的代码
(function(){debugger})(); // 这样一段代码可以看到. 浏览器进入了debugger断点
// 注意:
在js代码执行时,每一个function的对象,都是通过Function()来创建的
也就是说 函数是Function()的对象,函数体的代码 是通过Function构造器 的字符串参数 传递进行的
// 校验:
function fn(){};
console.log(fn.__proto__.constructor); // fn函数的原型对象 的 构造器是 ƒ Function() { [native code] }
console.log(fn.constructor); // fn函数的构造器是 ƒ Function() { [native code] }
// 故也可以通过Function来构建一个函数
new Function('debugger')(); // 效果一样的 (function(){debugger})();
// 上述的核心就是
普通定义函数:function(){函数体代码} ---> 函数体代码 是js的语句
Function构造器 (声明 Function 对象) 定义的函数:new Function('函数体代码'), ---> 函数体代码 是js的普通字符串
是字符串 就可以做各种的混淆操作 // 恶心之处
OK. 这东西对我们来说有什么用. 上代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="haha.js"></script>
<script>
txsdefwsw();
</script>
</head>
<body>
有内鬼. 终止交易
</body>
</html>
haha.js 中的内容如下:
function txsdefwsw() {
var r = "V", n = "5", e = "8";
function o(r) {
if (!r) return "";
for (var t = "", n = 44106, e = 0; e < r.length; e++) {
var o = r.charCodeAt(e) ^ n;
n = n * e % 256 + 2333, t += String.fromCharCode(o)
}
return t
}
try {
var a = ["r", o("갯"), "g", o("갭"), function (t) {
if (!t) return "";
for (var o = "", a = r + n + e + "7", c = 45860, f = 0; f < t.length; f++) {
var i = t.charCodeAt(f);
c = (c + 1) % a.length, i ^= a.charCodeAt(c), o += String.fromCharCode(i)
}
return o
}("@"), "b", "e", "d"].reverse().join("");
!function c(r) {
(1 !== ("" + r / r).length || 0 === r) && function () {
}.constructor(a)(), c(++r) // 核心:Function('debugger')()
}(0)
} catch (a) {
setTimeout(txsdefwsw, 100);
}
}
结果:页面跑起来没什么问题. 但是会无限debugger;
解决方案:
-
找到断点 行处 . 右键 ----> never pause here (不在这里暂停)
-
写js hook代码
// 解决原理: 既然 你无限debugger的混淆原理,是Function + 字符串的混淆操作 那么 解决原理:在Function的原型对象的 构造器 (因为通过 Function() 声明函数时,会调用该构造器) 做一个拦截处理 如果 Function构造器的 参数是 'debugger',就返回空 不执行构建函数 如果 Function构造器的 参数不是 'debugger',就执行 返回原生的构造器,进行声明函数 // 1.先将 原本的 Function原型对象的构造器,复制出来 var xxxx = Function.prototype.constructor; // 普通函数对象func.constructor === Function.constructor === Function.prototype.constructor // 2.再改造 Function原型对象的构造器 Function.prototype.constructor = function(){ if (arguments[0] === 'debugger'){ return; } else { return xxxx.apply(this, arguments); // 不能用.call 参数数量不固定 } }
更加详细的hook. 在下面.
3.5 window对象
window 窗口对象是一个很神奇的东西,可以理解成javascript的全局,是整个浏览器的全局作用域
如果默认不用任何东西,访问一个标识符,那么默认是在用window对象
注意:
1.是浏览器有这个,node.js没有该对象。
2.在浏览器上 delete window; 是默认删不掉的
故 用js逆向时,需要在node.js上 自定义伪造window对象
但 有些网站 防爬,就会时不时 delete window; 用来区分是程序伪装,还是浏览器。
解决:就是找到 网站源码,删除该js代码(delete window;)
例如:
eval === window.eval // true
setInterval === window.setInterval // true
var a = 10;
a === window.a // true
function fn(){}
fn === window.fn // true
window.mm = "爱你"
console.log(mm); //"爱你"
// window 中有很多功能对象,还可以控制页面跳转
window.location.herf = "新地址" // 当前窗口 跳转到新地址url
综上:全局变量可以用window.xxx来表示
ok. 接下来注意看. 我要搞事情了
(function(){
let chi = function(){
console.log("我是吃")
}
window.chi = chi
})();
chi()
// 换一种写法. 你还认识么?
(function(w){
let chi = function(){
console.log("我是吃")
}
w.chi = chi
})(window);
// 再复杂一点
(function(w){
let tools = {
b64: function(){
console.log("我是计算B64");
return "b64";
},
md5: function(){
console.log("我是计算MD5");
return "MD5"
}
}
w.jiami = {
AES: function(msg){
return tools.b64(),
tools.md5(),
'god like';
},
DES: function(){
console.log("我是DES");
},
RSA: function(){
console.log("我是RSA");
}
}
})(window);
jiami.AES("吃了么");
3.6 call和apply
对于逆向工程师而言,并不需要深入的理解call和apply的本质作用. 只需要知道这玩意执行起来的逻辑顺序是什么即可
作用: 其实也是 为对象 增加额外的方法
1.可以 执行函数
2.可以指定 执行函数 的对象
在运行时,正常的js调用:
function People(name, age){
this.name = name;
this.age = age;
this.chi = function(){
console.log(this.name, "在吃东西")
}
}
p1 = new People("alex", 18);
p2 = new People("wusir", 20);
p1.chi();
p2.chi();
接下来,可以使用call和apply也完成同样的函数调用
function People(name, age){
this.name = name;
this.age = age;
this.chi = function(what_1, what_2){
console.log(this.name, "在吃", what_1, what_2);
}
}
p1 = new People("alex", 18);
p2 = new People("wusir", 20);
p1.chi("馒头", "大饼");
p2.chi("大米饭", "金坷垃");
// 普通函数 eat 属于是window对象
function eat(what_1, what_2){
// this 没有明确的指向,最后会在window中 获取name
console.log(this.name, "在吃", what_1, what_2);
}
// call的语法是: 函数.call(对象, 参数1, 参数2, 参数3....)
// 执行逻辑是: 执行函数. 并把对象传递给函数中的this. 其他参数照常传递给函数
eat.call(p1, "查克拉", "元宇宙");
p2.chi.call(p1, "查克拉", "元宇宙"); // wusir 在吃 查克拉 元宇宙
// p2.chi 相当于一个普通函数,调用的this 是传递的p1对象
apply和他几乎一模一样. 区别是: apply传递参数要求是一个数组
eat.apply(p1, ["苞米茬子", "大饼子"]);
3.7 ES6中的箭头函数
在ES6中简化了函数的声明语法.
// 函数无参数
var fn = function(){};
var fn = () => {};
// 函数1个参数 括号可以省略 !!! 容易岔
var fn = function(name){}
var fn = (name) => {}
var fn = name => {}
// 函数多个参数
var fn = function(name, age){}
var fn = (name, age) => {}
3.8 ES6中的promise(难)
具体执行过程和推理过程. 请看视频. 这里很饶腾.
function send(url){
return new Promise(function(resolve, reject){
console.log("我要发送ajax了", url)
setTimeout(function(){
console.log("我发送ajax回来了")
// 成功了, 要去处理返回值
resolve("数据", url);
}, 3000);
});
}
send("www.baidu.com").then(function(data){
console.log("我要处理数据了啊", data);
return send("www.google.com");
}).then(function(data, url){
console.log("我又来处理数据了", data);
});
3.9 逗号运算符
function s(){
console.log(1), console.log(2), console.log(3); // 从前向后执行 1,2,3
let s = (1, 2, 3); // 整体进行赋值的时候. s 取的是最后一个值 3
console.log(s); // 注意: 这个括号,在作为返回值时,可省略 return 1, 2, 3
var a;
return a=10, // return 返回时,以逗号分割依次执行,同样只取最后一个 和python的差别 !!!
a++,
a+=100,
{name:"alex", "a":a};
}
let r = s();
console.log(r); // {name: 'alex', a: 111}
3.10 三元运算符
// 三元运算符
条件? 值1 :值2 // 条件成立时,返回 ?后面的 反之,返回 :后面的
let a = 10;
let b = 20;
let d = a > b? a :b ;
console.log(d); // 20
// 看一个恶心的: 逗号运算符 + 三元运算符
let a = 10;
let b = 20;
let d = 17;
let c = 5;
let e;
let m;
e = (e = a > 3 ? b : c, m = e < b++ ? c-- : a = 3 > b % d ? 27: 37, m++)
// 解析
e = (e = 20, m = a = 37, m++)
e = m++
console.log(e); // 37
console.log(c); // 5
console.log(m); // 38
3.11 JS Hook
Hook又称钩子,可以在调用系统函数之前,先执行我们的函数. 钩子函数
在逆向时, 常用的主要有: hook eval 、hook Function 、hook JSON.stringify、JSON.parse 、hook cookie、hook window对象
其他参考:https://zhuanlan.zhihu.com/p/584684377
3.11.0 设置 script 断点
注意:想要正常Hook 成功,需要在执行目标函数之前,先把Hook函数植入
在Sources面板 ---> Event Listener Breakpoints( 事件监听断点) ---> 设置 script 断点
目的:是为了让浏览器,在每加载一个JS,就被断下来,以方便进行 Hook
// Hook函数 添加操作
1.设置script断点后,刷新页面,会暂停到第一个JS文件加载前
2.复制你的Hook 函数,到控制台
3.先取消设置script断点,再释放断点 (第一个js script的断点)
4.正常情况下:sources面板会跳转到 Hook函数的 断点处,进行溯源分析
3.11.1 Hook eval
// 先保存系统的eval函数
var eval_ = eval;
// 钩子函数中添加断点,就可以溯源 在Call Stack调用栈中,查看到哪个函数在调用该方法
eval = function(s){
console.log(s);
debugger;
return eval_(s);
};
// 注意:钩子函数替换的eval,调用某些方法和原生就不一样了,有些网站可能会检测这个
// eg: toString方法
eval.toString()
// 'function(s){\n console.log(s);\n debugger;\n return eval_(s);\n}'
// 措施:需要再伪装 补救,换成原生的样子 具体需要补救哪些方法,看实际网站检测哪些
// 修改一下eval.toString
eval.toString = function(){return 'function eval() { [native code] }'}
// 或者如下:
var _old = Function.prototype.toString.call
Function.prototype.toString.call = function (arg) {
if (arg === eval) {
return "function eval() { [native code] }"
}
return _old(arg);
3.11.2 Hook Function
主要为了解决无限debugger
### 1 debugger函数的几种写法
# 1.普通 debugger函数
(function(){debugger})()
# 2.通过函数的构造器 写debugger函数
(function(){}).constructor("debugger")()
# 3.通过 Function 生成函数对象 写debugger函数
new Function("debugger")()
### 2 对应的Hook方法
# 1. 普通debugger函数, 一般是写进定时器里,实现无限debug
直接将定时器置空 # 详见下面hook 定时器
# 2.函数的构造器 的debugger函数
Hook 函数的构造器
var fnc_ = Function.prototype.constructor;
Function.prototype.constructor = function(){
if(arguments[0]==='debugger'){
return function(){}; // 返回空函数对象
} else {
return fnc_.apply(this, arguments);
}
}
# 更通用:
var Function_prototype_constructor_back = Function.prototype.constructor;
Function.prototype.constructor = function() {
if(arguments && typeof arguments[0]==='string'){
//alert("new function: "+ arguments[0]);
if("debugger" === arguments[0]){
//arguments[0]="console.log(\"anti debugger\");";
//arguments[0]=";";
return function(){}; // 返回空函数对象
}
}
return Function_prototype_constructor_back.apply(this,arguments);
}
# 3. Function 生成函数对象 的debugger函数
Hook Function
var Function_ = Function
Function = function() {
if(arguments && typeof arguments[0]==='string'){
if("debugger" === arguments[0]){
return function(){}; // 返回空函数对象
}
}
return Function_.apply(this,arguments);
}
3.11.3 Hook setInterval
// 1.定时器 与业务代码无关时 定时器只是用做无限debugger时
// 直接把该定时器,重置为空函数
var setInterval = function(){}
// 2.定时器 与业务代码有关时 用钩子函数勾出来,重新做判断,处理无限debugger
var setInterval_back = window.setInterval
setInterval = function(a,b){
if(a.toString().indexOf('debugger') != -1){
return null;
}
return setInterval_back(a, b);
}
3.11.4 Hook JSON
// hook JSON字符串的处理 特别常用:网站基本都是传递json数据,且伴随着前端的 加密 解密
// Hook JSON.stringify
var JSON_stringify_back = window.JSON.stringify;
// 在 js的数据格式,转化成json字符串 前 添加断点,溯源查找数据加密的方法
JSON.stringify = function(s){
debugger;
return JSON_stringify_back(s);
};
// Hook JSON.parse
var JSON_parse_back = JSON.parse;
// 在 json字符串 转化成 js的数据格式, 前 添加断点,溯源查找数据解密的方法
JSON.parse = function(s){
debugger;
return JSON_parse_back(s);
};
3.11.5 Hook ajax请求的请求头
// 发送Ajax请求,学了两个框架:1.jQuery 2.axios
// 但本质都是 通过 浏览器原生XMLHttpRequest对象,发送ajax
// hook ajax请求的请求头
// 本质是hook XMLHttpRequest对象.prototype原型的setRequestHeader属性
func_ = window.XMLHttpRequest.prototype.setRequestHeader; // 参数1 请求头的属性,参数2 请求头的值
func_ = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function(name, value){
if(name === '某值'){
debugger
}
return func_.apply(this, arguments)
};
3.11.6 Hook 属性
// Object对象.defineProperty
静态方法,会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象
// eg:Hook document.cookie属性 cookie属性就两个方法:get取值、set设置值
// 先定义一个变量,临时用来接受属性的值
var v;
Object.defineProperty(document, "cookie", {
// 两种形式
set: function(val) {
console.log("有人来存cookie了");
v = val;
debugger;
return val;
},
set(val) {
console.log("有人来存cookie了");
v = val;
debugger;
return val;
},
get() {
console.log("有人提取cookie了");
debugger;
return v;
}
});
剩下的,就不再赘述了.
四. JS和HTML交互
在HTML中,可以直接在标签上给出一些事件的触发
例如:页面上的一个按钮
<input type="button" value="点我就爱你"/>
该标签在页面中会产生一个按钮,但是该按钮无论如何进行点击. 都不会触发任何事件
但此时, 其实触发了. 只是没处理而已. 在点击该按钮的时候. 浏览器其实收集到了点击事件.
可以通过onclick
属性. 来给点击事件添加上具体要做什么
<input type='button' value="点我就爱你" onclick="fn()" />
当发生点击事件时去执行fn(). fn() 是什么? fn就是我们javascript的一个函数.
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
function fn(){
alert("臭不要脸")
}
</script>
</head>
<body>
<input type="button" value="点我就爱你" onclick="fn()">
</body>
</html>
至此, 成功实现了 从HTML中调用JS
那么在HTML中有多少种事件可以触发呢? 非常多....记住几个就好了
// html中的事件
click 点击事件
focus 获取焦点
blur 失去焦点
submit 提交表单
change 更换选项
scroll 滚动条滚动
mouseover 鼠标滑过
mouseout 鼠标滑出
mousemove 鼠标滑动
上述是第一种绑定事件的方案. 可以直接在html标签中,使用onxxx
系列属性来完成事件的绑定
同时js,还提供了以下事件绑定方案:
<input type="button" id="btn" value="别点我了">
<script>
// 注意:必须等到页面加载完毕了. 才可以这样
document.querySelector("#btn").addEventListener("click", function(){
console.log("你点我干什么?? ")
})
</script>
document.querySelector() 给出一个css选择器, 就可以得到一个html页面上标签元素的句柄(控制该标签).
获取句柄的方案有好多. 常见的有:
document.getElementById(); // 根据id的值 获取句柄
document.getElementsByClassName(); // 根据class的值 获取句柄
// <form name='myform'><input type="myusername"/></form>
document.form的name.表单元素的name; // document.myform.myusername;
现在相当于,可以从html转到JS中了,并且在js中可以捕获到html中的内容了
此时 对应的表单验证,也可以完成了
<form action="服务器地址" id="login_form">
<label for="username">用户名:</label><input type="text" name="username" id="username"><span id="username_info"></span><br/>
<label for="password">密码:</label><input type="text" name="password" id="password"><span id="password_info"></span><br/>
<input type="button" id="btn" value="点我登录">
</form>
<script>
// window.onload,等待页面加载完毕,在执行后面函数
window.onload = function(){
// let btnEle = document.getElementById('btn')
// btnEle.onclick = function(){
// 等价于上面
document.getElementById('btn').addEventListener("click", function(){
// 清空提示信息
document.getElementById('username_info').innerText = "";
document.getElementById('password_info').innerText = "";
let username = document.getElementById('username').value; // 获取username标签中的value属性
let password = document.getElementById('password').value; // 获取密码
let flag = true; // 最终是否可以提交表单?
if(!username){
document.getElementById('username_info').innerText = "用户名不能为空";
flag = false;
}
if(!password){
document.getElementById('password_info').innerText = "密码不能为空";
flag = false;
}
if (flag){
document.getElementById('login_form').submit();
}
})
}
</script>