郑重声明:本项目的全部代码和相关文章, 仅用于经验技术交流分享,禁止将相关技术应用到不正当途径,由于滥用技术产生的风险与本人无关。javascript
这篇文章是公众号《云爬虫技术研究笔记》的《2019年底逆向复习系列》的第八篇:《拼夕夕Web端anti_content参数逆向分析》html
本次案例+代码已上传至代码库https://github.com/lateautumn4lin/Review_Reverse
的路径pdd
下面了,欢迎老哥们Fork
+Star
二连操做。java
http://yangkeduo.com/
node
拼夕夕成立三年就上市,市值直追2/3京东的市值,算得上是互联网历史中的一“神话”了,此次案例来分析一下拼夕夕Web端的anti_content是如何生成的,anti_content能干什么呢?有了anti_content就能使用拼夕夕Web端的搜索接口来获取相应的商品列表了。python
咱们首先来分析下整个请求流程中请求的顺序以及各个请求所需的参数
git
能够看到搜索接口地址是http://yangkeduo.com/proxy/api/search
,具体的参数是
github
list_id
(不肯定,待会能够寻找)flip
(不肯定,待会能够寻找)anti_content
(不肯定,待会能够寻找)大体分析了所有参数,list_id
、flip
、anti_content
这三个参数属于未知,想一想既然是翻页,这些参数多是来源于上一页?,验证下
list_id
,flip
能够在搜索首页中找到,用来作首次请求
每次请求搜索接口返回的响应中能够获取下次请求参数的flip
,从值的含义上来看,应该是offset
偏移量相关,如今三个未知参数中只剩anti_content
这个参数未知,这就是咱们本次须要逆向分析的参数。web
咱们对比下先后两次的调用搜索接口的请求,发送前一个请求的响应中返回了set-cookie
接着在第二次调用请求中把cookie
中的jsessionid
给更换了
咱们的请求流程就分析到这里,具体流程以下(使用到了昨天我说的手绘风格画图工具):
express
逆向的第一步就是如何定位加密函数的位置,由于搜索请求是xhr
请求,咱们直接打个xhr
断点(或者想要直接全局搜索anti_content
字符都行),在Source
的tab
右侧下个断点,断点值就是搜索接口url
的一部分—/proxy/api/search
接着再把页面往下来,去请求下一页数据,能够看到函数断在这里
接着仍是通常的流程,看看右侧的调用栈call stack
的每一个函数来定位可能的调用位置
咱们不能跟踪到异步函数调用前的值,因此咱们从新打断点
能够发现,断点以前anti_content
值已经生成,也就是_sent
值,咱们须要回追
追到这里,咱们在这个return打断点,由于这里只有一个参数,比较容易观察,咱们从新请求
发现这个时候的t
的值不是以前的anti_content
,因此猜想多是这个函数以后的函数生成的anti_content
,咱们屡次f8
跳过,会发现断点断在这里
屡次试验以后发现,咱们再次按f8
就会t的值就会生成,所以猜想两次调用之间会找到加密函数,咱们f11
跟下去
多尝试几回就会看到跳到了新的一个js文件—RiskControl
文件,看着这个文件名,翻译过来是风险控制,也就是风控?,看来颇有多是这个文件,咱们来验证下这个函数是不是加密函数
继续f11
能够看出,而后在console
里面调试值,能够发现kt
这个函数就是加密的函数,如今咱们找到了加密的函数,下面跟进函数去分析。npm
咱们跟进刚才的kt
函数,看看整个函数的逻辑,整个未还原的函数的代码在这里,先大概静态分析下代码
function kt() { var t, n = {}; // 定义变量 n[h("0xda", "$1%G")] = function(t) { return t() } //h("0xda", "$1%G")的值在console里面是"CeIdF",因此包括这个和下面的几个函数 都是给n这个dict去赋值的 , n[h("0xdb", "wZhN")] = h("0xdc", "76m3"), n[h("0xdd", "Vfvl")] = function(t, n) { return t < n } , n[h("0xde", "YoRA")] = function(t, n) { return t * n } , n[h("0xdf", "OtK!")] = function(t) { return t() } , n[h("0xe0", "U#^v")] = function(t, n, r) { return t(n, r) } , n[h("0xe1", "[6Hz")] = function(t, n) { return t < n } , n[h("0xe2", "Nj]Q")] = h("0xe3", "7o8w"), n[h("0xe4", "L3Mt")] = function(t, n) { return t === n } , n[h("0xe5", "m3X(")] = function(t, n) { return t > n } , n[h("0xe6", "E5c@")] = function(t, n) { return t <= n } , n[h("0xe7", "JQC0")] = function(t, n) { return t - n } , n[h("0xe8", "hlS%")] = function(t, n) { return t << n } , n[h("0xe9", "QnID")] = function(t, n) { return t > n } , n[h("0xea", "XVjd")] = function(t, n) { return t << n } , n[h("0xeb", "xkH%")] = function(t, n) { return t === n } , n[h("0xec", "T[4u")] = h("0xed", "YoRA"), n[h("0xee", "rFSk")] = h("0xef", "[6Hz"), n[h("0xf0", "$1%G")] = function(t, n) { return t + n } , n[h("0xf1", "Nj]Q")] = h("0xf2", "QnID"), n[h("0xf3", "L3Mt")] = h("0xf4", "w@Yj"), Y = n[h("0xf5", "$1%G")](n[h("0xf6", "5f)w")](Math[y](), 10), 7) ? "" : "N"; //生成随机数 var r = [h("0xb5", "&k7t") + Y] , e = (t = [])[F].apply(t, [rt ? [][F](n[h("0xf7", "7o8w")](yt), st[r]()) : f[r](), ut[r](), ct[r](), ft[r](), wt[r](), ht[r](), lt[r](), dt[r](), xt[r](), _t[r](), vt[r]()].concat(function(t) { if (Array.isArray(t)) { for (var n = 0, r = Array(t.length); n < t.length; n++) r[n] = t[n]; return r } return Array.from(t) }(pt[r]()), [gt[r](), bt[r](), Ct[r]()])); //特别长的函数,先是把一个数组concat另外一个数组,而后appy这个数组,这段赋值能够获得r和e的值 n[h("0xe0", "U#^v")](setTimeout, function() { n[h("0xf8", "@25z")](Ot) }, 0); //异步延后执行Ot函数 for (var i = e[H][x](2)[h("0xf9", "5f)w")](""), a = 0; n[h("0xfa", "rFSk")](i[H], 16); a += 1) i[n[h("0xfb", "9njl")]]("0"); i = i[h("0xfc", "ZM84")](""); var o = []; n[h("0xfd", "1MxR")](e[H], 0) ? o[U](0, 0) : n[h("0xfe", "T[4u")](e[H], 0) && n[h("0xff", "EZlb")](e[H], n[h("0x100", "mMg5")](n[h("0x101", "&ETh")](1, 8), 1)) ? o[U](0, e[H]) : n[h("0x102", "7o8w")](e[H], n[h("0x103", "76m3")](n[h("0x104", "E5c@")](1, 8), 1)) && o[U](W[_](i[D](0, 8), 2), W[_](i[D](8, 16), 2)), //这里的三目运算符用的真牛皮! e = [][F]([n[h("0x105", "HMtq")](Y, "N") ? 2 : 1], [0, 0, 0], o, e); var c = u[n[h("0x106", "EZlb")]](e) , w = [][n[h("0x107", "eKTC")]][h("0x108", "w@Yj")](c, function(t) { return String[n[h("0x109", "w@Yj")]](t) }); //依旧是函数调用 return n[h("0x10a", "T[4u")](n[h("0x10b", "76m3")], s[n[h("0x10c", "@25z")]](w[h("0x10d", "hlS%")](""))) //计算出anti_content值 }
通过上面的分析,比较重要的点是下面图上打出的断点
接下来,就是动态调试去扣js
啦!
Y
值,能够发现这个Y
就是一个随机值,这里能够重写。Y = n[h("0xf5", "$1%G")](n[h("0xf6", "5f)w")](Math[y](), 10), 7) ? "" : "N";
e
的值得出是一个数组,不过具体的扣js
就详细说了,你们能够在调试的过程当中去简化代码。setTimeout
在这里使用n[h("0xe0", "U#^v")](setTimeout, function() { n[h("0xf8", "@25z")](Ot) }, 0);
看看n[h("0xe0", "U#^v")]
是什么函数
n[h("0xe0", "U#^v")] = function(t, n, r) { return t(n, r) }
因此简化来看就是
setTimeout(function() { n[h("0xf8", "@25z")](Ot) },0) n[h("0xda", "$1%G")] = function(t) { return t() } //也就是立刻执行Ot函数
看看Ot
函数是什么
function Ot() { f[h("0xce", "&ETh")](), [st, ut][G](function(t) { t[V] = [] }) }
先看第一个函数
f[h("0xce", "&ETh")] = function() { [z, j, T, S][D](function(t) { t[y] = [] }) } //在console里获得具体的值
scrollTop
在js
中表示垂直滚动条位置,应该是检测是否滑动的参数,这里写死就行。
return n[h("0x10a", "T[4u")](n[h("0x10b", "76m3")], s[n[h("0x10c", "@25z")]](w[h("0x10d", "hlS%")]("")))
具体能够自行简化,f11调试能够看到这里
能够看到这里的函数,t
应该是anti_content
的乱码,a("0x13", "Dd5H")
是encode
,这个函数应该是把乱码的anti_content
还原出来,具体的你们能够去扣,这里就不细讲了。
此次构造加密函数不使用python
去调用js
脚本,而是使用node
直接去调用,缘由主要有几个:
Python
调用js
的库主要是Pyexecjs
,然而做者已经宣布不维护了,能够参考https://gist.github.com/doloopwhile/8c6ec7dd4703e8a44e559411cb2ea221
。python
调用js
不如原生调用来的实际和方便。node
服务框架包装加密函数,解耦了函数之间的关系,更方便以后的维护和修改。基于以上的缘由,选择node
服务框架来调用js
调用暴露出接口,node
服务框架选用node
生态中占有份额最大的express
,使用简单上手,和python
的flask
同样,几行代码启动一个服务。
express
模块npm install express --save
express
例子const express = require('express'); const bodyParser = require('body-parser'); // 建立应用实例 const app = express(); pp.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.get('/get_anti_content', function (req, res) { let anti_result = o()()["messagePackSync"]("http://yangkeduo.com/search_result.html"); console.log( "获取anti_content值为: %s", anti_result ); res.json( { anti_result: anti_result } ) }); // 监听8000端口并在运行成功后向控制台输入服务器启动成功! const server = app.listen(8000, function () { let host = server.address().address; let port = server.address().port; console.log( "node服务启动,监听地址为: http://%s:%s", host, port ) });
node xxx.js
先从首页获取flip
和list_id
def get_pdd_search_lst(search_name: str) -> None: with requests.get( url=f"http://yangkeduo.com/search_result.html?search_key={search_name}", headers=headers ) as response: data = re.findall(r"window.rawData=([\s\S]*?)</script>", response.text) if not data: raise Exception("extract json error") data = data[0].strip().strip(";") json_data = json.loads(data)["store"]["data"]["ssrListData"] msg_data = dict(json.loads(json_data["loadSearchResultTracking"]["req_params"]), **{"flip": json_data["flip"]})
而后调用node
服务获取anti_content
with requests.get( url=f"http://yangkeduo.com/proxy/api/search", headers=headers, params={ "pdduid": "4787727322403", "source": "search", "search_met": "", "track_data": "refer_page_id,10169_1576665846887_tfHPiWnbtu", "list_id": msg_data["list_id"], "sort": "default", "filter": "", "q": search_name, "page": 2, "size": 50, "flip": msg_data["flip"], "anti_content": requests.get('http://localhost:8000/get_anti_content').json()["anti_result"] } ) as lst_response: print(lst_response.json())
headers
中加上AccessToken
,不加的话会报错{'server_time': 1576763884, 'error_code': 40001}
Cookie
JSESSIONID
多年反爬虫破解经验,AKA“逆向小学生”,沉迷数据分析和黑客增加不能自拔,虚名有CSDN博客专家和华为云享专家。
呕心沥血从浩瀚的资料中整理了独家的“私藏资料”,公众号内回复“私藏资料”便可领取爬虫高级逆向教学视频以及多平台的中文数据集
2019年底逆向复习系列之知乎登陆formdata加密逆向破解