乐趣区

关于python:JS-逆向百例网洛者反爬练习平台第一题JS-混淆加密反-Hook-操作

关注微信公众号:K 哥爬虫,继续分享爬虫进阶、JS/ 安卓逆向等技术干货!

申明

本文章中所有内容仅供学习交换,抓包内容、敏感网址、数据接口均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关,若有侵权,请分割我立刻删除!

写在后面

题目自身不是很难,然而其中有很多坑,次要是反 Hook 操作和本地联调补环境,本文会具体介绍每一个坑,并不只是一笔带过,写得十分具体!

通过本文你将学到:

  1. Hook Function 和定时器来打消有限 debugger;
  2. 解决反 Hook,通过 Hook 的形式找到加密参数 _signature;
  3. 剖析浏览器与本地环境差别,如何寻找 navigator、document、location 等对象,如何本地补环境;
  4. 如何利用 PyCharm 进行本地联调,定位本地和浏览器环境的差别,从而过掉检测。

逆向指标

  • 指标:网洛者反反爬虫练习平台第一题:JS 混同加密,反 Hook 操作
  • 链接:http://spider.wangluozhe.com/…
  • 简介:本题要提交的答案是 100 页的所有数据并加和,要求以 Hook 的形式实现此题,不要以 AST、扣代码等形式解决,不要应用 JS 反混同工具进行解密。(Hook 代码的写法和用法,K 哥以前文章有,本文不再具体介绍)

绕过有限 debugger

首先察看到点击翻页,URL 并没有发生变化,那么个别就是 Ajax 申请,每一次申请有些参数会扭转,纯熟的按下 F12 筹备查找加密参数,会发现立马断住,进入有限 debugger 状态,往上跟一个栈,能够发现 debugger 字样,如下图所示:

这种状况在 K 哥以前的案例中也有,过后咱们是间接重写这个 JS,把 debugger 字样给替换掉就行了,然而本题很显然是心愿咱们以 Hook 的办法来过掉有限 debugger,除了 debugger 以外,咱们留神到后面还有个 constructor 字样,在 JavaScript 中它叫构造方法,个别在对象创立或者实例化时候被调用,它的根本语法是:constructor([arguments]) {...},具体介绍可参考 MDN 构造方法,在本案例中,很显著 debugger 就是 constructor 的 arguments 参数,因而咱们能够写出以下 Hook 代码来过掉有限 debugger:

// 先保留原 constructor
Function.prototype.constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
    // 如果参数为 debugger,就返回空办法
    if(a == "debugger") {return function (){};}
    // 如果参数不为 debugger,还是返回原办法
    return Function.prototype.constructor_(a);
};

注入 Hook 代码的办法也有很多,比方间接在浏览器开发者工具控制台输出代码(刷新网页会生效)、Fiddler 插件注入、油猴插件注入、自写浏览器插件注入等,这些办法在 K 哥以前的文章都有介绍,明天就不再赘述。

本次咱们应用 Fiddler 插件注入,注入以上 Hook 代码后,会发现会再次进入有限 debugger,setInterval,很显著的定时器,他有两个必须的参数,第一个是要执行的办法,第二个是工夫参数,即周期性调用办法的工夫距离,以毫秒为单位,具体介绍可参考菜鸟教程 Window setInterval(),同样咱们也能够将其 Hook 掉:

// 先保留原定时器
var setInterval_ = setInterval
setInterval = function (func, time){
    // 如果工夫参数为 0x7d0,就返回空办法
    // 当然也能够不判断,间接返回空,有很多种写法
    if(time == 0x7d0)
    {return function () {};}
    // 如果工夫参数不为 0x7d0,还是返回原办法
    return setInterval_(func, time)
}

将两段 Hook 代码粘贴到浏览器插件里,开启 Hook,从新刷新页面就会发现曾经过掉了有限 debugger。

Hook 参数

过掉有限 debugger 后,咱们轻易点击一页,抓包能够看到是个 POST 申请,Form Data 里,page 是页数,count 是每一页数据量,_signature 是咱们要逆向的参数,如下图所示:

咱们间接搜寻 _signature,只有一个后果,其中有个 window.get_sign() 办法就是设置 _signature 的函数,如下图所示:

