对于某高考志愿信息网站写了一个爬虫, 遇到了一些问题, 在这里记录一下, 顺便学到了一些反爬虫的技巧.
获取每个学校的分数线情况, 首先在浏览器查看所有请求, 找到获取数据的请求, 但是可以发现他这个请求回来的某些数据应该是经过加密的.
这个解密代码一定在客户端, 因此我们找找, 看看能不能找到解密的代码. 首先我们添加请求断点, 看看能有什么突破.
接下来触发请求, 我们来看一下Call Stack
中的信息, 经过排查, 我们可以发现如下重要的函数:
通过查找源码, 我们找到上面的那个函数的源码, 这里代码没经过压缩和混淆, 因此可读性是真的好, 太良心了. 我们可以发现其中的两个关键的函数showNumber
和cnDeCrypt
, 猜测应该是这两个函数完成的对于参数的解密
直接在这些函数下断点, 来看看具体的调用, 我们可以发现showNumber
的代码非常简单, 这里就不解释了.
接下来是cnDeCrypt
, 这个函数看起来比较复杂, 实际上, 可以直接复制代码到控制台, 这个是可以直接执行的, 简单分析一下可以知道他调用了split()
和forEach
两个函数
在这里, 简单翻译一下这段代码吧, 到这里, 这个函数的作用就十分明显了.
var _cnDeCrypt = function (zlVjhiyMm1) {
var YB2 = "";
zlVjhiyMm1.split("|").forEach(function ($lvd3) {
if ($lvd3.search(/【(.*?)】/) !== -1) {
YB2 += $lvd3.replace('【', '').replace('】', '');
} else {
$lvd3 = $lvd3.replace(/[g-t]/ig, "");
YB2 += "\x26\x23\x78" + $lvd3 + "\x3b" }
});
return YB2
}
最后我们可以发现, 最终返回的是html编码, 但是这个和数据好像并不一样, 我们审查元素来看看, 发现其内容是看不懂的内容, 这里感觉应该是采用了字体加密
我们通过请求搜索一下字体文件, 我们可以发现如下的可疑文件, 利用FontEditor, 来查看一下.
首先打开数字来看看, 字符串和数字差不多, 在这里就不截图展示了.
这个手动写一个字典吧, 我没找到好的解决方案, 反正这个也不多, 有大佬有好的方案可以告诉我, 我是截图然后识别的.
from fontTools.ttLib import TTFontdef get_number_dict(): font = TTFont('./number_a.woff')
keys = font['glyf'].keys()
values = list(" 6920714538")
return dict((k, v) for k, v in zip(keys, values))def get_string_dict(): font = TTFont('./cn_5.woff')
keys = font['glyf'].keys()
values = list(
" 像艺包据印探及营命历曲服机境械磁飞食备植空武设水居油洋测纽统件软轻美丁下与世业丝中主义乌书事互交产人仪价休会伤伦估体作供侦俄保信修健儿光克党全公共关兵其军农准减出分划则别制刷力功加务动助劳化医华卫发古史司合告品商回国土地型培声大天女媒子学安定宝实审室家宾密小少尔展属嵌工市师广应康建开录形影律微德心情想感成战房技投护报拉控推播收放政教数文料斯无日时景智术材村来查正民气污河治法泰海源灾炸然照爆版物环班理生用画界疗监知石研种科秘移程税立筑算管类精纺组织经网职育能自航船英草葡萄行表装观规视计论评识译试语财质资路车轨轮运通造采量金鉴间非韩项预饰馆验高麻()0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
return dict((k, v) for k, v in zip(keys, values))
直接下载字体文件, 然后生成对应的字典. 对于数字和字符串解密的函数, 这里有两种方案:
def show_number(string="o6onqsf732|oipr72hef1|g6htsfn732"): code = """var showNumber = function(myNumbers) {
var ns = [];
myNumbers.split("|").forEach(function(value) {
value = value.replace(/[g-t]/ig, "");
ns.push(value);
});
return ns
}
""" ctx = execjs.compile(code)
return ctx.call('showNumber', string)def cn_decrypt(string="gmcl6ak5|gsc6gl74|c6nktgda"): code = """
var _cnDeCrypt = function (zlVjhiyMm1) {
var YB2 = "";
var string_list = [];
zlVjhiyMm1['\x73\x70\x6c\x69\x74']("\x7c")['\x66\x6f\x72\x45\x61\x63\x68'](function ($lvd3) {
if ($lvd3['\x73\x65\x61\x72\x63\x68'](/【(.*?)】/) != -1) {
YB2 += $lvd3['\x72\x65\x70\x6c\x61\x63\x65']("\u3010", "")['\x72\x65\x70\x6c\x61\x63\x65']("\u3011", "")
string_list.push($lvd3['\x72\x65\x70\x6c\x61\x63\x65']("\u3010", "")['\x72\x65\x70\x6c\x61\x63\x65']("\u3011", ""));
} else {
$lvd3 = $lvd3['\x72\x65\x70\x6c\x61\x63\x65'](/[g-t]/ig, "");
YB2 += "\x26\x23\x78" + $lvd3 + "\x3b"
string_list.push("\x26\x23\x78" + $lvd3 + "\x3b")
}
});
return string_list
}
""" ctx = js2py.eval_js(code)
string_list = ctx.call('_cnDeCrypt', string)
ret = '' string_dict = get_string_dict()
for string in string_list:
if string.startswith('&#x'):
ret += string_dict['uni' + string.replace('&#x', '').replace(';', '').upper()]
else:
ret += string
return ret
在这里, 我修改了部分js的代码, 便于python解析, 网站本身的代码我就不完整的贴上来了, 前面的截图都有. 在这里有一个坑, 如果用execjs
遇到转义字符会有问题, 当然这个代可以手动吧转义代码去掉, 或者用另一个库js2py
.
到这里, 返回数据的解密就完成了, 下面我们来看一看请求的参数是如何加密的.
采用之前的方案, 在url处下断点, 查看找到关键函数, 我们可以发现这里调用是youzyEpt
这个函数进行的加密
从源码中找到这个函数, 先来看看.
显然这里采用的AES进行的加密, 这么明显, 不过多解释了, 我们来看一下他的密钥是怎么得到的, 查看源码, 发现3个关键文件
读一下代码, 不难发现密钥是: [11, 23, 32, 43, 45, 46, 67, 8, 9, 10, 11, 12, 13, 14, 15, 16]
, 下面我们直接用Python写加密算法吧, 因为我不想引入js的aes加密算法了, 调用js还麻烦.
from Crypto.Cipher import AESdef params_encrypt(data): key = bytes([11, 23, 32, 43, 45, 46, 67, 8, 9, 10, 11, 12, 13, 14, 15, 16])
txt = str(json.dumps(data)).encode(encoding='utf-8')
ctr = Counter.new(128, initial_value=5)
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
data = cipher.decrypt(txt)
return b2a_hex(data).decode(encoding='utf-8'
写完之后, 让我们下断点测试一下代码是否正确.
对于密钥的获取, 如果不想读代码的话, 可以直接通过断点解决.
这样就完成了对于请求的加密和解密.
在这里讲一个小技巧, 如果请求的js
文件后面有类似于v=timestamps
等代码, 在source
下断点之后, 刷新页面断点会消失.
可以采用代{过}{滤}理软件将其代{过}{滤}理到本地, 然后硬编码debugger
这样就可以刷新也可以保持断点了. 在这里我用的Charles
, 找到对应的url, 右键map local
, 然后配置一下就可以了.
对于这个网站, 代码几乎没有混淆和压缩, 因此读起来还是十分愉悦的, 对于调试, 如果在请求js
的后面添加v=timestamps
等类似的东西的话, 下完断点之后, 重新请求会消失, 可以采用代{过}{滤}理软件, 把这些东西代{过}{滤}理到本地, 添加debugger
下断点来进行调试.