关于javaScript变量 作用域链 this指针的详解

Undefined

代码:

var a = 1;

function hehe()

{

         window.alert(a);

         var a = 2;

         window.alert(a);

}

hehe();

第一个alert:

图片失效!!!

第二个alert:

图片失效!!!

原理我描述如下:

按照javascript作用域链的原理,当一个变量在当前作用域下找不到该变量的定义,会沿着作用域链往上找直到在全局作用域里查找。

按上面的代码所示,虽然函数内部重新定义了变量的值,但在定义之前使用了该变量;按照作用域链的原理会在全局作用域里找到变量定义,而实际情况却是变量未定义,这到底是怎么回事呢?

下面开始本文的主要内容,我会从基础知识一步步讲起。

Javascript的变量

变量的两种类型:基本类型和引用类型。基本类型是指:Undefined、Null、Boolean、Number和String,引用类型是指多个基本类型构成的对象。

下面我们来看看下面的代码:

var str = "sharpxiajun";
str.attr01 = "hello world";
console.log(str);//  运行结果:sharpxiajun
console.log(str.attr01);// 运行结果:undefined

运行之,发现基本类型无法添加属性,当然方法也同样不可以,例如下面的代码:

str.ftn = function(){
    console.log("str ftn");
}
str.ftn();

运行之,结果如下图所示:

img

当我们使用引用类型时候,结果就和上面完全不同了,大家请看下面的代码:

var obj1 = new Object();
obj1.name = "obj1 name";
console.log(obj1.name);// 运行结果:obj1 name

Javascript里的基本类型是存放在栈区(内存里的栈内存),存储结构如下图所示:

img

javascript里引用类型的存储需要内存的栈区和堆区(内存里的堆内存)共同完成,如下图所示:

img

场景一:如下代码所示:

var qqq;
console.log(qqq);// 运行结果:undefined

上面的代码就是命名了变量但未初始化,此时在内存里只有栈区的变量标示符而没有变量值,更没有堆区存储的对象。

场景二:如下代码所示:

var qqq;
console.log(qqq);// 运行结果:undefined
console.log(xxx);

img

提示变量未定义!在任何语言里变量未定义就使用都是违法的,但是我们javascript变量未定义也可以使用,怎么我的例子里却不能使用了?

那么我们看看下面的代码:

xxx = "outer xxx";
console.log(xxx);// 运行结果:outer xxx
function testFtn(){
     sss = "inner sss";
     console.log(sss);// 运行结果:outer sss
}
testFtn();
console.log(sss);//运行结果:outer sss
console.log(window.sss);//运行结果:outer sss

我们看看window对象的结构,如下图所示:

img

由这两个场景我们可以知道:

***javascript**变量如果没有被var定义也没有赋值操作才会**报出“xxx is not defined”,后续的代码将不能正常运行;而只有赋值操作的变量,不管在哪个作用域里赋值,都是全局变量即window对象*

下面我修改一下代码,如下所示:

//var a = 1;
function hehe()
{
     console.log(a);
     var a = 2;
     console.log(a);
}
hehe();

结果如下图所示:

img

我再改下代码:

//var a = 1;
function hehe()
{
    console.log(a);
    // var a = 2;
    console.log(a);
}
hehe();

运行之,结果如下所示:

img

对比二者代码,我们发现问题的关键是var a=2所引起的!

javascript变量是松散类型即定义时不需要指定变量类型,当变量值确定后才有类型,但没有使用var的变量必须有赋值。

**javascript代码运行前还有一个*预加载*,构造运行环境例如全局环境、函数运行环境和作用域链。环境和作用域的构造就是指定好变量属于哪个作用域,变量的定义是在预加载时完成而非在运行时。*预加载时会扫描所有代码但不运行,当扫描到被赋值操作的变量没有var定义,那么该变量就会被赋予全局变量即window对象。*

重新分析引子里的代码:在函数的局部作用域内变量a被重新定义了,在预加载时a的作用域也就不再属于全局变量而是函数作用域;赋值操作是在运行期执行的,第一次使用a变量时,a变量在局部作用域内还没被赋值,只有栈区的标示符,结果就undefined了。

*javascript两个特别的类型:undefined和null。变量的值为undefined,说明只有栈区的标示符;如果对变量进行赋值基本类型,那么栈区就有值了;如果***赋值引用类型******那么堆区会有一个对象,而栈区的值则是堆区对象的地址。变量的值是null,说明这个变量是个空对象,栈区的标示符和值都有值,堆区也有个空对象,这么说来null其实比undefined更耗内存了!

那么我们看看下面的代码:

var ooo = null;
console.log(ooo);// 运行结果:null
console.log(ooo == undefined);// 运行结果:tru
console.log(ooo == null);// 运行结果:true
 console.log(ooo === undefined);// 运行结果:false
console.log(ooo === null);// 运行结果:true

运行之,null可以和undefined相等;但使用更加精确的三等号“===”,发现还是有点不同,其实javascript里null是undefined的父类,要让一个变量是null必须使用等号“=”进行赋值!