这里问题来了!!!咱们再看看本题的题目,JS 混同加密,反 Hook 操作,作者也再三强调本题是考验 Hook 能力!并且到目前为止,咱们如同还没有遇到什么反 Hook 伎俩,所以,这样间接搜寻 _signature 很显然太简略了,必定是要通过 Hook 的形式来获取 _signature,并且后续的 Hook 操作必定不会一帆风顺!

话不多说,咱们间接写一个 Hook window._signature 的代码,如下所示:

(function() {
    // 谨严模式 查看所有谬误
    'use strict';
    //window 为要 hook 的对象,这里是 hook 的 _signature
    var _signatureTemp = "";
    Object.defineProperty(window, '_signature', {
        //hook set 办法也就是赋值的办法 
        set: function(val) {console.log('Hook 捕捉到 _signature 设置 ->', val);
                debugger;
                _signatureTemp = val;
                return val;
        },
        //hook get 办法也就是取值的办法 
        get: function()
        {return _signatureTemp;}
    });
})();

将两个绕过有限 debugger 的 Hook 代码,和这个 Hook _signature 的代码一起,应用 Fiddler 插件一起注入(这里留神要把绕过 debugger 的代码放在 Hook _signature 代码的前面,否则有可能不起作用,这可能是插件的 BUG),从新刷新网页,能够发现前端的一排页面的按钮不见了,关上开发者工具,能够看到右上角提醒有两个谬误,点击可跳转到出错的代码,在控制台也能够看到报错信息,如下图所示:

整个 1.js 代码是通过了 sojson jsjiami v6 版本混同了的,咱们将外面的一些混同代码在控制台输入一下,而后手动还原一下这段代码,有两个变量 i1I1i1liillllli1,看起来吃力,间接用 ab 代替,如下所示:

(function() {
    'use strict';
    var a = '';
    Object["defineProperty"](window, "_signature", {set: function(b) {
            a = b;
            return b;
        },
        get: function() {return a;}
    });
}());

是不是很相熟?有 get 和 set 办法,这不就是在进行 Hook window._signature 操作吗?整个逻辑就是当 set 办法设置 _signature 时,将其赋值给 a,get 办法获取 _signature 时,返回 a,这么操作一番,实际上对于 _signature 没有任何影响,那这段代码存在的意义是啥?为什么咱们增加了本人的 Hook 代码就会报错?

来看看报错信息:Uncaught TypeError: Cannot redefine property: _signature,不能从新定义 _signature?咱们的 Hook 代码在页面一加载就运行了 Object.defineProperty(window, '_signature', {}),等到网站的 JS 再次 defineProperty 时就会报错,那很简略嘛,既然不让从新定义,而且网站本人的 JS Hook 代码不会影响 _signature,间接将其删掉不就行了嘛!这个中央大略就是反 Hook 操作了。

保留原 1.js 到本地,删除其 Hook 代码,应用 Fiddler 的 AutoResponder 性能替换响应(替换办法有很多,K 哥以前的文章同样有介绍),再次刷新发现异常解除,并且胜利 Hook 到了 _signature

逆向参数

胜利 Hook 之后,间接跟栈,间接把办法裸露进去了:window._signature = window.byted_acrawler(window.sign())

先来看看 window.sign(),选中它其实就能够看到是 13 位毫秒级工夫戳,咱们跟进 1.js 去看看他的实现代码:

咱们将局部混同代码手动还原一下:

window["sign"] = function sign() {
    try {div = document["createElement"];
        return Date["parse"](new Date())["toString"]();} catch (IIl1lI1i) {return "123456789abcdefghigklmnopqrstuvwxyz";}
}

这里就要留神了,有个坑给咱们埋下了,如果间接略过,感觉就一个工夫戳没啥难看的,那你就大错特错了!留神这是一个 try-catch 语句,其中有一句 div = document["createElement"];,有一个 HTML DOM Document 对象,创立了 div 标签,这段代码如果放到浏览器执行,没有任何问题,间接走 try 语句,返回工夫戳,如果在咱们本地 node 执行,就会捕捉到 document is not defined,而后走 catch 语句,返回的是那一串数字加字母,最初的后果必定是不正确的!

