文章目录
- 1. 写在前面
- 2. 接口分析
- 3. 断点分析
- 4. RPC调用
- 5. 算法还原
【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章
1. 写在前面
最近有点小忙,又来到了周末,熬夜更一篇逆向技术文章!本期写的是之前做过的一个网站,一个类似七麦数据的APP聚合类平台,平台内榜单、搜索、评论、评分、下载量等接口在发送请求的时候需要携带一个加密参数K
往期同类文章回顾,感兴趣的可阅读:七麦analysis参数分析与Python算法实现
2. 接口分析
这个加密参数K的算法在所有的接口均是通用的,加密手法跟很多网站相似也不相似,相似的是API路径加请求参数参打包加密,不相似的是在此之外又增加了动态参数参与加密,这里我们以评论页面为例,开始分析,如下所示:
随意点击一个切换评论天数,触发请求,查看一下接发包情况。可以看到就一个加密参数K值,其他字段的话都是固定的!其中id字段是APP应用的唯标识,也是密文!这里我没有去深入研究,按理说也是可以去分析找到生成算法的,当然非必要!应为在搜索的时候可以从上级接口中拿到每个APP对应的密文id,如下所示:
3. 断点分析
针对上面的加密参数,首先需要定位加密参数生成的位置,也是逆向分析的第一步!有多种定位的方式,其中包括XHR断点以及Hook,直接搜索加密之后的K,基本是无望,一搜一大堆。这里我们下一个XHR断点
触发断点后往前跟栈,可以看到JS文件没有一百也有大几十,都是Webpack,如下经过反复调试找到加密入口函数,如下所示:
h加密方法接受四个参数,e是请求提交的参数,path是接口的短路径,r是请求方法,n也是一串参数,不知道是哪里来的话,这里可以先不管。只需要知道它肯定也是参与了加密就行,可以在控制台看了一下明文信息,如下所示:
把之前的断点释放在加密方法内下个断,往下走可以看到最终加密参数生成的结果返回,我们可以在控制台打印验证一下,如下所示:
4. RPC调用
这里借着这个案例分别说说RPC跟扣加密算法扣取与还原两种解决方案。找到了加密方法后,如果你不想再继续调试去扣加密代码的话,就可以上RPC了!RPC是什么?即远程调用,调用什么?调用最终的JS加密方法,我们可以不需要继续去了解这个方法下面的具体加密逻辑,因为就算我们扣下来了,可能还会面临环境问题
做爬虫JS逆向,不需要去深入了解RPC技术,只需要知道如何在逆向中巧妙运用即可!
直接注入ws服务,替换编辑后的JS文件,如下所示:
h加密方法最终的返回值就是密文K,在其内部直接编写一个自执行函数,接受加密所需参数,再调用加密方法生成K参数值,这里我们甚至不需要去分析参数n的来源
编写Python调用代码,并在本地启动后刷新网页,代码实现如下:
import asyncio
import websocketsasync def receive_message(websocket):try:while True:send_text = input("请输入要加密的字符串:")if send_text == "exit":print("退出!")await websocket.send(send_text)breakelse:await websocket.send(send_text)response_text = await websocket.recv()print("加密结果:", response_text)except Exception as e:print("发生异常:", e)finally:await websocket.close()async def main():async with websockets.connect('ws://127.0.0.1:8765') as websocket:await receive_message(websocket)asyncio.run(main())
可以看到ws服务接受到提交参数后,调用加密方法成功拿到加密结果,如下所示:
上面RPC的方案,在ws服务开启后可把Python调用稍微修改一下做成一个API服务提供远端调用,这样爬虫可以直接并发调用加密
5. 算法还原
上面说到的RPC方案虽然可以绕过加密的这个问题,但是性能肯定是不能跟算法相比。能还原算法的大部分是不会去使用RPC方案的,一般场景都在短期内无法快速还原加密算法的情况下,会采用RPC作为一个临时的数据抓取方案
接下来,我们往下分析,去扣JS代码并还原加密算法!首先我们先找到n参数内d、k、l、num、s、sort是如何生成的!断点继续往下走,注意看!在K进行base64编码返回密文前的一行JS代码,如下所示:
来,就是这里!我们总结分析一下!上图几个参数的值跟n参数内的字段对应关系如下:
n = {“s”: s, “k”: d, “l”: f, “d”: 0, “sort”: “dd”, “num”: 10}
像上面三个动态生成的参数,参与加密一般也有可能是其他算法生成的、但是也可能是在前面某些接口内生成的,请求一次刷新一次最新的参数,所以可以先用搜索大法去验证一下,一般可能会有意想不到的结果,如下所示:
至此,参与加密的动态参数n就搞定了,每次请求之前请随便找个主页接口求一下把上面的动态参数拿出来即可!
接着继续,分析加密逻辑,现在已经是周六的凌晨3点了~一边回忆复盘加调试、一边记录到文章中
进入到Object(l.b)函数内部,这里提示一下各位新手朋友,像一些基本的调式技巧不熟悉的可以先学习一下,多看看大佬的视频或者文章,我觉得我一般都写的够细了,还有人私信问怎么操作?说看不懂!如下所示:
现在我们尝试将这个方法加上前面的加密函数h全部扣出来,进行替换,JS代码中写有注释!如下所示:
function m(e, n, o) {var d = "";// t.from自己定义一下,就一个编码方法n = t.from(n, "utf8"),o = t.from(o, "utf8");//使用NodeJS中内置经典加解密模块替换//var c = Object(r.createDecipheriv)("aes-128-cbc", n, o);//使用AES算法创建解密器var c = crypto.createDecipheriv("aes-128-cbc", n, o);//将十六进制编码的密文进行解密return d += c.update(e, "hex", "utf8"),d += c.final("utf8")
}function h(e, path, n, r) {var s = n.s, d = n.k, f = n.l, v = n.d, h = n.sort, k = n.num, y = function(content, t, e) {for (var a = Array.from(content), n = Array.from(t), r = a.length, o = n.length, d = String.fromCodePoint, i = 0; i < r; i++)a[i] = d(a[i].codePointAt(0) ^ n[(i + e) % o].codePointAt(0));return a.join("")}(function(s, t, path, e) {return [s, t, e, path].join("(&&)")}(function(t, e) {var n = c()(t);if (!_()(n)) {var r = [];for (var d in n)m()(n[d]) && "get" === e && (n[d] = n[d].join("")),"post" === e && (m()(n[d]) || o()(n[d])) && (n[d] = JSON.stringify(n[d])),r.push(n[d]);return r.sort(),r.join("")}// 这里我们先将Object(l.b)替换为上面的m函数}(e, r), parseInt((new Date).getTime() / 1e3) - 655876800 - v, path, h), m(s, d, f), k);//return t.from(y).toString("base64")//采用浏览原生函数编码方式,return btoa(y)
}
上面JS代码已经还原了90%,只差最后一步t.from,这个方法是干嘛的?断点继续调式,如下所示:
from方法接受两个参数,r默认空,t需要编码的字符串,e编码类型,默认UTF-8,来~用调式环境的数据编写一个from函数测试一下,跟浏览器一致,如下所示:
最后将浏览器环境请求参数跟动态参数n的数据丢给上面还原的加密算法,测试如下:
K加密结果与浏览器一致,至此结束!补环境的话代码量会更多,这个网站加密结构还是很清晰的
测试运行一下抓取评论接口数据,效果如下:
好了,到这里又到了跟大家说晚安的时候了。创作不易,帮忙点个赞再走吧。你的支持是我创作的动力,希望能带给大家更多优质的文章