javascript开发规范要求变量定义时马上赋值,好处就是程序很难因为变量未定义报错从而终止程序的运行,而且变量定义最好放在变量作用域的最前端。

下面我们再看一段代码:

var str;
    if (undefined != str && null != str && "" != str){
        console.log("true");
    }else{
        console.log("false");
    }
    if (undefined != str && "" != str){
        console.log("true");
    }else{
        console.log("false");
    }
    if (null != str && "" != str){
        console.log("true");
    }else{
        console.log("false");
    }
    if (!!str){
        console.log("true");
    }else{
        console.log("false");
    }
    str = "";
    if (!!str){
        console.log("true");
    }else{
        console.log("false"){
    }

运行之,结果都是打印出false。

使用双等号“==”时,undefined和null是一回事,所以第一个if语句的写法完全多余,而第二种和第三种写法是等价,究其本质前三种写法本质都是一致的;第四种写法更加完美,javascript里如果if语句的条件是undefined和null,if判断是false,所以判断代码是否为未定义和null时最好使用!运算符。

代码四里字符串被赋值空字符串时,if的判断也是false。javascript里有五种基本类型,undefined、null、boolean、Number和string,现在我们发现除了Number都可以使用!来判断if的ture和false,那么基本类型Number呢?

var num = 0;
if (!!num){
        console.log("true");
}else{
        console.log("false");
}

运行之,结果是false;如果我们把num改为负数或正数,那么运行之的结果就是true了。

所以变量初始化值如果基本类型是string就赋值空字符串,如果是number就赋值0,***if语句就可以判断变量是否初始化过***!

但是当变量是对象时候,结果却不一样了,如下代码:

var obj = {};
if (!!obj){
        console.log("true");
}else{
        console.log("false");
}

运行之,代码是true。

所以引用类型变量赋值null,if语句就可以判断变量是否初始化过!

场景三:复制变量的值和函数传递参数

首先看看这个场景的代码:

var s1 = "sharpxiajun";
var s2 = s1;
console.log(s1); 运行结果:sharpxiajun
console.log(s2); 运行结果:sharpxiajun
s2 = "xtq";
console.log(s1); 运行结果:sharpxiajun
console.log(s2); 运行结果:xtq

上面是基本类型变量的复制,我们再看看下面的代码:

var obj1 = new Object();
obj1.name = "obj1 name";
console.log(obj1.name);// 运行结果:obj1 name
var obj2 = obj1;
console.log(obj2.name);// 运行结果:obj1 name
obj1.name = "sharpxiajun";
console.log(obj2.name);// 运行结果:sharpxiajun

当复制的是对象,obj1和obj2两个对象被关联起来了,obj1的属性改变时,obj2的属性也改变。

我们看看下面的代码:

function testFtn(sNm,pObj){
        console.log(sNm);// 运行结果:new Name
        console.log(pObj.oName);// 运行结果:new obj
        sNm = "change name";
        pObj.oName = "change obj";
}
var sNm = "new Name";
var pObj = {oName:"new obj"};
testFtn(sNm,pObj);
console.log(sNm);// 运行结果:new Name
console.log(pObj.oName);// 运行结果:change obj

这个结果和变量复制的结果是一致的。

函数传参的本质就是外部的变量复制到函数参数的变量里,是按值传递的

如下两张图所示:

img

img

在javascript里变量的存储有三个位置:

位置一:栈区变量标示符;

位置二:栈区变量值(栈区对象地址);

位置三:堆区对象。

**javascript的变量复制(函数传参也是)本质是传栈区变量值。基本类型的值在**栈区变量值里,复制时**两个变量是独立的;复制引用类型时,*栈区变量值*是堆区对象的地址,因此其中一个改变,另一个也改变。

原理讲完了,下面我列举一个拔高的例子,代码如下:

var ftn1 = function(){
        console.log("test:ftn1");
    };
var ftn2 = function(){
        console.log("test:ftn2");
};
function ftn(f){
       f();
       f = ftn2;
}
ftn(ftn1);// 运行结果:test:ftn1
console.log("====================华丽的分割线======================");
ftn1();// 运行结果:test:ftn1

这个代码以前是这么分析的:f是函数的参数,属于函数的局部作用域,更改f的值,是没法改变ftn1的值,因为到了外部作用域f就失效了。但是这种解释很难说明我上文里给出的函数传参的实例。其实这个问题应该这么分析:在javascript里函数也是对象,局部作用域里f = ftn2操作是将f在栈区变量地址改为ftn2的地址,对ftn1和ftn2没有影响。

***记住:javascript变量复制和函数传参是在传递栈区变量值,*当栈区变量值为undefined、null、“”(空字符串)、0、false时,if判断是为false,我们可以通过!运算符计算。

当我们的代码如下:

var obj = {};
    if (!!obj){
        console.log("true");
    }else{
        console.log("false");
}

结果则是true,其中var obj = {}相当于var obj = new Object()。虽然对象里没什么内容,但是在堆区里对象的内存已经分配,栈区变量值已经是内存地址了,所以if判断是true。

3) 作用域链相关的问题