解决办法也很简略,在本地代码里,要么去掉 try-catch 语句,间接 return 工夫戳,要么在结尾定义一下 document,再或者间接正文掉创立 div 标签的这行代码,然而 K 哥在这里举荐间接定义一下 document,因为谁能保障在其余中央也有相似的坑呢?万一暗藏得很深,没发现,岂不是白费力气了?

而后再来看看 window.byted_acrawler(),return 语句里次要用到了 sign() 也就是 window.sign() 办法和 IIl1llI1() 办法,咱们跟进 IIl1llI1() 办法能够看到同样应用了 try-catch 语句,nav = navigator[liIIIi11('2b')]; 和后面 div 的状况一模一样,同样的这里也倡议间接定义一下 navigator,如下图所示:

到这里用到的办法基本上剖析结束,咱们将 window、document、navigator 都定义一下后,本地运行一下,会提醒 window[liIIIi11(...)] is not a function

咱们去网页里看看,会发现这个办法其实就是一个定时器,没有太大作用,间接正文掉即可:

PyCharm 本地联调

通过以上操作当前,再次本地运行,会提醒 window.signs is not a function,出错的中央是一个 eval 语句,咱们去浏览器看一下这个 eval 语句,发现明明是 window.sign(),为什么本地就变成了 window.signs(),平白无故多了个 s 呢?

造成这种状况的起因只有一个,那就是本地与浏览器的环境差别,混同的代码里必定有环境检测,如果不是浏览器环境的话,就会批改 eval 里的代码,多加了一个 s,这里如果你间接删掉蕴含 eval 语句的整个函数和下面的 setInterval 定时器,代码也能失常运行,然而,K 哥一贯是谋求细节的!多加个 s 的起因咱必须得搞清楚呀!

咱们在本地应用 PyCharm 进行调试,看看到底是哪里给加了个 s,出错的中央是这个 eval 语句,咱们点击这一行,下个断点,右键 debug 运行,进入调试界面(PS:原代码有有限 debugger,如果不做解决,PyCharm 里调试同样也会进入有限 debugger,能够间接把后面的 Hook 代码加到本地代码后面,也能够间接删除对应的函数或变量):

左侧是调用栈,右侧是变量值,整体上和 Chrome 外面的开发者工具差不多,具体用法可参考 JetBrains 官网文档,次要介绍一下图中的 8 个按钮:

  1. Show Execution Point (Alt + F10):如果你的光标在其它行或其它页面,点击这个按钮可跳转到以后断点所在的行;
  2. Step Over (F8):步过,一行一行地往下走,如果这一行上有办法也不会进入办法;
  3. Step Into (F7):步入,如果以后行有办法,能够进入办法外部,个别用于进入用户编写的自定义办法内,不会进入官网类库的办法;
  4. Force Step Into (Alt + Shift + F7):强制步入,能进入任何办法,查看底层源码的时候能够用这个进入官网类库的办法;
  5. Step Out (Shift + F8):步出,从步入的办法内退出到办法调用处,此时办法已执行结束,只是还没有实现赋值;
  6. Restart Frame:放弃以后断点,从新执行断点;
  7. Run to Cursor (Alt + F9):运行到光标处,代码会运行至光标行,不须要打断点;
  8. Evaluate Expression (Alt + F8):计算表达式,能够间接运行表达式,不须要在命令行输出。

咱们点击步入按钮(Step Into),会进入到 function IIlIliii(),这里同样应用了 try-catch 语句,持续下一步,会发现捕捉到了异样,提醒 Cannot read property 'location' of undefined,如下图所示:

咱们输入一下各个变量的值,手动还原一下代码,如下:

function IIlIliii(II1, iIIiIIi1) {
    try {href = window["document"]["location"]["href"];
        check_screen = screen["availHeight"];
        window["code"] = "gnature = window.byted_acrawler(window.sign())";
        return '';
    } catch (I1IiI1il) {window["code"] = "gnature = window.byted_acrawlers(window.signs())";
        return '';
    }
}

