做的网站老被攻击,画网页,上海消费品网络营销推广公司,网页设计可以自学吗准备介绍#xff1a; 当我们学习完整个 JS 逆向技巧后#xff0c;这里是一次完整的分析爬取实战
案例介绍
本节案例网站不仅在 API 参数有加密#xff0c; 而且前端 JS 也带有压缩混淆#xff0c;其前端压缩打包工具使用 webpack , 混淆工具使用 javascript-obfuscator 。…准备介绍 当我们学习完整个 JS 逆向技巧后这里是一次完整的分析爬取实战
案例介绍
本节案例网站不仅在 API 参数有加密 而且前端 JS 也带有压缩混淆其前端压缩打包工具使用 webpack , 混淆工具使用 javascript-obfuscator 。 分析该网站需要熟练掌握浏览器的开发者使用工具和一定的调试技巧另外还需要用到一些 Hook 技术等辅助分析手段
案例网址 https://spa6.scrape.center 看着没什么不同点进去看一下每部电影的 URL 的变化 可以看到详情页的 URL 包含了一个长字符串看上去像是 Base64 编码
接下来看 Ajax 请求 我们从第一页到第十页依次点击一下看看 Ajax 请求的变化 可以看到 Ajax 接口的 URL 里多了一个 token 而且不同的页码 token 都是不一样的它们看上去同样是 Base64 编码的字符串
另外更困难的是这个接口还有时效性。 如果我们把 Ajax 接口的 URL 直接复制下来短期内可以访问但是过段时间就无法访问了会直接返回 401 状态码
我们这里把第一部电影的返回结果全部展开了 但是刚才我们观察到第一部电影的 URL 是 https://spa6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 看起来是 Base64 编码我们对其进行解码 结果为
ef34#teuq0btua#(-57w1q5o5--j98xygimlyfxs*-!i-0-mb1
看起来毫无规律这个解码后的结果怎么来的? 返回的结果里也并不包含这个字符串这又是怎么构造的
还有这仅仅是详情页的 URL 其真实数据是通过 Ajax 加载的 那么 Ajax 请求又是怎样的呢 这里我们发现其 Ajax 接口除了包含刚才所说的 URL 中携带的字符串又多了一个 token 同样也是类似 Base64 编码的内容总结下来这个网站就有如下特点
列表页的 Ajax 接口带有加密的 token
详情页的 URL 带有加密 id
详情页的 Ajax 接口参数带有加密 id 和加密的 token
如果我们想要通过接口的形式爬取 必须把这些加密 id 和 token 构造出来才行而且必须一步步来。首先我们要构造出列表页 Ajax 接口的 token 参数然后获取每部电影的数据信息接着根据数据信息构造出加密 id 和加密 token
到此为止我们知道了这个网站接口的加密情况下一步就是去找这个加密实现逻辑
由于是网页所以其加密逻辑一定藏在前端代码里但是前端为了保护其接口加密逻辑不被轻易的分析出来会采取压缩混淆等方式来加大分析难度。下面我们来看看这个网站的源代码和 JS 文件是怎样的
首先看网站源代码我们在网站上点击右键 此时会弹出快捷菜单然后点击查看源代码 !DOCTYPE htmlhtml langenheadmeta charsetutf-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width,initial-scale1link relicon href/favicon.icotitleScrape | Movie/titlelink href/css/chunk-19c920f8.2a6496e0.css relprefetchlink href/css/chunk-2f73b8f3.5b462e16.css relprefetchlink href/js/chunk-19c920f8.c3a1129d.js relprefetchlink href/js/chunk-2f73b8f3.8f2fc3cd.js relprefetchlink href/js/chunk-4dec7ef0.e4c2b130.js relprefetchlink href/css/app.ea9d802a.css relpreload asstylelink href/js/app.5ef0d454.js relpreload asscriptlink href/js/chunk-vendors.77daf991.js relpreload asscriptlink href/css/app.ea9d802a.css relstylesheet/headbodynoscriptstrongWere sorry but portal doesnt work properly without JavaScript enabled. Please enable it to continue./strong/noscriptdiv idapp/divscript src/js/chunk-vendors.77daf991.js/scriptscript src/js/app.5ef0d454.js/script/body/html 这是一个典型的 SPA单页 Web 应用页面其 JS 文件名带有编码字符 chunk, vendors 等关键字这是经过 webpack 打包压缩后的源代码目前主流起那段开发框架 Vue.js , React.js 等的输出结果都是类似这样的
接下来我们再看一下其 JS 代码是什么样子的。 在开发者工具中打开 Sources 选项卡下的 Page 选项卡 然后打开 js 文件夹 在这里我们能看到 JS 的源代码 我们随便复制一些看看什么样子的 (window[webpackJsonp] window[webpackJsonp] || [])[push]([[chunk-2f73b8f3], { 02f4: function(_0x22a2a8, _0xab6f51, _0x4245f8) { var _0x249496 _0x4245f8(4588) , _0x123c24 _0x4245f8(be13); _0x22a2a8[exports] function(_0x486f5a) { return function(_0x411fbe, _0x52029e) { var _0x51e1ce, _0xe9c0f4, _0x4b2912 String(_0x123c24(_0x411fbe)), _0x4d2326 _0x249496(_0x52029e), _0x110e8e _0x4b2912[length]; return _0x4d2326 0x0 || _0x4d2326 _0x110e8e ? _0x486f5a ? : void 0x0 : (_0x51e1ce _0x4b2912[charCodeAt](_0x4d2326), _0x51e1ce 0xd800 || _0x51e1ce 0xdbff || _0x4d2326 0x1 _0x110e8e || (_0xe9c0f4 _0x4b2912[charCodeAt](_0x4d2326 0x1)) 0xdc00 || _0xe9c0f4 0xdfff ? _0x486f5a ? _0x4b2912[charAt](_0x4d2326) : _0x51e1ce : _0x486f5a ? _0x4b2912[slice](_0x4d2326, _0x4d2326 0x2) : _0xe9c0f4 - 0xdc00 (_0x51e1ce - 0xd800 0xa) 0x10000); } ; } 可以看到一些变量是十六进制字符串而且代码被压缩了
没错我们就是要从这里找出 token 和 id 的构造逻辑
寻找列表页 Ajax 入口
这里简单介绍两种寻找入口的方法
全局搜索标志字符串
设置 Ajax 断点
全局搜索标志字符串
一些关键的字符串通常会被作为寻找 JS 混淆入口的依据我们可以通过全局搜索的方式来查找然后根据搜索到的结果答题观察入口是否为我们想找的入口
重新打开 Ajax 接口 看一下请求的 Ajax 接口 这里 Ajax 接口的 URL 为 https://spa6.scrape.center/api/movie/?limit10offset0tokenZjI3YjI1NDRlYjM2NjFkNWNjM2M0MGIzYjZkY2UwMmJhNTgxYWM3ZSwxNzIzNDU2NDE1 可以看到带有 limit . offset , token 三个参数关键就是找 token 我们就全局搜索是否存在 token 点击开发者右上角的“三个小竖点” 然后点击 Search 这样我们就进入了全局搜索模式搜索 token,可以看到搜索到了几个结果 观察一下下面两个结果可能是我们想要的点击第一个进入看看此时定位到一个 JS 文件 如果是一行就点击左下角的 { } 进行格式化然后我们定位到 token ,可以看到这里有 limit , offset , token 。然后观察其他逻辑基本上能够确定这就是构造 Ajax 请求的地方如果不是的话可以继续搜索其他文件观察
设置 Ajax 断点
由于这里的字符串 token 并没有被混淆 所以上面的方法是奏效的。因为这种字符串非常容易称为寻找入口的依据所以这样的字符串也会被混淆成类似 Unicode , Base64 , RC4 等的编码形式这样我们就不能轻松的搜索到了
另外我们也可以通过 XHR 断点方便的找到发起 Ajax 请求的一些入口位置
我们可以在 Sources 选项卡右侧 XHR/fetch Breakpoints 处添加一个断点首先点击 号 此时就会让我们输入匹配的 URL 内容由于 Ajax 接口的形式是 /api/movie/?limit10..... 这样的格式所以截取一段填进去就好了这里填的就是 /api/movie 然后重新刷新页面就进入了断点模式 如果代码又变成一行我们还是点击 { } 格式化代码找到断点位置这里可以看到有一个 send 字符我们可以初步猜测它相当于发送 Ajax 请求的一瞬间
这里我们来回溯查找相关逻辑点击 Call Stack 这里记录了 JS 方法的逐层调用过程 当前指向的是一个名为 anonymous 也就是匿名的调用在他下面显示了调用 anonymous 的方法名叫作 _0x29474e 的方法然后 _0x29474e 下面的方法又是调用 _0x29474e 的一次类推。我们可以继续找下去观察类似 token 这样的信息就能找到对应的位置了。
最后我们找到了 onFetchData 这个方法实现了 token的构造逻辑这样就成功找到了 token 的参数构造位置了 到此为止我们就通过两个方法找到入口了其实还有其他寻找入口的方法比如 Hook 关键函数等
寻找列表页加密逻辑
我们已经找到 token 的位置了 可以观察这个 token 对应的变量它叫作 _0x263439 所以关键就要看这个变量哪里来的
怎么找呢? 加断点就好了
看一下变量在哪里生成的然后我们在对应的行添加断点 我们先取消刚才打的 XHR 断点 这时我们就设置了一个断点由于只有一个断点刷新网页之后我们会发现网页停在新断点上 这时我们就能观察到正在运行的一些变量了比如把鼠标放在各个变量上可以看到变量的值和类型把鼠标放在 _0x2fa7bd 上会有一个浮窗提示 另外还可以在右侧的 Watch 面板中添加想要查看的变量这行代码的内容如下 , _0x263439 Object(_0x2fa7bd[a])(this[$store][state][url][index]); 我们比较感兴趣的可能是 _0x2fa7bd 和 this 里面的 $store 属性。 展开 Watch 面板 然后点击 号 把想看到的变量添加到 Watch 面板中 可以发现 _0x2fa7bd 是一个对象 它具有属性 a 其值是一个方法。
this[$store][state][url][index] 的值其实就是 /api/movie 即 Ajax 请求 URL 的 Path 。 _0x263439 就是调用前者的方法传入 /api/movie 得到的
下一步就是去寻找这个方法。我们可以把 Watch 面板的 _0x2fa7bd 展开这里会显示 FunctionLocation 就是这个函数代码的位置 点击进入这时我们就进入一个新的名字为 _0x456254 的方法里在这个方法里应该就有 token 的生成逻辑了。添加断点然后点击右上方的 Resume scrpt execution 按钮 这时我们会发现单步执行到 for (var _0x5da681 Math[round](new Date()[getTime]
位置接下来我们不断进行单步调试观察一下里面的执行逻辑和每一步调试的结果有什么变化在每步的执行过程中我们可以发现一些运行值被打到代码右侧并高亮表示这里教程说的是在 Watch 下面会有每步的结果不过我的并没有任何结果但是在 Scope 下面倒是有一些数据的结果 Watch 下面并没有任何数据 最后我们总结出这个 token 的构造逻辑
传入 /api/movie 会构造一个初始化列表将变量命名为 _0x31a891
获取当前时间戳命名为 _0x5da681调用 push 方法将其添加到 _0x31a891 变量代表的列表中
将 _0x31a891 变量用 拼接然后进行 SHAI 编码命名为 _0xf7c3c7
将 _0xf7c3c7 SHAI 编码结果 和 _0x5da681时间戳用逗号拼接命名为 _0x3c8435
将 _0x3c8435 进行 Base64 编码命名为 _0x104b5b 得到最后的 token 经过反复观察可以得出以上逻辑其中变量可以实时查看同时也可以自己输入到控制台验证
现在加密逻辑分析出来了基本思路是
将 /api/movie 放到一个列表中
在列表中加入当前时间戳
将列表内容用逗号拼接
将拼接结果进行 SHAI 编码
将编码结果和时间戳再次拼接
将拼接后的结果进行 Base64 编码
使用 Python 实现列表页爬取
要用 Python 实现这个逻辑我们需要借助两个库 一个是 hashlib ,它提供了 sha1 方法另一个是 base64 它提供了 b64encode 方法对结果进行 Base64 编码 import hashlib
import time
import base64
from typing import List, Any
import requestsINDEX_URL https://spa6.scrape.center/api/movie/?limit{limit}offset{offset}token{token}
LIMIT 10
OFFSET 0def get_token(args: List[Any]):timestamp str(int(time.time()))args.append(timestamp)sign hashlib.sha1(,.join(args).encode(utf-8)).hexdigest()return base64.b64encode(,.join([sign, timestamp]).encode(utf-8)).decode(utf-8)args [/api/movie]
token get_token(argsargs)
index_url INDEX_URL.format(limitLIMIT, offsetOFFSET, tokentoken)
response requests.get(index_url)
print(response, response.json()) 部分输出结果 response {count: 103, results: [{id: 1, name: 霸王别姬, alias: Farewell My Concubine, cover: https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg464w_644h_1e_1c, categories: [剧情, 爱情], published_at: 1993-07-26, minute: 171, score: 9.5, regions: [中国内地, 中国香港]}, {id: 2, name: 这个杀手不太冷, alias: Léon, cover: https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg464w_644h_1e_1c, categories: [动画, 歌舞, 冒险], published_at: 1995-07-15, minute: 89, score: 9.0, regions: [美国]}]} 寻找详情页 id 入口
我们观察前面的输出结果
这里我们看到有个 id 是 1 另外还有一些其他字段 如电影名称封面类别等这里面一定有某个信息是用来唯一区分电影的
但是当我们点击第一部电影的 信息时可以看到它跳转了 URL 为 https://spa6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 的页面可以看到这里的 URL 里面有一个加密 id 为 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
它和电影信息有什么关系呢
如果仔细观察其实可以找出规律来但是这总归是观察出来的如果遇到一些观察不出来规律的那就很麻烦了。因此还是要靠技巧去找到它真正的加密位置。这时候该怎么办
分析一下这个加密 id 怎么生成的。
点击详情页的时候我们就可以看到它访问的 URL 里面就带上了 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 这个加密 id 了。而且不同详情页的加密 id 是不同的这说明这个加密 id 的构造依赖于列表页 Ajax 的返回结果。因此可以确定这个加密 id 的生成发生在 Ajax 请求完成后或点击详情页的一瞬间 为了进一步确定发生在何时我们查看页面源码可以看到在没点击前详情页的 href 里面就已经带有加密 id 了 由此可以肯定这个加密在 Ajax 请求完成之后生成的而且肯定也是由 JS 生成的。
怎么去找 Ajax 完成之后的事件呢 是否应该去找 Ajax 完成之后的事件呢
可以试试。 在 Sources 面板的右侧 有一个 Event Listener Breakpoints 这里有一个 XHR 的监听包括发起时成功后发生错误时的一些监听 这里我们勾选上 readystatechange 事件代表 Ajax 得到响应时的事件其他断点都可以删除了然后刷新一下页面 可以看到此时就停在 Ajax 得到响应时的位置了。我们怎么知道这个 id 是怎么加密的呢可以通过断点一步步调试下去但这个过程非常繁琐因为这里可能会逐渐用到页面 UI 渲染的一些底层实现甚至可能找着找着都不知道找到哪里去了
怎么办呢这里我们就可以使用 Hook 这个加密 id 是一个 base64 编码的字符串那么生成过程中想必调用了 JS 的 Base64 编码的方法。这个方法叫做 btoa 。当然 Base64 也有其他实现方法比如利用 crypto-js 库实现可能底层调用的就不是 btoa 方法了
现在我们其实并不确定是不是通过调用 btoa 方法实现的 Base64 编码 那就先试试
要实现 Hook 关键在于将原来的方法改写 这里我们其实就是 Hook btoa 这个方法了 btoa 这个方法属于 window 对选哪个这里直接改写 window 对象的 btoa 方法即可 (function (){use strictfunction hook(object, attr){var func object[attr]object[attr] function (){console.log(hooked, object, attr, arguments)var ret func.apply(object, arguments)debuggerconsole.log(result, ret)return ret}}hook(window, btoa)
})() 这里我们定义了一个 hook 方法给其传入 object 和 attr 参数 意思就是 Hook object 对象的 attr 参数例如如果我们想 Hook alert 方法那就把 object 设置为 window 把 attr 设置为 alert 。这里我们想要 Hook Base64 的编码方法 所以只需要 Hook window 对象的 btoa 方法就可以了
hook 方法的第一句 var func object[attr] 相当于把它赋值为一个变量我们调用 func 方法就可以实现和原来相同的功能。然后我们改写这个方法的定义将其改成一个新方法。在新的方法中通过 func.apply 方法有重新调用了原来的方法。这样我们可以保证前后方法的执行效果不受影响的前提下在 func 方法执行的前后加入自己的代码如使用 console.log 将信息输出到控制台通过 debugger 进入断点等。在这个过程中我们先临时保存 func 方法 然后定义一个新方法来接管程序控制权在其中自定义我们想要的实现同时新方法重新调回 func 方法保证前后结果不受影响。因此我们达到了在不影响原有方法效果的前提下可以实现在方法的前后实现自定义的功能就是 Hook 的完整实现过程
最后我们调用 hook 方法传入 window 对象和 btoa 字符串即可
怎么注入这个代码呢
控制台注入
重写 JS
Tampermonkey 注入
控制台注入
对于我们这个场景控制台注入其实就够了我们先来介绍这个方法就是直接在控制台输入这行代码并运行即可
首先我们将页面恢复到最初的状态 然后打开控制台输入前面的那段代码 回车 执行完这段代码之后就相当于我们已经把 window 的 btoa 方法改写了然后在控制台输入 btoa(germey) 回车 可以看到它进入了我们自定义的 debugger 的位置停下来了我们把断点向下执行然后点击 Resume script execution 按钮 就可以看到控制台输出了一些对应的结果 我们通过 Hook 的方式改写了 btoa 方法 使其每次在调用的时候都能停到一个断点同时还能输出对应的结果
接下来怎么用 hook 找到对应的加密 id 入口?
由于此时我们是控制台直接输入的 Hook 代码所以页面刷新就无效了。但我们这个网站是 SPA 页面点击详情页的时候是不会刷新整个页面的因此这段代码依然生效。如果不是 SPA 页面即每次访问都需要刷新页面网站那么这种注入方式就不生效了
我们想要 Hook 列表页 Ajax 加载完成后的逻辑 对应的就是加密 id 的 Base64 编码过程 怎样在不刷新页面的情况下复现这个操作呢 很简单点击下一页就好了
这时候点击第二页的按钮可以看到它确实再次停到了 Hook 方法的 debugger 处。 由于列表页的 Ajax 和 加密 id 都带有 Base64 编码操作所以都能 Hook 到。接着官产对应的 Arguments 或当前网站的行为 或者观察栈信息我们就能大体知道现在走到哪个位置了 从而进一步通过栈的调用信息找到调用 Base64 编码的位置
根据调用栈的信息我们可以观察这些变量是在哪一层发生变化。比如对于最后一层我们可以很明显看到它执行了 Base64 编码编码前的结果是
ef34#teuq0btua#(-57w1q5o5--j98xygimlyfxs*-!i-0-mb1 编码后的结果
ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 控制台输出的结果 那么核心问题来了编码前的
ef34#teuq0btua#(-57w1q5o5--j98xygimlyfxs*-!i-0-mb1
是怎么来的 我们展开栈信息一层层看这个字符串的变化情况如果不变就看下一层如果改变了就停下来细看。最后在第五层找到了它的变化过程 var _0x11a046 ef34#teuq0btua#(-57w1q5o5--j98xygimlyfxs*-!i-0-mb 是一个写死的字符串然后和 _0x177944 拼接形成了最后的字符串那么 _0x177944 是怎么来的继续向下看 可以看到 Ajax 返回结果的单个电影信息的 id
因此这个逻辑就清楚了就是 固定字符串 ef34#teuq0btua#(-57w1q5o5--j98xygimlyfxs*-!i-0-mb 加上电影 id 然后进行 Base64 编码即可
在控制台注入的不好之处在于页面刷新就失效了而代码必须在页面加载完才能注入所以并不能在一开始生效。
重写 JS
借助 Chrome 浏览器的 Overrides 功能我们可以实现某些 JS 文件的重写和保存。 Overrides 会在本地生成一个 JS 文件副本 以后每次刷新都会使用副本内容
这里我们需要切换到 Sources 面板中的 Overrides 选项卡然后选择一个文件夹比如这里我自定义了一个 ChromeOverrides 文件夹 然后随便选择一个 JS 脚本在后面贴上这段注入的脚本保存文件如果页面崩溃刷新一下页面就好了 同时我们还注意到目前直接进入到了断点模式并且成功 Hook 到了 btoa 方法。
其实 Overrides 的功能很有用有了它我们可以持久化保存任意修改的 JS 代码想在哪里修改都可以甚至可以直接修改 JS 的执行逻辑
Tampermonkey 注入
如果不想使用 Overrides 的方式注入我们也可以使用 Tampeermonkey 插件来注入
开始之前先关闭所有断点和 刚才的 Overrides 功能以防干扰 Tampermonkey 的安装和简单使用
写文章-CSDN创作中心
我们可以将脚本内容改写成下面这样 // UserScript // name HookBase64 // namespace https://scrape.center/ // version 0.1 // description Hook Base64 encode function // author Germey // match https://spa6.scrape.center/ // grant none // run-at document-start // /UserScript (function() { use strict; function hook(object, attr){ var func object[attr] console.log(func, func) object[attr] function(){ console.log(hooked, object, attr) var ret func.apply(object, arguments) debugger return ret } } hook(window, btoa) })() 这时候启动脚本重新刷新页面可以发现成功 Hook btoa 方法 寻找详情页 Ajax 的 token
现在我们已经找到详情页的加密 id 了但是还差一步其 Ajax 请求也有一个 token 因为也是 Ajax 请求我们可以通过前面提到的同样的方法对该 token 的生成逻辑进行分析最终可以发现其实这个 token 和详情页 token 的构造逻辑是一样的
使用 Python 实现详情页的爬取
现在我们已经成功把详情页的加密 id 和 Ajax 请求的 token 找出来了下一步就是使用 Python 完成爬取这里我们只实现第一页的爬取 import hashlib
import time
import base64
from typing import List, Any
import requestsINDEX_URL https://spa6.scrape.center/api/movie/?limit{limit}offset{offset}token{token}
DETAIL_URL https://spa6.scrape.center/api/movie/{id}?token{token}
LIMIT 10
OFFSET 0
SECRET ef34#teuq0btua#(-57w1q5o5--j98xygimlyfxs*-!i-0-mbdef get_token(args: List[Any]):timestamp str(int(time.time()))args.append(timestamp)sign hashlib.sha1(,.join(args).encode(utf-8)).hexdigest()return base64.b64encode(,.join([sign, timestamp]).encode(utf-8)).decode(utf-8)args [/api/movie]
token get_token(argsargs)
index_url INDEX_URL.format(limitLIMIT, offsetOFFSET, tokentoken)
response requests.get(index_url)
print(response, response.json())result response.json()
for item in result[results]:id item[id]encrypt_id base64.b64encode((SECRET str(id)).encode(utf-8)).decode(utf-8)args [f/api/movie/{encrypt_id}]token get_token(argsargs)detail_url DETAIL_URL.format(idencrypt_id, tokentoken)response requests.get(detail_url)print(response, response.json())