了解作用域链前先了解作用域:

作用域在许多程序设计语言中非常重要。 通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。 作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。

在java里通过{}来设置作用域,在{}里面的变量会得到保护,这种保护就是不让{}里的变量被外部变量混淆。那么{}的方式适合于javascript吗?

我们看看下面的例子:

var s1 = "sharpxiajun";
    function ftn(){
        var s2 = "xtq";
        console.log(this);// 运行结果: windows
        console.log("s1:" + this.s1 + ";s2:" + this.s2);//运行结果:s1:sharpxiajun;s2:undefined
        console.log("s1:" + this.s1 + ";s2:" + s2);// 运行结果:s1:sharpxiajun;s2:xtq
    }
    ftn();

javascript最大的作用域是window,加载时自动构造。上面代码里的大括号是函数定义,函数作用域内定义的s2变量不能被window对象访问的。可以说s2变量是被{}保护起来了,它的生命周期和这个函数的生命周期有关。

但是在javascript语言里还有非函数的{},我们再看看下面的例子:

if (true){
        var a = "aaaa";
    }
    console.log(a);// 运行结果:aaaa

我们发现javascript里{}有时是起不到定义作用域的功能。这也说明javascript里的作用域定义是和其他语言例如java不同的。

javascript作用域execution context,翻译成执行上下文或执行环境。我们来想想javascript里哪些情况是执行的:

情况一:页面加载时script标签下的代码会顺序执行,而这些能被执行的代码都是属于window的变量或函数;

情况二:函数的名字后面加上小括号(),例如ftn(),执行的是函数。

如此说来,javascript里的执行环境有两类一类是全局执行环境,即window代表的全局环境;一类是函数代表的函数执行环境,这也就是局部作用域。

执行环境有个对象叫做variable object,翻译为变量对象或上下文变量,存储其执行环境里所有的变量和函数,全局执行环境的上下文变量就是window。

javascript执行环境栈execution context stack:每个要被执行的函数会先把函数的执行环境压入到执行环境栈里,执行完后被弹出,控制权交给全局环境;如果函数后面还有代码,代码接着执行;如果函数里嵌套了函数,那么嵌套函数执行完毕后,执行环境栈的控制权就交由了外部函数,然后依次类推,最后就是全局执行环境了。

函数的执行环境被压入到执行环境栈里后,函数执行的第一步不是函数里的第一行代码而是在上下文变量里构造一个作用域链scope chain,保证执行环境里有权访问的变量和函数是有序的。

我们看看下面的代码:

var b1 = "b1";
    function ftn1(){
        var b2 = "b2";
        var b1 = "bbb";
        function ftn2(){
            var b3 = "b3";
            b2 = b1;
            b1 = b3;
            console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 运行结果:b1:b3;b2:bbb;b3:b3
        }
        ftn2();
    }
    ftn1();
console.log(b1);// 运行结果:b1

ftn2函数可以访问变量b1,b2,这个体现了有权访问;当ftn1作用域里改变了b1的值并且把b1变量重新定义为ftn1的局部变量,那么ftn2访问到的b1就是ftn1的,ftn2访问到b1后就不会在全局作用域里查找b1了,这个体现了有序性。

**总结:**作用域指的是执行环境execution context,通过上下文变量variable object体现。**当函数的执行环境压入到了执行环境栈**execution context stack**时上下文变量会构造一个对象作用域链scope chain保证执行环境里有权访问的变量和函数是有序的。作用域链只能向上访问**变量**,直到window,*不允许*向下访问。

4) this、new、apply和call详解

*this***对象****:上下文变量构建作用域链时还会构造一个this对象,*是当前执行环境外部上下文变量的一份拷贝,*不包含作用域链变量。

例如代码:

var b1 = "b1";
    function ftn1(){
        console.log(this);// 运行结果: window
        var b2 = "b2";
        var b1 = "bbb";
        function ftn2(){
            console.log(this);// 运行结果: window
            var b3 = "b3";
            b2 = b1;
            b1 = b3;
            console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 运行结果:b1:b3;b2:bbb;b3:b3
        }
        ftn2();
    }
    ftn1();

函数ftn1和ftn2里的this指针都是指向window,这是为什么了?因为通过function xxx(){}形式定义函数,这个函数不管在哪定义都属于window。

但是我们知道很多this指针都不是指向window,例如下面的代码:

var obj = {
    name:"sharpxiajun",
    ftn:function(){
        console.log(this);// 运行结果: Object { name="sharpxiajun", ftn=function()}
        console.log(this.name);//运行结果: sharpxiajun
    }
}
obj.ftn();// :

这里this指针指向了Object,我前文不是说javascript里作用域只有两种:一个是全局的一个是函数,为什么这里Object也是可以制造出作用域?

那我们看看下面的代码:

var obj1 = new Object();
obj1.name = "xtq";
obj1.ftn = function(){
    console.log(this);// 运行结果: Object { name="xtq", ftn=function()}
    console.log(this.name);//运行结果: xtq
}
obj1.ftn();