这么一来,就发现了端倪,在本地咱们并没有 document、location、href、availHeight 对象,所以就会走 catch 语句,变成了 window.signs(),就会报错,这里解决办法也很简略,能够间接删掉多余代码,间接定义为不带 s 的那串语句,或者也能够抉择补一下环境,在浏览器里看一下 href 和 screen 的值,定义一下即可:

var window = {
    "document": {
        "location": {"href": "http://spider.wangluozhe.com/challenge/1"}
    },
}

var screen = {"availHeight": 1040}

而后再次运行,又会提醒 sign is not defined,这里的 sign() 其实就是 window.sign(),也就是上面的 window[liIIIi11('a')] 办法,任意改一种写法即可:

再次运行,没有谬误了,咱们能够本人写一个办法来获取 _signature:以下写法二选一,都能够:

function getSign(){return window[liIIIi11('9')](window[liIIIi11('a')]())
}

function getSign(){return window.byted_acrawler(window.sign())
}

// 测试输入
console.log(getSign())

咱们运行一下,发现在 Pycharm 里并没有任何输入,同样的咱们在题目页面的控制台输入一下 console.log,发现被置空了,如下图所示:

看来他还对 console.log 做了解决,其实这种状况问题不大,咱们间接应用 Python 脚本来调用后面咱们写的 getSign() 办法就能失去 _signature 的值了,然而,再次重申,K 哥一贯是谋求细节的!我就得找到解决 console.log 的中央,把它变为失常!

这里咱们依然应用 Pycharm 来调试,进一步相熟本地联调,在 console.log(getSign()) 语句处下个断点,一步一步跟进,会发现进到了语句 var IlII1li1 = function() {};,查看此时变量值,发现 console.logconsole.warn 等办法都被置空了,如下图所示:

再往下一步跟进,发现间接返回了,这里有可能第一次运行 JS 时就会对 console 相干命令进行办法置空解决,所以先在疑似对 console 解决的办法外面下几个断点,再从新调试,会发现会走到 else 语句,而后间接将 IlII1li1 也就是空办法,赋值给 console 相干命令,如下图所示:

定位到了问题所在,咱们间接把 if-else 语句正文掉,不让它置空即可,而后再次调试,发现就能够间接输入后果了:

调用 Python 携带 _signature 挨个计算每一页的数据,最终提交胜利:

残缺代码

GitHub 关注 K 哥爬虫,继续分享爬虫相干代码!欢送 star!https://github.com/kgepachong/

以下只演示局部要害代码,不能间接运行! 残缺代码仓库地址:https://github.com/kgepachong…

JavaScript 加密要害代码架构

var window = {
    "document": {
        "location": {"href": "http://spider.wangluozhe.com/challenge/1"}
    },
}

var screen = {"availHeight": 1040}
var document = {}
var navigator = {}
var location = {}

// 先保留原 constructor
Function.prototype.constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
    // 如果参数为 debugger,就返回空办法
    if(a == "debugger") {return function (){};}
    // 如果参数不为 debugger,还是返回原办法
    return Function.prototype.constructor_(a);
};

// 先保留原定时器
var setInterval_ = setInterval
setInterval = function (func, time){
    // 如果工夫参数为 0x7d0,就返回空办法
    // 当然也能够不判断,间接返回空,有很多种写法
    if(time == 0x7d0)
    {return function () {};}
    // 如果工夫参数不为 0x7d0,还是返回原办法
    return setInterval_(func, time)
}

var iil = 'jsjiami.com.v6'
  , iiIIilii = [iil, '\x73\x65\x74\x49\x6e\x74\x65\x72\x76\x61\x6c', '\x6a\x73\x6a', ...];
