关注微信公众号:K 哥爬虫,继续分享爬虫进阶、JS/ 安卓逆向等技术干货!
申明
本文章中所有内容仅供学习交换,抓包内容、敏感网址、数据接口均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关,若有侵权,请分割我立刻删除!
写在后面
题目自身不是很难,然而其中有很多坑,次要是反 Hook 操作和本地联调补环境,本文会具体介绍每一个坑,并不只是一笔带过,写得十分具体!
通过本文你将学到:
- Hook Function 和定时器来打消有限 debugger;
- 解决反 Hook,通过 Hook 的形式找到加密参数 _signature;
- 剖析浏览器与本地环境差别,如何寻找 navigator、document、location 等对象,如何本地补环境;
- 如何利用 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 版本混同了的,咱们将外面的一些混同代码在控制台输入一下,而后手动还原一下这段代码,有两个变量 i1I1i1li
和 illllli1
,看起来吃力,间接用 a
和 b
代替,如下所示:
(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 个按钮:
- Show Execution Point (Alt + F10):如果你的光标在其它行或其它页面,点击这个按钮可跳转到以后断点所在的行;
- Step Over (F8):步过,一行一行地往下走,如果这一行上有办法也不会进入办法;
- Step Into (F7):步入,如果以后行有办法,能够进入办法外部,个别用于进入用户编写的自定义办法内,不会进入官网类库的办法;
- Force Step Into (Alt + Shift + F7):强制步入,能进入任何办法,查看底层源码的时候能够用这个进入官网类库的办法;
- Step Out (Shift + F8):步出,从步入的办法内退出到办法调用处,此时办法已执行结束,只是还没有实现赋值;
- Restart Frame:放弃以后断点,从新执行断点;
- Run to Cursor (Alt + F9):运行到光标处,代码会运行至光标行,不须要打断点;
- 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.log
、console.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()