这两种写法是等价的,第一种叫做字面量定义,而第二种是标准写法。Object对象的本质也是个function,调用对象里的函数时,函数的外部执行环境就是obj1本身,this指针也是指向了obj1。

下面我们看看在java语言里是如何使用this指针的,代码如下:

public class Person {
    private String name;
    private String sex;
    private int age;
    private String job;
    public Person(String name, String sex, int age, String job) {
        super();
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
    }
    private void showPerson(){
        System.out.println("姓名:" + this.name);
        System.out.println("性别:" + this.sex);
        System.out.println("年龄:" + this.age);
        System.out.println("工作:" + this.job);
    }
    public void printInfo(){
        this.showPerson();
    }
    public static void main(String[] args) {
        Person person = new Person("马云", "男", 46, "董事长");
        person.printInfo();
    }
}
//姓名:马云
//性别:男
//年龄:46
//工作:董事长

上面的代码执行后没有任何问题,下面我修改下这个代码,加一个静态的方法,静态方法里使用this指针调用类里的属性,如下图所示:

img

我们发现IDE会报出语法错误“Cannot use this in a static context”,this指针在java语言里是不能在静态方法里使用的。

在面向对象编程里有两个重要的概念:一个是类,一个是实例化的对象。类是一个抽象的概念,类就像一个模具,而实例化对象就是通过这个模具制造出来的产品。

有上面代码我们可以看到,this指针在java语言里只能在实例化对象里使用,this指针等于这个被实例化好的对象,而this后面加上点操作符后面的东西就是this所拥有的东西,例如:姓名,工作,手,脚等等。

javascript的this指针也只能在实例化对象里使用,但是javascript的this指针却比java难以理解的多,原因得有三:

原因一:javascript是一个函数编程语言,也是面向对象的语言;javascript的函数是高阶函数,可以作为对象传递,还可以作为构造函数,创建实例化对象,结果导致方法执行时候this指针的指向会不断发生变化。

原因二:由上面java的例子我们看到,this指针只有在使用new操作符后才会生效;但是javascript里的this在没有进行new操作也会生效,这时候this往往会指向全局对象window。

原因三:javascript里call和apply操作符可以改变this指针。

我们先看看下面的代码:

<script type="text/javascript">
    this.a = "aaa";
    console.log(a);//aaa
    console.log(this.a);//aaa
    console.log(window.a);//aaa
    console.log(this);// window
    console.log(window);// window
    console.log(this == window);// true
    console.log(this === window);// true
</script>

script标签里直接使用this指针就指向window,window在页面加载时由javascript引擎完成。程序员无法通过编程语言来控制这个实例化过程,所以开发时就没有构建这个this指针的感觉,常常会忽视它。

还和function的使用有关,我们看看下面的代码:

<script type="text/javascript">
    function ftn01(){
       console.log("I am ftn01!");
    }
    var ftn02 = function(){
        console.log("I am ftn02!");
    }
</script>

上面是两种定义函数的方式,第一种称作声明函数,第二种称作函数表达式,这两种方式的区别常常会让我们混淆this指针的使用。

我们再看看下面的代码:

<script type="text/javascript">
    console.log(ftn01);//ftn01()  注意:在firebug下这个打印结果是可以点击,点击后会显示函数的定义
    console.log(ftn02);// undefined
    function ftn01(){
       console.log("I am ftn01!");
    }
    var ftn02 = function(){
        console.log("I am ftn02!");
    }
</script>

这又是一段没有按顺序执行的代码,ftn02打印结果是undefined,有栈区变量标识符,没有栈区变量值,堆区没有对象,这是javascript引擎在预处理时扫描变量定义所致。ftn01打印出完整的函数定义,这只能说明一个问题:

***声明函数会在预处理时把函数定义和赋值操作都完成,且声明函数都是window对象的属性*

关于函数表达式的写法还有秘密可以探寻,我们看下面的代码:

<script type="text/javascript">
    function ftn03(){
        var ftn04 = function(){
            console.log(this);// window
        };
        ftn04();
    }
    ftn03();
</script>

ftn04虽然在ftn03作用域下,但是它里面的this指针指向window。说明函数表达式在函数内部写时,this指针指向window。

原因是javascript里任何匿名函数都是属于window,在全局作用域构造时完成定义和赋值。匿名函数是没有名字的变量,定义时会返回内存地址;如果有个变量接收了这个地址,那么匿名函数就能使用了;匿名函数是在全局执行环境构造时定义和赋值,所以匿名函数内的this指向window对象。

这下子坏了,this都指向window,那我们到底怎么才能改变它了?

在本文开头我说出了this的秘密,this都是指向实例化对象,前面讲到那么多情况this都指向window,就是因为这些时候只做了一次实例化操作,实例化window对象,所以this都是指向window。我们要把this从window变成别的对象,就得要让function被实例化,那如何让javascript的function实例化呢?

答案就是使用new操作符。我们看看下面的代码:

<script type="text/javascript">
    var obj = {
        name:"sharpxiajun",
        job:"Software",
        show:function(){
            console.log("Name:" + this.name + ";Job:" + this.job);
            console.log(this);// Object { name="sharpxiajun", job="Software", show=function()}
        }
    };
    var otherObj = new Object();
    otherObj.name = "xtq";
    otherObj.job = "good";
    otherObj.show = function(){
        console.log("Name:" + this.name + ";Job:" + this.job);
        console.log(this);// Object { name="xtq", job="good", show=function()}
    };
    obj.show();//Name:sharpxiajun;Job:Software
    otherObj.show();//Name:xtq;Job:good
</script>

这是我上篇讲到的关于this指向Object的实例。Javascript里通过字面量方式定义对象的方式是new Object的简写,二者是等价的,本质也是new操作符。

下面我使用javascript来重写本篇开头用java定义的类,代码如下:

<script type="text/javascript">
    function Person(name,sex,age,job){
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
        this.showPerson = function(){
            console.log("姓名:" + this.name);
            console.log("性别:" + this.sex);
            console.log("年龄:" + this.age);
            console.log("工作:" + this.job);
            console.log(this);// Person { name="马云", sex="男", age=46, 更多...}
        }
    }
    var person = new Person("马云", "男", 46, "董事长");
    person.showPerson();
</script>

function Person相当于在定义一个类,javascript的function既是函数又是对象;javascript的构造函数可以理解为类和构造函数合二为一,当然javascript语言规范里是没有类的概念。

new操作符会让构造函数产生如下变化:

1. 创建一个新对象;

2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);

3. 执行构造函数中的代码(为这个新对象添加属性);

4. 返回新对象

第四点要着重讲下,记住构造函数被new操作,要让new正常作用最好不要在构造函数里写return,没有return的构造函数都是按上面四点执行,有了return情况就复杂了,这个知识我会在讲prototype时候讲到。

Javascript改变this指针还有call和apply,它们的作用相同,只是参数不同。call和apply的第一个参数一样,apply第二个参数是数组,call从第二个参数开始后面有许多参数。

我们看看下面的码:

<script type="text/javascript">
    var name = "sharpxiajun";
    function ftn(name){
        console.log(name);
        console.log(this.name);
        console.log(this);
    }
    ftn("101");
    var obj = {
      name:"xtq"
    };
    ftn.call(obj,"102");
    /*
    * 结果如下所示:
    *101
     T002.html (第 73 行)
     sharpxiajun
     T002.html (第 74 行)
     Window T002.html
     T002.html (第 75 行)
     T002.html (第 73 行)
     xtq
     T002.html (第 74 行)
     Object { name="xtq"}
    * */
</script>

其实理清上面情况也是有迹可循的,就以定义对象里的方法有传入函数参数为例:

情形一:传入的参数是函数的别名,那么函数的this就是指向window;

情形二:传入的参数是被new过的构造函数,那么this就是指向实例化的对象本身;

情形三:如果想把被传入的函数对象里this的指针指向外部字面量定义的对象,那么我们就是用apply和call

我们可以通过代码看出我的结论,代码如下:

<script type="text/javascript">
var name = "I am window";
var obj = {
    name:"sharpxiajun",
    job:"Software",
    ftn01:function(obj){
        obj.show();
    },
    ftn02:function(ftn){
        ftn();
    },
    ftn03:function(ftn){
        ftn.call(this)
    }
};
function Person(name){
    this.name = name;
    this.show = function(){
        console.log("姓名:" + this.name);
        console.log(this);
    }
}
var p = new Person("Person");
obj.ftn01(p);
obj.ftn02(function(){
   console.log(this.name);
   console.log(this);
});
obj.ftn03(function(){
    console.log(this.name);
    console.log(this);
});
</script>

JS逆向的简单分析

基于requests\hashlib模块实现md5加密方式的有道翻译接口破解
第一步打开控制台查询任意一个单词进行抓包,获取请求方式和查看所需的查询参数(salt,ts,sign)
第二部,根据所需查询参数在控制台中搜索参数所在的 js文件的位置,打断点调试,查看各个参数在js文件中对应函数的实现过程;经调试发现ts是一个时间戳,salt是0-9和时间戳拼接成的一个字符串,sign是所查询单词和salt以及两个固定字符串的一个拼接.
第三步,用python实现Form Data中查询参数的动态生成.
第四步,发请求,传入第三步实现的参数,获取相应结果

阅读更多

基于butterfly标签页头部透明

前言

这玩意只是适用于butterfly,具体效果看本文头图,没错没了。为什么我要写这玩意呢?因为群里某个叫天的人一直在那bb说要改Butterfly的头图,他一个劲的问,我一个劲的回答,然后我发现,朽木不可雕也!废话不多说,直接进入教程。

阅读更多

JS逆向反调试和反反调试

写在最前面

本文转载于CSDN大佬: @花爷
已获得其本人允许
根据《中华人民共和国著作权法》规定