var liIIIi11 = function(_0x11145e, _0x3cbe90) {_0x11145e = ~~'0x'['concat'](_0x11145e);
    var _0x636e4d = iiIIilii[_0x11145e];
    return _0x636e4d;
};
(function(_0x52284d, _0xfd26eb) {
    var _0x1bba22 = 0x0;
    for (_0xfd26eb = _0x52284d['shift'](_0x1bba22 >> 0x2); _0xfd26eb && _0xfd26eb !== (_0x52284d['pop'](_0x1bba22 >> 0x3) + '')['replace'](/[fnwRwdGKbwKrRFCtSC=]/g,''); _0x1bba22++) {_0x1bba22 = _0x1bba22 ^ 0x661c2;}
}(iiIIilii, liIIIi11));
// window[liIIIi11('0')](function() {//     var l111IlII = liIIIi11('1') + liIIIi11('2');
//     if (typeof iil == liIIIi11('3') + liIIIi11('4') || iil != l111IlII + liIIIi11('5') + l111IlII[liIIIi11('6')]) {//         var Ilil11iI = [];
//         while (Ilil11iI[liIIIi11('6')] > -0x1) {//             Ilil11iI[liIIIi11('7')](Ilil11iI[liIIIi11('6')] ^ 0x2);
//         }
//     }
//     iliI1lli();
// }, 0x7d0);
(function() {var iiIIiil = function() {}();
    var l1liii11 = function() {}();
    window[liIIIi11('9')] = function byted_acrawler() {};
    window[liIIIi11('a')] = function sign() {};
    (function() {}());
    // (function() {
    //     'use strict';
    //     var i1I1i1li = '';
    //     Object[liIIIi11('1f')](window, liIIIi11('21'), {//         '\x73\x65\x74': function(illllli1) {
    //             i1I1i1li = illllli1;
    //             return illllli1;
    //         },
    //         '\x67\x65\x74': function() {
    //             return i1I1i1li;
    //         }
    //     });
    // }());
    var iiil1 = 0x0;
    var l11il1l1 = '';
    var ii1Ii = 0x8;
    function i1Il11i(iiIll1i) {}
    function I1lIIlil(l11l1iIi) {}
    function lllIIiI(IIi1lIil) {}

    // 此处省略 N 个函数
    
    window[liIIIi11('37')]();}());

function iliI1lli(lil1I1) {function lili11I(l11I11l1) {if (typeof l11I11l1 === liIIIi11('38')) {return function(lllI11i) {}
            [liIIIi11('39')](liIIIi11('3a'))[liIIIi11('8')](liIIIi11('3b'));
        } else {if ((''+ l11I11l1 / l11I11l1)[liIIIi11('6')] !== 0x1 || l11I11l1 % 0x14 === 0x0) {(function() {return !![];
                }
                [liIIIi11('39')](liIIIi11('3c') + liIIIi11('3d'))[liIIIi11('3e')](liIIIi11('3f')));
            } else {(function() {return ![];
                }
                [liIIIi11('39')](liIIIi11('3c') + liIIIi11('3d'))[liIIIi11('8')](liIIIi11('40')));
            }
        }
        lili11I(++l11I11l1);
    }
    try {if (lil1I1) {return lili11I;} else {lili11I(0x0);
        }
    } catch (liIlI1il) {}}
;iil = 'jsjiami.com.v6';

// function getSign(){//     return window[liIIIi11('9')](window[liIIIi11('a')]())
// }

function getSign(){return window.byted_acrawler(window.sign())
}

console.log(getSign())

Python 计算要害代码

# ==================================
# --*-- coding: utf-8 --*--
# @Time    : 2021-12-01
# @Author  : 微信公众号:K 哥爬虫
# @FileName: challenge_1.py
# @Software: PyCharm
# ==================================


import execjs
import requests

challenge_api = "http://spider.wangluozhe.com/challenge/api/1"
headers = {
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Cookie": "将 cookie 值改为你本人的!",
    "Host": "spider.wangluozhe.com",
    "Origin": "http://spider.wangluozhe.com",
    "Referer": "http://spider.wangluozhe.com/challenge/1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36",
    "X-Requested-With": "XMLHttpRequest"
}


def get_signature():
    with open('challenge_1.js', 'r', encoding='utf-8') as f:
        ppdai_js = execjs.compile(f.read())
    signature = ppdai_js.call("getSign")
    print("signature:", signature)
    return signature


def main():
    result = 0
    for page in range(1, 101):
        data = {
            "page": page,
            "count": 10,
            "_signature": get_signature()}
        response = requests.post(url=challenge_api, headers=headers, data=data).json()
        for d in response["data"]:
            result += d["value"]
    print("后果为:", result)


if __name__ == '__main__':
    main()

退出移动版