数月前写过某网站 (请原谅我的掩耳盗铃) 的爬虫, 这两天需要重新采集一次, 用的是 scrapy-Redis 框架, 本以为二次爬取可以轻松完成的, 可没想到爬虫启动没几秒, 出现了大堆的重试提示, 心里顿时就咯噔一下, 悠闲时光估计要结束了.
仔细分析后, 发现是获取店铺列表的请求出现问题, 通过浏览器抓包, 发现请求头参数中相比之前多了一个 X-Shard 和 x-uab 参数, 如下图所示:
X-Shard 倒是没什么问题, 一看就是兴趣点的经纬度, 但 x-uab 看过之后就让人心里苦了, JS 加密啊, 只能去逆向解密了.
2 JS 逆向求解
最直接的思路是根据 "x-uab" 关键字在所有关键中查找(Chrome 浏览器 - source 中按 ctrl + shift + F 快捷键), 结果如下所示:
接下来, 打个断点调试一下: 在数字那里点一下, 数字位置出现蓝点, 表示添加断点成功, 然后刷新获取店铺列表的页面, 程序会在断点处停下. 如下所示:
在控制台调试 o.getUA()函数, 看一下输出:
果然是, 证明猜测没错, 就是这个 o.getUA()函数负责生成请求头中的 x-uab 参数.
继续向下查看这个 getUA()函数的引用(把光标放在要查看的函数上, 就可以查看这个函数的引用), 就是下图这个函数:
图中的 s 就是我们要的 x-uab 参数, 下图在控制台输出可以证明:
所以, u-xab 是这里的 e 生成的, 而函数 e 传入的参数中, 第一个是常量 2, 第二个参数 a 是 undefined, 呵, 看起来没有传其它参数. 继续向下找这个 e(2,a)函数:
就是这个 function e(r, i, n, h, p) 方法, 直接运行可以获取加密后的参数. 把这个 function e(r, i, n, h, p) 方法全部代码取出来, 另存为一个 JS 文件.
3 撸代码
3.1 方案一
你以为上面找出生成 x-uab 的 JS 代码, 就大功告成了吗? 少年, you are too young too simple!
怎么把这段 JS 脚本运行起来, 才是关 (nan) 键(dian).
这个 function e(r, i, n, h, p) 函数有近 4 万行代码, 重新用 Python 实现难 (jiu) 度(shi)有 (bu) 点(ke)大(neng). 所以, 我选择直接用 Python 来执行这段 JS 脚本.
怎么用 python 执行 JS 脚本, 度娘会给你一堆资料, 自己查吧. 我这里选择的是 ExecJS.
因为在上面复制出来的脚本中, 只单单定义了一个 e(r, i, n, h, p)方法, 并没有调用这个方法, 所以, 我要要在 JS 文件的末尾添加一些代码来调用:
- function getParam() {
- var a;
- var param = e(2,a);
- return param
- };
然后, 开始撸 Python 代码吧:
- import ExecJS
- node = ExecJS.get()
- file = 'eleme.js'
- ctx = node.compile(open(file).read())
- js_encode = 'getParam()'
- params = ctx.eval(js_encode)
- print(params)
尝试执行, 心凉, 代码异常:
ExecJS._exceptions.ProgramError: TypeError: 'window' 未定义
Windows 对象估计是浏览器打开是创建的, 蕴含浏览器的信息, 所以用 Python 来执行这段代码时, 没有这个对西乡. 本来想尝试伪造 Windows 对象, 但查找之后发现 JS 脚本中上百个地方用到 Windows, 这还没完, 代码经过混淆, 在下水平不够, 没法追根溯源(这地方困扰了我许久, 哪位前辈如果知道方法, 请告知).
后来, 从一个前辈那里 (感谢前辈) 获知一个方法绕过去. 这个前辈的方法是将 ExecJS 的引擎换成 PhantomJS 这个无头浏览器(之前用的引擎是 node.JS), 换句话说就是用 PhantomJS 来执行 JS 脚本, PhantomJS 是一个浏览器, 自然就会创建 Windows 对象.
使用 PhantomJS
之前, 需要下载它的驱动 http://phantomjs.org/download.html , 然后放下 Python 代码统一目录下. 对之前的 Python 代码也进行修改:
- import ExecJS
- import os
- os.environ["EXECJS_RUNTIME"] = "PhantomJS"
- node = ExecJS.get()
- file = 'eleme.js'
- ctx = node.compile(open(file).read())
- js_encode = 'getParam()'
- params = ctx.eval(js_encode)
- print(params)
果然, 按照这个方法, 成功获取加密字符串.
3.2 方案二
事实上, 这个方案二才是我在出现未定义 Windows 对象异常后首先尝试的方法, 不过因为往 JS 代码中添加的 JS 脚本有问题, 以为行不通, 所以请教前辈, 得到了方案一.
方案二的思路和方案一类似, 不过更加粗暴一些. 不是因为没在浏览器执行, 造成没有 Windows 对象吗? 那我就模拟浏览器来执行.
在执行之前, 同样要修改 JS 脚本, 在 JS 文件末尾调用 e 方法, 添加如下代码:
- var a;
- var param = e(2,a);
- return param;
切记: 不要放在任何函数里面, 我之前就是因为将这段代码放在函数里头强制执行, 导致的结果就是在浏览器里可以获取加密字符串, 但是在 Python 中获取到的却是 None.
模拟浏览器用的 selenium 和 Chrome 的 webDriver, 代码如下:
- from selenium import webdriver
- browser = webdriver.Chrome(executable_path='chromedriver.exe')
- with open('eleme.js', 'r') as f:
- JS = f.read()
- print(browser.execute_script(JS))
这个方法也是可以获得加密之后的字符串.
最后, 有必要说一下的是, 如果需要获取大量的 x-uab, 采用方案二效率会高一下, 因为采用方案二的话, 可以自打开一个浏览器(都调用一个 webdriver 对象), 然后快速执行 JS, 返回加密字符串.
4 总结
一次 JS 逆向解密, 算是完成了吧. 但是也留下了一些问题:
(1)使用 Chrome 断点调试时, JS 脚本都是压缩混淆之后的, 通过 Chrome 的 pretty print 功能 (也就是说那对花括号) 可以格式美化, 但是, 有的时候却会失败, 就像下图, 格式化后, 还是一团糟:
这个问题耽搁了我很长时间, 没法调试啊!
(2)在下 JS 基础不行, 很困惑为什么运行时, 先通过 o.getUA()调用 e 函数内的嵌套函数, 然后 e 函数内部嵌套函数中调用 e 方法本身, 这是什么操作? 函数调用不都应该先外层函数, 然后再调用嵌套函数吗?
(3)如果不适用浏览器执行 JS 的方法, 就只能替换 Windows 对象, 这该如何操作?
(4)这个 e 函数有近 4 万行, 一个加密函数这么多代码, 我可不信, 里面肯定很多事混淆视听用的, 但我尝试调试追踪过, 只能说混淆之后让我无从追踪, 头晕. 怎么才能简化这段脚本呢?
如果哪位前辈可以解惑, 请一定告知, 不胜感激! 拜谢!
来源: https://www.cnblogs.com/chenhuabin/p/10946085.html