1、已在报刊上刊登或者网络上传播的作品,除著作权人声明或者上载该作品的网络服务提供者受著作权人的委托声明不得转载、摘编的以外,网站予以转载、摘编,并按有关规定支付报酬、注明出处的,不构成侵权。

2、为个人学习、研究或者欣赏,使用他人已经发表的作品而转载的不属于侵权,可以不经著作权人许可,不向其支付报酬,但应当指明作者姓名、作品名称,并且不得侵犯著作权人依照本法享有的其他权利。

切入JS逆向反调试

现在调试JS各种反调,既然有反调,那我们就肯定有过这个反调试的方法。这里给大家推荐一个JS逆向练习平台。

JS逆向练习平台

1.我们这里讲的是第5题,它这里的反调试是不让你打开开发者人员工具。打开开发者人员工具,就自动返回主页面。

2.思路:a:它不让我打开开发者人员工具,肯定是在打开开发者人员工具之后它检测了什么玩意

b:.那他就是调用了浏览器内置的东西,我这个就解决方法是下页面事件监听断点,如图:

这里的意思:
事件侦听器断点 event listener breakpoints
事件突变 Dom mutation
下面还有一些我就不翻译了,有兴趣可以去看看,翻译一下,小弟英语不好。
下这个断点之后,跳转完页面,加载ok之后他就会断下来,如图,我们就可以得到这道题的答案了。

答案大佬打了一下马赛克,这边大佬不公布答案,大家可以尝试一下。

切入JS反反调试

你千万别跟任何人谈任何事情。你只要一谈起,就会想念起每一个人来,我只知道我很想念我所谈到的每一个人。——J·D·塞林格《麦田里的守望者》

[JS逆向]过无限debugger调试

在JS逆向过程当中,获取用发F12抓取XHR的时候,常常会发现网页不让我们打开F12开发人员工具。如果这个时候我们开启工具中禁止断点之后,虽然我们可以抓取xhr,但是这样我们无法调试代码部分了,开启了禁止断点之后,我们自己也无法在代码当中下断点了

需求

在采集某些网站时,目标网站为了防止别人分析调试前端代码,采取了反调试措施。其中一种做法是当你按F12进入浏览器控制台后,浏览器会自动命中debugger断点,并且无限循环,导致无法调试。以食品药品监督管理总局数据查询网站为例。如下图:

按F12进入控制台

解决方法

禁用浏览器断点


点击图中按钮,之后将不会再命中任何断点。这种方法虽然可以防止无限循环命中debugger断点,但是也存在很大的缺陷,因为对于其他代码,我们还是需要断点调试功能的。所以这个方法仅限于静态分析。

利用中间人修改响应代码

用Fiddler删除响应代码中的debugger即可达到目的
实现的核心代码很简单:如下

FiddlerApplication.BeforeRequest += delegate(Fiddler,Session oS)
{
     oS.bBufferResponse = true;
}

FiddlerApplication.BeforeResponse += (oS) =>
{
     oS.utilDecodeResponse();
     oS.utilReplaceLnResponse("debugger", String.Empty);
     /*
     *
     Code 实在是找不到FiddlerCode的代码格式,只能用Python代替一下
     *
     */
}

实际使用中发现,位于响应html页的debugger被删除了,但是仍然会弹出断点。分析页面得到,debugger断点位置一共有2处 第一处位于”http://qy1.sfda.gov.cn/datasearch/face3/dir.html"debugger以明文形式存在,Fiddler删除的就是这部分。通过分析另一处debugger位置,发现debugger是通过eval去实现的,响应中并没有直接出现debugger字段,所以没有被替换掉。

在Console输入

> _$uj()
<· "eval"
> _$dQ()
<· "(function() {var a = new Date(); debugger; return new Date() - a > 100;}())"
> _

代码经过强混淆,读者看到的函数名称是和我不一样的。

利用浏览器插件修改响应代码

具体原理和使用Fiddler是相同的,通过浏览器插件将请求重定向以达到修改代码的目的。也存在相同的问题

手动替换代码

既然修改响应结果无法满足需求,那只能从代码中寻找突破了。
以本文的网站为例,查看debugger断点处的调用栈堆,找到调用位置。其实在上文中间人方式结尾处已经发现了。是通过eval去实现断点的。我们先构造一个空方法


将目标网站的方法偷梁换柱

由于网站代码强混淆,所以函数名称会不一样。下面放个GIF图

完美解决 但是注意不要刷新,页面刷新后需要重新替换。

傻瓜式技巧

上文的几种方法,要么是存在缺陷,要么是步骤较为繁琐,我这边还有个压箱底的神技,不需要写任何代码,鼠标点点点就能够满足需求,为了避免伸手党,所以不放了。

总结

1-DebugPort
2-KdDisableDebugger
3-IsDebuggerPresent和CheckRemoteDebuggerPresent
4-hook

http://www.moguizuofang.com/bbs/thread-3235-1-1.html

http://bbs.pediy.com/showthread.php?t=126802

http://bbs.pediy.com/showthread.php?t=129810

