逆向-JavaScript
Javascript基础入门
JavaScript, 是一门能够运行在浏览器上的脚本语言. 简称JS. 首先, Javascript这个名字的由来就很有意思, 不少人认为Javascript和Java貌似很像. 容易想象成Java的脚本. 但其实不然, 两者之间没有任何关系. 纯粹是商业碰瓷. 那么既然JS是可以运行在浏览器上的脚本. 并且, 我们知道本质上, 浏览器是执行HTML程序的. 那么如何在HTML中引入JS呢? 方案一, 直接在<script>
标签中引入编写js代码 方案二, 将js代码写在js文件中, 然后通过script标签的src属性进行引入
两种方式运行出的效果是一致的. 但是需要各位注意一点, HTML程序在执行的时候是从上到下进行渲染的. 那么如果我把脚本放在下面和放在上面是有一些不同的
一. Javascript基本数据类型
JS虽然是一个脚本语言. 麻雀虽小, 五脏俱全. 在js中也是可以像其他编程语言一样. 声明变量, 条件判断, 流程控制等等. 我们先看一下JS中的数据类型 在js中主要有这么几种数据类型(基本)
number 数字, 不论是整数还是小数, 数据类型都是number
string 字符串, 这个没啥可聊的. 就是很单纯的字符串
boolean 布尔值, 只有两个, true和false. 注意不是大写T和F.
object 对象, 这个比较特殊. 你可以理解为所有被new出来的东西都是对象
undefined, 这个表示未定义. 所有没有被定义过的东西默认都是该类型 类似像空一样的东西
在js中声明变量用var来声明 在js中使用// 来表示单行注释. 使用/* */表示多行注释.
var 变量名; // 创建变量, 此时该变量除了被赋值啥也干不了.
var 变量名 = 值; // 创建一个变量, 并且有值.
var 变量名 = 值1, 变量名2 = 值2, 变量名3 = 值3.....; // 一次创建多个变量.并都有值
var 变量名1, 变量名2, 变量名3 = 值3; // 创建多个变量. 并且只有变量3有值
JS中的运算符和Python几乎一致. 但有些特殊的. and, or, not 到了js中注意,换成了&&, ||, !, 其含义和概念是一致的.
var a = 10, b = 20, c = 30 ;
console.log(a > b && b > c); // false
console.log(!(a > b)) // 注意括号
== 和 ===, == 只是判断值是否一致, === 会判断数据类型和数据是否都一致.
var a = "123";
var b = 123;
console,log(a == b); // true
console.log(a === b); // false
数据类型转换:
// string -> number : parseInt(字符串)
var a = "10086";
a = parseInt(a); // 变成整数
console.log(a + 10); // 10096
// number -> string : 数字.toString() 或者 数字 + ""
var a = 100;
var b = a.toString();
var c = a + "";
console.log(b);
console.log(c);
// number -> string: 数字转化成16进制的字符串
var m = 122;
var n = m.toString(16);
console.log(n);
// 进制转换
var a = 10;
// 16进制的数字是多少
var x = a.toString(16); // a
// AB的十进制是多少
var d = parseInt("AB", 16); // 171
关于++
// 在python中是没有++操作的. 但是在js中是有的.
a++; // 翻译一下就是a = a + 1
++a; // 翻译一下就是a = a + 1
a--; // 翻译一下就是a = a - 1
--a; // 翻译一下就是a = a - 1
//困扰无数初学者的疑惑, a++ 和 ++a有什么区别
// 两句话,
// 1. 不论是a++还是++a. 目的都是让a自增1.
// 2.
// a++这个表达式整体运算出来的结果是 a
// ++a这个表达式整体运算出来的结果是 a + 1
/*
理解一下, 这里还好理解
a = 10
b = a++
console.log(b) // ?
console.log(a) // ?
a = 10
b = ++a
console.log(b) // ?
console.log(a) // ?
难度升级, 这里非常绕. 答案就在上面那两句话上.
a = 10
a = a++
console.log(a) // ?
a = 10
a = ++a
console.log(a) // ?
*/
字符串操作:
s.split() 字符串切割
s.substr(start, len) 字符串切割, 从start开始切, 切len个字符
s.substring(start, end) 字符串切割, 从start切割到end
s.length 字符串长度
s.charAt(i) 第i索引位置的字符 s[i]
s.indexOf('xxx') 返回xxx的索引位置, 如果没有xxx. 则返回-1
s.lastIndexOf("xxx") 返回xxx的最后一次出现的索引位置,如果没有xxx. 则返回-1
s.toUpperCase() 转换成大写字母
s.startsWith("xxx") 判断是否以xxx开头
s.charCodeAt(i) 某个位置的字符的ascii
关于null和undefined. 这两个会很容易混. 你可以这样来记. null就是空对象. undefined就是空变量. 两者都可以表示空. 啥也没有. 本质其实是一样的. 都啥也干不了. 两者都可以当做false来看待就好了.
二. JS条件分支
除了HTML以外. 几乎所有的编程语言都有条件判断的功能. 比如, python, 我们用if语句来做条件判断. 到了javascript中也是一样的, 也使用javascript来做条件上的判断.
// 语法
if(条件1){
代码块1
}
// 解读: 当`条件1`成立时, 执行代`码块1`中的内容, 如果`条件1`不成立. 则不执行该`代码块1`中的内容
// 注, 如果代`码块1`中的内容只有一行. 则可以省略外面的大括号(一些逆向工程里会有)
// 语法
if(条件1){
代码块1
} else {
代码块2
}
// 解读: 当`条件1`成立时, 执行`代码块1`中的内容, 如果`条件1`不成立. 则执行`代码块2`中的内容
// 语法
if(条件1){
代码块1
} else if(条件2) {
代码块2
} else if(条件3) {
代码块3
} ... {
代码块n
} else {
代码块else
}
// 解读: 当`条件1`成立时, 执行`代码块1`中的内容, 如果`条件2`不成立. 则执行`代码块2`中的内容...如果都不成立, 最终执行`代码块else`中的内容.
switch语句. 该语句是python中不存在的. 但是在Java和C, 以及JS中依然会有使用
switch(变量){
case 值1:
代码块1
break // 可选
case 值2:
代码块2
break // 可选
case 值3:
代码块3
break // 可选
default: // 可选
default代码块
}
/*
解读:
执行时,
switch会判断变量的值是否是`值1`,
如果是, 则执行代码块1以及代码块1中的break,
如果不是, 则继续判断`值2`...
如果前面的`值`都没有和`变量`相等的.则执行`default代码块`.
注意,
每一个`case`中都可以选择`break`, 也可以不选择`break`, 需要注意的是, 如果不写`break`.
那么就会形成`case穿透`现象.
例, `变量`的值如果和`值1` 相等. 并且case1中没有写`break`,
则在执行的时候. 会执行完`case1`中的代码.
然后会自动穿透到`case2`中去执行里面的代码, 而不经过case2中的数据的验证.
*/
三. JS中的循环语句
在js中有三种循环语句. 首先是while循环. 它的逻辑和咱们python中的while几乎一模一样, 就是符号上有些许的区别.
// 语法
while(条件){
循环体 -> 里面可以有break和continue等关键字
}
/*
判断`条件`是否为真, 如果`真`, 则执行`循环体`.执行完`循环体`, 会再次判断`条件`....
并且在循环中也可以使用`break`和`continue`等关键字来控制循环的走向.
*/
// 语法
do{
循环体
} while(条件);
/*
解读:
先执行`循环体`, 然后判断`条件`是否成立, 如果成立.在来一次.
注意, 由于do..while是先执行的`循环体`. 所以, 不论条件如何, 至少执行一次`循环体`
*/
// 语法: for的第一种语法
for(表达式1; 表达式2; 表达式3){
循环体
}
/*
解读:
for循环和我们python中的循环是完全不一样的. 解读起来会有点儿麻烦.
首先, 在执行的时候, 先执行`表达式1`,
然后, 判断`表达式2`得到的结果是否真, 如果`真`, 则执行循环体,
再然后, 执行`表达式3`,
再然后, 判断`表达式2`执行的结果是否为`真`, 如果`真`, 则执行`循环体`
再然后, 执行`表达式3`
.....
直到, `表达式2`得到的结果是`假`, 则跳出循环
*/
// 看起来很绕. 我们用for循环来跑一个1~99
for(var i = 1; i < 100; i++){
console.log(i);
}
/*
首先, i = 1,
然后, 判断 i < 100 成立
打印i
在然后, i++, i变成2
再然后, 判断 i < 100 还是成立
打印i
再然后, i++, i变成3
再然后, 判断 i< 100 还是成立
打印3....
....
当i = 100了. i < 100不成立. 程序结束
*/
// for循环的固定逻辑也就这样了
for(变量声明; 条件判断; 改变变量){
循环体
}
// for的第二种用法
var a = [11,22,33,44,55,66]
for(let i in a){
console.log(i + "_" + a[i])
}
// 这种写法非常类似python中的for循环. 但是要注意. 这里的`i`拿到的仅仅是 `数组a`的索引信息.
// 如果需要数据 a[i]
四.JS中的数组和对象
在JS中创建数组非常简单. 直接[ ]即可. 也可以用正规军的new Array(). 不过效果都是一样的.
var as = [11,22,33,44,55];
var bs = new Array(11,22,33,44,55);
数组的常用操作
arr.length; // 数组长度
arr.push(data); // 添加数据
arr.pop(); // 删除数据, 从后面删除, 并返回被删除的内容
arr.shift() // 删除数据, 从前面删除, 并返回被删除的内容
// arr中的每一项循环出来. 分别去调用function函数, 会自动的将`数据`传递给函数的第一个参数
arr.forEach(function(e, i){ // 第二个参数是可选的
console.log(i+"__"+e);
});
arr.join("连接符"); // 使用`连接符`将arr中的每一项拼接起来. 和python中的 "".join()雷同
在JS中创建一个对象非常容易. 和python中的字典几乎一样
var p = {
name: "汪峰",
age: 18,
wife: "章子怡",
chi: function(){
console.log("吃饭")
}
};
使用对象
p.name
p.age
p['wife']
p.chi()
p['chi']()
从上述内容中几乎可以看到. JS对象的使用几乎是没有门槛的. 十分灵活
for(var n in p){
if(typeof(p[n]) != 'function'){
console.log(p[n])
}
}
五. JS中的函数(重点)
在JS中声明函数和python差不多. 也要有一个关键字顶在前面. python是def, 到了JS里换成了function, 只不过在JS中没有像python那么死板, 必须def后面必须跟上函数名. 这也为我们未来做逆向提供了第一个超大的伏笔.
// 语法
// 声明函数
function 函数名(形参1, 形参2, 形参3....){
函数体
return 返回值
}
// 调用函数
函数名(实参1, 实参2, 实参3....)
// 除了写法换了一丢丢. 其他的东西和python完全一致,
简单来个案例看看
function an(a, b){
return a + b
}
ret = an(1, 2)
console.log(ret); // 3
很简单不是么? 那为什么我们在网页上看到一些网站的JS是如此的复杂呢? 注意了, JS其实没有一个非常非常严格的语法规则. 只要能够形成 xxx() 并且xxx是一个函数的话就可以执行. 例如:
var an = function(a, b){
return a + b
}
an(1, 2) // an虽然是var声明的, 但是它的指向是一个函数. 那就可以执行
var $ = function(a, b){
}
$(1, 2) // $没有被JS使用. 我就可以用啊. _也OK
// 这个也很过分. 这个东东要拆开来看 第一个括号里面放的就是一个函数啊. 所以依然可以执行.
(function(a, b){
return a + b
})(1, 2)
c = (function(){
var m = {
name:'alex',
age:'18',
xijiao: function(a){
console.log(a+"来帮我洗脚");
}
}
return m
})
// 还有最后一个问题. 未来我们也会遇到的. 就是它这个return
var an = function(){
return console.log("我爱你"), console.log("爱你妹"), "哈哈"
}
// 注意我们发现js会把return后的每一个,都执行一次. 但是最终真正的返回值其实是最后的那个"哈哈"
Javascript进阶
一. 变量提升
看以下代码, 或多或少会有些问题的.
function fn(){
console.log(name);
var name = '大马猴';
}
fn()
发现问题了么. 这么写代码, 在其他语言里. 绝对是不允许的. 但是在js里. 不但允许, 还能执行. 为什么呢? 因为在js执行的时候. 它会首先检测你的代码. 发现在代码中会有name使用. OK. 运行时就会变成这样的逻辑:
function fn(){
var name;
console.log(name);
name = '大马猴';
}
fn()
console.log(a);
看到了么. 实际运行的时候和我们写代码的顺序可能会不一样....这种把变量提前到代码块第一部分运行的逻辑被称为变量提升. 这在其他语言里是绝对没有的. 并且也不是什么好事情. 正常的逻辑不应该是这样的. 那么怎么办? 在新的ES6中. 就明确了, 这样使用变量是不完善的. es6提出. 用let来声明变量. 就不会出现该问题了.
function fn(){
console.log(name); // 直接报错, let变量不可以变量提升.
let name = '大马猴';
}
fn()
结论一, 用let声明变量是新版本javascript提倡的一种声明变量的方案. let还有哪些作用呢?
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 = "王力宏";
console.log(name);
}
fn()
注意, 报错是发生在代码检查阶段. 所以. 上述代码根本就执行不了. 结论二, 在同一个作用域内. let声明的变量只能声明一次. 其他使用上和var没有差别
二. 闭包函数
我们先看一段代码.
let name = "周杰伦"
function chi(){
name = "吃掉"
}
chi();
console.log(name);
发现没有, 在函数内部想要修改外部的变量是十分容易的一件事. 尤其是全局变量. 这是非常危险的. 试想, 我写了一个函数. 要用到name, 结果被别人写的某个函数给修改掉了. 多难受. 接下来. 我们来看一个案例: 我准备两个工具人. 来编写代码. 分别是js01和js02.
// 1号工具人.
var name = "alex"
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) // 一号工具人还以为是alex呢, 但是该变量是不安全的.
}, 5000);
})()
// 二号工具人
(function(){
var name = "周杰伦"
console.log("二号工具人", name);
})()
运行结果 这样虽然解决了变量的冲突问题. 但是...我们想想. 如果在外面需要函数内部的一些东西来帮我进行相关操作怎么办...比如, 一号工具人要提供一个功能(加密). 外界要调用. 怎么办?
// 1号工具人.
let jiami = (function(){
let key = "10086" // 假装我是秘钥
// 我是一个加密函数
let mi = function(data){ // 数据
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+key);
console.log("数据:"+data);
// 返回密文
return "我要返回密文";
}
// 外面需要用到这个功能啊. 你得把这个东东返回啊. 返回加密函数
return mi;
})();
好像有点儿复杂了哈. 别着急. 注意了. 我们如果封装一个加密js包的时候. 好像还得准备出解密的功能. 并且, 不可能一个js包就一个功能吧..那也太痛苦了(起名字). 那怎么办? 我们可以返回一个对象. 对象里面可以存放好多个功能. 而一些不希望外界触碰的功能. 就可以很好的保护起来.
// 1号工具人.
let jiami = (function(){
let key = "10086" // 加装我是秘钥
// 我是一个加密函数
let rsa_jiami = function(data){ // 数据
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+key);
console.log("数据:"+data);
// 返回密文
return "我要返回密文";
}
// 该函数只属于该模块内部. 外界无法访问.
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中, 有两种设置定时器的方案
// 语法规则
t = setTimeout(函数, 时间)
// 经过xxx时间后, 执行xxx函数
// 5秒后打印我爱你
t = setTimeout(function(){
console.log("我爱你")
}, 5);
window.clearTimeout(t) // 停止一个定时器
//语法规则
t = setInterval(函数, 时间)
// 每隔 xxx时间, 执行一次xxx函数
// 每隔5秒钟, 打印`我爱你`
t = setInterval(function(){
console.log("我爱你")
}, 5000)
window.clearInterval(t) // 停止一个定时器
for(let i = 0; i <= 9999; i++)window.clearInterval(i); // 清理掉所有定时器
3.2 关于时间
http://www.baidu.com/s?word=jfdsaf&_t=1640090719637
var d = new Date(); // 获取系统时间
var d = new Date("2018-12-01 15:32:48"); // 得到一个具体时间
// 时间格式化
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;
d.getTime() // 时间戳. 表示从1970-1-1 00:00:00 到现在一共经过了多少毫秒
3.3 eval函数(必须会. 隔壁村老太太都会.)
eval本身在js里面正常情况下使用的并不多. 但是很多网站会利用eval的特性来完成反爬操作. 我们来看看eval是个什么鬼? 从功能上讲, eval非常简单. 它和python里面的eval是一样的. 它可以动态的把字符串当成js代码进行运行.
s = "console.log('我爱你')";
eval(s);
也就是说. eval里面传递的应该是即将要执行的代码(字符串). 那么在页面中如果看到了eval加密该如何是好? 其实只要记住了一个事儿. 它里面不论多复杂. 一定是个字符串. 比如,
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()里面的东西拷贝出来. 执行一下. 最终一定会得到一个字符串. 要不然eval()执行不了的. 对不...于是就有了下面的操作. http://tools.jb51.net/password/evalencode, 在赠送你一个在线JS处理eval的网站. 大多数的eval加密. 都可以搞定了. 常用方法 把eval函数小括号的内容复制出来,到浏览器控制台先输入一对小括号,再粘贴进去(自运行函数)
3.4* prototype是个什么鬼
prototype是js里面给类增加功能扩展的一种模式. 写个面向对象来看看
function People(name, age){
this.name = name;
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();
能看到一些效果了是吧. 也就是说. 可以通过prototype给我们的对象增加一些功能. 接下来. 聊几个重要的概念.
- 构造器构造一个对象的函数. 叫构造器.
function People(){ //这个东西就是构造器 constractor
}
var p = new People(); // 调用构造器
p.constractor == People; // true
- 原型对象每一个js对象中. 都有一个隐藏属性__proto__指向该对象的原型对象. 在执行该对象的方法或者查找属性时. 首先, 对象自己是否存在该属性或者方法. 如果存在, 就执行自己的. 如果自己不存在. 就去找原型对象.
function Friend(){
this.chi = function(){
console.log("我的朋友在吃");
}
}
Friend.prototype.chi = function(){
console.log("我的原型在吃")
}
f = new Friend();
f.chi(); // 此时. 该对象中. 有chi这个方法. 同时, 它的原型对象上, 也有chi这个方法.
// 运行结果:
// 我的朋友在吃
- prototype和__proto__有什么关系?在js中. 构造器的prototype属性和对象的__proto__是一个东西. 都是指向这个原型对象.
f.__proto__ === Friend.prototype // true
- 原型链这个比较绕了. 我们从前面的学习中, 了解到. 每个对象身体里. 都隐藏着__proto__也就是它的原型对象. 那么我们看哈, 原型对象也是对象啊, 那么也就是说. 原型对象也有__proto__属性. 类似于.....这样:
f.__proto__.__proto__
打印出来的效果是这样的: 此时. 又出现一堆看不懂的玩意. 这些玩意是什么? 这些其实是Object的原型.
f.__proto__.__proto__ === Object.prototype //true,对象的__proto__所指向的是 这个类型的prototype
所以, 我们在执行f.toString()的时候不会报错. 反而可以正常运行. 原因就在这里. 执行过程: 先找f对象中是否有toString. 没有, 找它的原型对象.原型对象中没有, 继续找原型对象的原型对象. 直至你找到Object的原型为止. 如果还是没有. 就报错了.
f.hahahahahahah() // 报错.
综上, 原型链是js 方法查找的路径指示标.
- 我们用原型链能做什么?(每日一恶心)
我们来看一段神奇的代码.
(function(){debugger})();
这样一段代码可以看到. 浏览器进入了debugger断点. 那么这段代码的背后是什么呢? 注意. 在js代码执行时. 每一个function的对象都是通过Function()来创建的. 也就是说. 函数是Function()的对象.
function fn(){}
console.log(fn.__proto__.constructor); // ƒ Function() { [native code] }
函数就是Function的对象. 那么. 我们可以通过Function来构建一个函数.
new Function('debugger')();
跑起来的效果一样的. 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)
}(0)
} catch (a) {
setTimeout(txsdefwsw, 100);
}
}
页面跑起来没什么问题. 但是会无限debugger; 解决方案:
- 找到断点出. 右键-> never pause here;
- 写js hook代码;
var xxxx = Function.prototype.constructor;
Function.prototype.constructor = function(code){
console.log("i love you");
if (code != 'debugger'){
return new xxxx(code);
} else {
return;
}
}
应对无限debugger
//两种方式都要在console执行
//---------------构造器方式---------------
// 1. 记录一下以前的Function.prototype.constructor
var xxxx = Function.prototype.constructor;
// 2. 给Function.prototype.constructor设置一个新的功能
Function.prototype.constructor = function(){
// 3. 判断参数是否是debugger
if (arguments[0] === 'debugger'){
return; // 4 如果是debugger. 就不构建函数
} else {
// 5.如果不是debugger. 需要放行.继续执行原来本应该的操作
return xxxx.apply(this, arguments); // 这里不能用call
}
}
//--------------eval方式-----------------
var eval_ = window.eval;
window.eval = function(){
let can = arguments[0];
if(can.indexOf("debugger")){
return;
}else{
eval_.apply(window, arguments);// window.eval
}
}
3.5 神奇的window
window对象是一个很神奇的东西. 你可以把这东西理解成javascript的全局. 如果我们默认不用任何东西访问一个标识符. 那么默认认为是在用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.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("吃了么");
window是整个浏览器的全局作用域.
3.6* call和apply
对于咱们逆向工程师而言. 并不需要深入的理解call和apply的本质作用. 只需要知道这玩意执行起来的逻辑顺序是什么即可 在运行时. 正常的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("大米饭", "金坷垃");
function eat(what_1, what_2){
console.log(this.name, "在吃", what_1, what_2);
}
// call的语法是: 函数.call(对象, 参数1, 参数2, 参数3....)
// 执行逻辑是: 执行函数. 并把对象传递给函数中的this. 其他参数照常传递给函数
eat.call(p1, "查克拉", "元宇宙");
apply和他几乎一模一样. 区别是: apply传递参数要求是一个数组
eat.apply(p1, ["苞米茬子", "大饼子"]);
3.7 ES6中的箭头函数
在ES6中简化了函数的声明语法.
var fn = function(){};
var fn = () => {};
var fn = function(name){}
var fn = name => {}
var fn = (name) => {}
var fn = function(name, age){}
var fn = (name, age) => {}
// 如果只有一个参数。那么, 可以省略括号
var x = a =>{
console.log("===>", a)
}
x(123);
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); // 整体进行赋值的时候. 取的是最后一个值 3
console.log(s);
// 注意. 这个括号可以在返回值时省略
var a;
return a=10,
a++,
a+=100,
{name:"alex", "a":a};
}
let r = s();
console.log(r); // {name: 'alex', a: 111}
3.10 三元运算符
let a = 10;
let b = 20;
let d = a > b? a: b
console.log(d);
看一个恶心的:
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++)
console.log(e); // 37
console.log(c); // 5
console.log(m); // 38
3.11 JS hook
hook又称钩子. 可以在调用系统函数之前, 先执行我们的函数. 例如, hook eval
eval_ = eval; // 先保存系统的eval函数
eval = function(s){
console.log(s);
debugger;
return eval_(s);
}
eval()
eval.toString = function(){return 'function eval() { [native code] }'} // 可能会被检测到, 用这种方案来进行
var old_parse = JSON.parse;
JSON.parse = function(s){
debugger;
return old_parse(s);
}
对Function的hook, 主要为了解决无限debugger
fnc_ = Function.prototype.constructor;
Function.prototype.constructor = function(){
if(arguments[0]==='debugger'){
return;
} else {
return fnc_.apply(this, arguments);
}
}
上面都是hook的系统函数. 但有时, 我们需要hook某个属性. 此时应该怎么办?
var v;
Object.defineProperty(document, "cookie", {
set: function(val) {
console.log("有人来存cookie了");
v = val;
debugger;
return val;
},
get() {
console.log("有人提取cookie了");
debugger;
return v;
}
});
剩下的咱就不再赘述了. 在逆向时, 常用的主要有: hook eval 、hook Function 、hook JSON.stringify、JSON.parse 、hook cookie、hook window对象
四. JS和HTML交互(选修)
在HTML中可以直接在标签上给出一些事件的触发. 例如, 页面上的一个按钮 <input type="button" value="点我就爱你"/>
我们能够知道此时在页面中会产生一个按钮. 但是该按钮无论如何进行点击. 都不会触发任何事件. 但, 此时我要告诉你, 人家其实触发了. 只是你没处理而已. 在我们点击该按钮的时候. 浏览器其实收集到了点击事件. 但是由于我们没有给出任何发生了点击事件应该做什么的事情. 所以也就没有了反应. 我们可以通过onclick属性. 来给点击事件添加上具体要做什么 <input type='button' value="点我就爱你" onclick="fn()" />
看到了吧. 多了个onclick, 其含义是, 当发生点击事件时. 去执行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 = 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>
发现了么. 不经意间, 我们通过js可以改变html中的内容了.
五. jsonp
为了解决浏览器跨域问题. jquery提供了jsonp请求. 在网页端如果见到了服务器返回的数据是: xxxxxxxxxxdjsfkldasjfkldasjklfjadsklfjasdlkj({json数据}) 在Preview里面可以像看到json一样去调试 这就是jsonp。 这东西依然是ajax. jsonp的逻辑是. 在发送请求的时候. 带上一个callback字符串. 该字符串自动发送给服务器. 服务器返回数据的时候. 会带上该callback字符串. 我们在抓包中看到的就是这样的效果: 在Python中. 接下来, 我们还原一下该效果. 首先, 在flask中. 必须接收到前端返回的callback, 然后在返回数据的时候. 需要用前端返回的callback字符串. 将数据包裹
@app.route("/process_jsonp", methods=["GET"])
def process_jsonp():
# 获取回调字符串
cb = request.args.get("cb")
print(cb)
data = {
"name": "alex",
"age": 18
}
import json
# 用回调字符串将真实要返回的数据包裹起来
# 如果不包裹起來。前端ajax中的success将无法获取到数据
return cb + "("+json.dumps(data)+")"
在发送ajax的时候. 需要指定dataType为jsonp, 以及自由配置回调函数的参数名
$(function(){
$.ajax({
url: "/process_jsonp",
method:"get",
// 典型, 京东.
dataType: "jsonp", // 它的执行逻辑是. 请求服务上的一个js. 然后会自动执行该js.将js函数内的东西. 丢给success
jsonp:"cb", // 传递给服务器的时候. 自动带上cb=xxxxxx 服务器端接收cb即可
success: function(data){ // 此时data可以直接收取到数据
console.log(data);
}
});
});
抓包效果: 服务器处理cb时的效果:
抓包中. 看到的服务器返回的数据
success中接收到的数据效果
我们以后见到这种网站. 如何处理? 首先, 固定好callback的值. 如上述案例. 我们就可以直接给出一个固定的cb值. 如果原网站就是固定的值. 此步骤可以忽略 http://127.0.0.1:5000/process_jsonp?cb=haha&_=1654767783595 然后, 得到返回值后. 用正则. 或者字符串操作. 即可处理.
import json
s = 'haha({"name": "alex", "age": 18})' # 得到这样一个字符串. 处理成json很容易的
s = s.strip("haha(").strip(")")
print(s)
dic = json.loads(s)
print(dic)
# 现在请思考. 为什么要让你把callback的值固定?
jsonp万能处理
# jsonp万能处理方式.
jsonp_re = re.compile(r"\((?P<code>.*)\)", re.S)
code = jsonp_re.search(resp.text).group("code")
六. axios
由于jquery有严重的地狱回调逻辑. 再加上jquery的性能逐年跟不上市场节奏. 很多前端工程师采用axios来发送ajax. 相比jquery. axios更加灵活. 且容易使用. 更加美丽的是. 这玩意是用promise搞的. 所以更加贴合大前端的项目需求. 来吧. 上手试试吧
<script src="/static/axios.min.js"></script>
<script>
window.onload = function(){
axios.post("/movies", {"page": 10086}).then(function(resp){
console.log(resp.data);
})
}
</script>
看到没. 这玩意比jquery简单n多倍. 而且, axios为了更加适应大前端. 它默认发送和接收的数据就是json. 所以, 我们在浏览器抓包时. 直接就是request payload. 这对于前端工程师而言. 爽爆了.
七. axios拦截器
在前端, 我们能看到有些网站会对每次请求都添加加密信息. 或者每次返回数据的时候, 都有解密逻辑. 那此时. 你思考. 不可能每次请求都要程序员去手动写加密逻辑. 例如:
window.onload = function(){
// 加密数据
axios.post("/movies", {"page": 10086}).then(function(resp){
明文 = 解密(resp.data);
console.log(明文);
})
// 加密数据
axios.post("/movies", {"page": 10086}).then(function(resp){
明文 = 解密(resp.data);
console.log(明文);
})
}
这样很麻烦. 也很蛋疼. axios想到过类似的问题. 它提供了拦截器. 一次性处理好这种问题
axios.interceptors.request.use(function(config){ // 拦截所有请求
console.log("我是拦截器. 我可以对数据进行加密");
console.log(config)
return config;
}, function(error){
return Promise.reject(error);
});
axios.interceptors.response.use(function(response){ // 拦截所有响应
console.log("我是响应回来之后拦截器. 我可以对数据进行解密")
return response.data;
}, function(error){
return Promise.reject(error);
});
这样. 对于业务层的代码而言就简单很多了
window.onload = function(){
// 加密的逻辑拦截器帮我完成了
axios.post("/movies", {"page": 10086}).then(function(data){
// 解密的逻辑拦截器帮我完成了
console.log(data);
})
// 加密的逻辑拦截器帮我完成了
axios.post("/movies", {"page": 10086}).then(function(data){
// 解密的逻辑拦截器帮我完成了
console.log(data);
})
}