DebugPort是进程EPROCESS结构里的一个成员,指向了一个用于进程调试的对象,如果一个进程不在被调试的时候那么就是NULL,否则他是一个指针。该对象负责在调试器与被调进程之间进行调试事件传递,因此被称为调试端口。被调试程序的事件由这个端口发送到调试器进程的。

HOOK系统中一些与调试相关的函数,也可以防止被各种调试器调试。比如某款程序在内核中就HOOK了下面这些函数:
NtOpenThread():防止调试器在程序内部创建线程
NtOpenProcess():防止OD(OllyDbg)等调试工具在进程列表中看到
KiAttachProcess():防止被附加上
NtReadVirtualMemory():防止被读内存
NtWriteVirtualMemory():防止内存被写
KdReceivePacket():KDCOME.dll 中Com串口接收数据函数
KdSendPacket():KDCOME.dll 中Com串口发送数据函数,可以HOOK这2个函数用来防止双机调试。

反反调试的思路也就出来了。针对清零DebugPort来防止调试的方法,可以通过对DebugPort内存地址下内存断点:
ba w4 debugport_addr
这样一旦有程序代码在修改DebugPort,就会被断下,从而找到对应的清零DebugPort的反调试代码,然后对这部分代码进行patch(用机器码0×90(nop)或者0xC3(ret)取代),从而让它失去作用,当然有的程序会对代码进行校验,一旦发现代码被篡改,就会采取保护措施,比如抛出异常或者退出程序。
针对调用系统函数如KdDisableDebugger()来检测调试器存在从而禁止被调试的方法,可以在对应的这些函数的地址下断点,然后对相关的代码进行patch,然后使该函数判断失效。比如:
bp KdDisableDebugger、eb xxx
针对通过HOOK系统函数来防止进程被调试的方法,可以直接将这些系统函数的钩子直接恢复,可以通过内核驱动程序或者借助一些ARK工具(比如Pchunter)就可以直接检测和恢复这些函数钩子。

总结 -2

在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己。为了了解如何破解反调试技术,首先我们来看看反调试技术。

一、Windows API方法

Win32提供了两个API, IsDebuggerPresent和CheckRemoteDebuggerPresent可以用来检测当前进程是否正在被调试,以IsDebuggerPresent函数为例,例子如下:

BOOL ret = IsDebuggerPresent();
printf("ret = %d\n", ret);

破解方法很简单,就是在系统里将这两个函数hook掉,让这两个函数一直返回false就可以了,网上有很多做hook API工作的工具,也有很多工具源代码是开放的,所以这里就不细谈了。

二、查询进程PEB的BeingDebugged标志位

当进程被调试器所附加的时候,操作系统会自动设置这个标志位,因此在程序里定期查询这个标志位就可以了,例子如下:

bool PebIsDebuggedApproach()
{
       char result = 0;
       __asm
       {
// 进程的PEB地址放在fs这个寄存器位置上
              mov eax, fs:[30h]
// 查询BeingDebugged标志位
              mov al, BYTE PTR [eax + 2]
              mov result, al
       }

       return result != 0;
}

三、查询进程PEB的NtGlobal标志位

跟第二个方法一样,当进程被调试的时候,操作系统除了修改BeingDebugged这个标志位以外,还会修改其他几个地方,其中NtDll中一些控制堆(Heap)操作的函数的标志位就会被修改,因此也可以查询这个标志位,例子如下:

bool PebNtGlobalFlagsApproach()
{
       int result = 0;

       __asm
       {
  // 进程的PEB
              mov eax, fs:[30h]
  // 控制堆操作函数的工作方式的标志位
              mov eax, [eax + 68h]
  // 操作系统会加上这些标志位FLG_HEAP_ENABLE_TAIL_CHECK,
  // FLG_HEAP_ENABLE_FREE_CHECK and FLG_HEAP_VALIDATE_PARAMETERS,
  // 它们的并集就是x70
  //
  // 下面的代码相当于C/C++的
  //     eax = eax & 0x70
              and eax, 0x70
              mov result, eax
       }

       return result != 0;
}

四、查询进程堆的一些标志位

这个方法是第三个方法的变种,只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,ForceFlags这个标志位会被修改,因此可以通过判断这个标志位的方式来反调试。因为进程可以有很多的堆,因此只要检查任意一个堆的头信息就可以了,所以这个方法貌似很强大,例子如下:

bool HeapFlagsApproach()
{
       int result = 0;

       __asm
       {
      // 进程的PEB
              mov eax, fs:[30h]
      // 进程的堆,我们随便访问了一个堆,下面是默认的堆
              mov eax, [eax + 18h]
  // 检查ForceFlag标志位,在没有被调试的情况下应该是
              mov eax, [eax + 10h]
              mov result, eax
       }

       return result != 0;
}

五、使用NtQueryInformationProcess函数

NtQueryInformationProcess函数是一个未公开的API,它的第二个参数可以用来查询进程的调试端口。如果进程被调试,那么返回的端口值会是-1,否则就是其他的值。由于这个函数是一个未公开的函数,因此需要使用LoadLibrary和GetProceAddress的方法获取调用地址,示例代码如下:

// 声明一个函数指针。
typedef NTSTATUS (WINAPI *NtQueryInformationProcessPtr)(
       HANDLE processHandle,
       PROCESSINFOCLASS processInformationClass,
       PVOID processInformation,
       ULONG processInformationLength,
       PULONG returnLength);

bool NtQueryInformationProcessApproach()
{
       int debugPort = 0;
       HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll "));
       NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
       if ( NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL) )
              printf("[ERROR NtQueryInformationProcessApproach] NtQueryInformationProcess failed\n");
       else
              return debugPort == -1;

       return false;
}

六、NtSetInformationThread方法

这个也是使用Windows的一个未公开函数的方法,你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger),等于告诉操作系统,将所有附加的调试器统统取消掉。示例代码:

// 声明一个函数指针。
typedef NTSTATUS (*NtSetInformationThreadPtr)(HANDLE threadHandle,
       THREADINFOCLASS threadInformationClass,
       PVOID threadInformation,
       ULONG threadInformationLength);

void NtSetInformationThreadApproach()
{
      HMODULE hModule = LoadLibrary(TEXT("ntdll.dll"));
      NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)GetProcAddress(hModule, "NtSetInformationThread");

      NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, 0, 0);
}

七、触发异常的方法

这个技术的原理是,首先,进程使用SetUnhandledExceptionFilter函数注册一个未处理异常处理函数A,如果进程没有被调试的话,那么触发一个未处理异常,会导致操作系统将控制权交给先前注册的函数A;而如果进程被调试的话,那么这个未处理异常会被调试器捕捉,这样我们的函数A就没有机会运行了。

这里有一个技巧,就是触发未处理异常的时候,如果跳转回原来代码继续执行,而不是让操作系统关闭进程。方案是在函数A里修改eip的值,因为在函数A的参数_EXCEPTION_POINTERS里,会保存当时触发异常的指令地址,所以在函数A里根据这个指令地址修改寄存器eip的值就可以了,示例代码如下:

// 进程要注册的未处理异常处理程序A
LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *pei)
{
       SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)
              pei->ContextRecord->Eax);
       // 修改寄存器eip的值
       pei->ContextRecord->Eip += 2;
       // 告诉操作系统,继续执行进程剩余的指令(指令保存在eip里),而不是关闭进程
       return EXCEPTION_CONTINUE_EXECUTION;
}

bool UnhandledExceptionFilterApproach()
{
       SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
       __asm
       {
              // 将eax清零
              xor eax, eax
              // 触发一个除零异常
              div eax
       }

       return false;
}

八、调用DeleteFiber函数

如果给DeleteFiber函数传递一个无效的参数的话,DeleteFiber函数除了会抛出一个异常以外,还是将进程的LastError值设置为具体出错原因的代号。然而,如果进程正在被调试的话,这个LastError值会被修改,因此如果调试器绕过了第七步里讲的反调试技术的话,我们还可以通过验证LastError值是不是被修改过来检测调试器的存在,示例代码:

bool DeleteFiberApproach()
{
       char fib[1024] = {0};
       // 会抛出一个异常并被调试器捕获
       DeleteFiber(fib);

       // 0x57的意思是ERROR_INVALID_PARAMETER
       return (GetLastError() != 0x57);
}

如何像打王者一样学习?

如何像打王者一样学习?

王者是什么?这篇文章不是说我这个学习方法是王者,别理解错了"王者"指的是《王者荣耀》,什么,你不知道王者荣耀?不会吧不会吧,该不会真的没有人会不知道王者荣耀吧?我简单来说一下:《王者荣耀》是由腾讯游戏天美工作室群开发并运行的一款运营在Android、IOS、NS平台上的MOBA类手机游戏,于2015年11月26日在Android、IOS平台上正式公测,游戏前期使用名称有《英雄战迹》、《王者联盟》。《Arena Of Valor》,即《王者荣耀》的欧美版本于2018年在任天堂Switch平台发售。王者荣耀中的玩法以竞技对战为主,玩家之间进行1VS1、3VS3、5VS5等多种方式的PVP对战,还可以参加游戏的冒险模式,进行PVE的闯关模式,在满足条件后可以参加游戏的排位赛等,是属于推塔类型的游戏。对,就是你经常玩的"王者"都知道王者有许多的机制,比如:排位机制,反馈机制,唯我独尊...

阅读更多

JsDelivr图床

介绍

先说一下:

JsDelivrGitHub的CDN

直接访问即可。

使用

GitHub

新建一个库,放图片。

(细说略。。)

PicGo

下载不说了。

默认的不好用。

用插件githubPlus

配置

项目 描述
repo 你的库名:用户名/库名
branch 直接填:master
token Github申请token(后面讲)
path 库内路径(选):img/
customUrl 使用JsDerlivr:https://cdn.jsdelivr.net/gh/用户名/库名
origin github

GitHub Token

GitHub申请Token

注意: Token妥善保管,且只显示一次。

上传

如果出错,改个名字,重试。

视频版