对某N博客网站的反爬虫机制分析
偶尔用手机搜索CSDN文章的时候,广告太多了动不动就打开淘宝支付宝,因此就想做一个简单的代{过}{滤}理APP,代{过}{滤}理CSDN仅显示博客正文。遇到了js反爬机制,在这里简单分析下。友情提示,小白一枚,务必轻喷~
反爬虫机制 - 简单介绍
1. 在没有携带cookie的情况下访问CSDN不会返回html页面,返回的是一串混淆的js代码,如下图。
2. 如果对这串代码直接进行调试,会一直弹出新的调试窗口【反调试机制】;
分析思路
1. 简单还原编码的js代码
因为网上的js解密,会导致复制下来的js代码报错【原因是没有对特殊字符如 “'”、“\”等进行处理】,因此自己写了一段python代码进行编码转换
def reverseJS():
def b2str(matched):
Char = chr(int(matched.group(1),16))
if Char in ["'", "\\",'"']: # 对特殊字符进行处理
Char = '\\' + Char
return Char # 返回就是将匹配到的字符替换完毕的字符
with open("csdnres.html", "r+") as f:
c = "".join(f.readlines())
res = re.sub(r"\\x(\d[a-zA-Z0-9])", b2str, c) # 最终返回替换好的字符串
with open("utf8.txt", "w+") as f:
f.write(res)
【坑一】:就是一定要先将js代码进行格式化之后再进行编码转换,不然转换之后的js代码不能进行格式化了。
【坑二】:对\字符进行处理,导致js的正则出错。
2. 分析代码:格式化之后的代码有300多行,这里就不贴了,有兴趣的可以自己下载一下
分析代码发现,js共有三个入口点,即开始渲染网页就会被执行的代码。
入口点0 => 反调试函数_0x4db1c(),代码如下:
_0x4db1c();
setInterval(function() {
_0x4db1c();
}, 0xfa0);
var _0x4db1c = function() {
function _0x355d23(_0x450614) {
if (('' + _0x450614 / _0x450614)[_0x55f3('0x1c', 'V2KE')] !== 0x1 || _0x450614 % 0x14 === 0x0) {
(function() {}
[_0x55f3('0x1d', 'CNUY')]((undefined + '')[0x2] + (true + '')[0x3] + ([][_0x55f3('0x1e', 'w8PR')]() + '')[0x2] + (undefined + '')[0x0] + (![] + [0x0] + String)[0x14] + (![] + [0x0] + String)[0x14] + (true + '')[0x3] + (true + '')[0x1])());
// 这个地方会触发新的debug窗口
} else {
(function() {}
['constructor']((undefined + '')[0x2] + (true + '')[0x3] + ([][_0x55f3('0x1f', 'L$(D')]() + '')[0x2] + (undefined + '')[0x0] + (![] + [0x0] + String)[0x14] + (![] + [0x0] + String)[0x14] + (true + '')[0x3] + (true + '')[0x1])());
}
_0x355d23(++_0x450614);
}
try {
_0x355d23(0x0);
} catch (_0x54c483) {}
};
简单分析代码流程:
- js开始渲染会触发_0x4db1c函数,并且设置了定时器,每隔0xfa0执行一次这个函数
- 代码执行到注释处会触发debug窗口,接着对debug机制进行分析,将debug函数拆解一下如下;
- 这段代码就是不断的弹出debu窗口,直到异常,然后被捕获,返回空
// if处:
(function() {}
[_0x55f3('0x1d', 'CNUY')](
(undefined + '')[0x2] \ //字符串d
+ (true + '')[0x3] \ //字符串e
+ ([][_0x55f3('0x1e', 'w8PR')]() + '')[0x2] \
+ (undefined + '')[0x0] \ //字符串u
+ (![] + [0x0] + String)[0x14] \ //字符串g, 自此猜测整个字符串为debugger, else处的代码也是如此
+ (![] + [0x0] + String)[0x14] \
+ (true + '')[0x3] \
+ (true + '')[0x1])
()
- 同时代码中还有一个_0x55f3函数,省略代码如下,对代码进行分析,发现其实是一个RC4算法,且接收的第一个参数是在js代码开头的数组【经过变换的,后面会解释】,第二个参数是密钥。在知道参数的情况下,用python模拟RC4算法,可以将其解密成原始字符串。
var _0x55f3 = function(_0x4c97f0, _0x1742fd) {
var _0x4c97f0 = parseInt(_0x4c97f0, 0x10);
var _0x48181e = _0x4818[_0x4c97f0]; //代码开头的字符串数组
// 。。。。。。省略大段代码
_0x48181e = _0x55f3['rc4'](_0x48181e, _0x1742fd);
_0x55f3['data'][_0x4c97f0] = _0x48181e;
} else {
_0x48181e = _0x55f3['data'][_0x4c97f0];
}
return _0x48181e;
};
过反调试机制:
0. 将js文件down到本地进行分析,我怕影响js中的变量的值,因此没有采用这种方法。【显然,怪我想太多~】
1. 使用burp,拦截返回的数据包,由上面分析将_0x4db1c函数 直接返回空即可。关于如何拦截返回数据包,看下图~
入口点1 => 开头的匿名函数,代码如下
(function(_0x4c97f0, _0x1742fd) {
var _0x4db1c = function(_0x48181e) {
while (--_0x48181e) {
_0x4c97f0['push'](_0x4c97f0['shift']());
}
};
var _0x3cd6c6 = function() {
var _0xb8360b = {
'data': {
'key': 'cookie',
'value': 'timeout'
},
'setCookie': function(_0x20bf34, _0x3e840e, _0x5693d3, _0x5e8b26) {
_0x5e8b26 = _0x5e8b26 || {};
var _0xba82f0 = _0x3e840e + '=' + _0x5693d3;
var _0x5afe31 = 0x0;
for (var _0x5afe31 = 0x0, _0x178627 = _0x20bf34['length']; _0x5afe31 < _0x178627; _0x5afe31++) {
var _0x41b2ff = _0x20bf34[_0x5afe31];
_0xba82f0 += '; ' + _0x41b2ff;
var _0xd79219 = _0x20bf34[_0x41b2ff];
_0x20bf34['push'](_0xd79219);
_0x178627 = _0x20bf34['length'];
if (_0xd79219 !== !![]) {
_0xba82f0 += '=' + _0xd79219;
}
}
_0x5e8b26['cookie'] = _0xba82f0;
},
'removeCookie': function() {
return 'dev';
},
'getCookie': function(_0x4a11fe, _0x189946) {
_0x4a11fe = _0x4a11fe || function(_0x6259a2) {
return _0x6259a2;
};
var _0x25af93 = _0x4a11fe(new RegExp('(?:^|; )' + _0x189946['replace'](/([.$?*|{}()[]\/+^])/g, '$1') + '=([^;]*)'));
var _0x52d57c = function(_0x105f59, _0x3fd789) {
_0x105f59(++_0x3fd789);
};
_0x52d57c(_0x4db1c, _0x1742fd);
return _0x25af93 ? decodeURIComponent(_0x25af93[0x1]) : undefined;
}
};
var _0x4a2aed = function() {
var _0x124d17 = new RegExp('\\w+ *\\(\\) *{\\w+ *[\'|\"].+[\'|\"];? *}');
return _0x124d17['test'](_0xb8360b['removeCookie']['toString']());
};
_0xb8360b['updateCookie'] = _0x4a2aed;
var _0x2d67ec = '';
var _0x120551 = _0xb8360b['updateCookie']();
if (!_0x120551) {
_0xb8360b['setCookie'](['*'], 'counter', 0x1);
} else if (_0x120551) {
_0x2d67ec = _0xb8360b['getCookie'](null, 'counter');
} else {
_0xb8360b['removeCookie']();
}
};
_0x3cd6c6();
}(_0x4818, 0x15b)); //_0x4818 是一个长度为56的经base64编码的字符串数组,具体文件爬取csdn可获得
简单分析代码流程:
-
函数执行流程:
- 匿名函数 =>
- _0x3cd6c6函数 =>
- 依据_0x4a2aed函数的返回值(false)执行【触发坑二,导致分析半天】 =>
- _0xb8360b['getCookie']函数;
-
对 _0xb8360b['getCookie']函数进行分析,本质上执行的方法为:_0x52d57c(_0x4db1c, _0x1742fd)
- _0x1742fd 是匿名函数的参数
- _0x4db1c 是匿名函数最前面的函数
-
对_0x4db1c函数进行分析,其实就是执行了js中数组的两个方法:shift和push方法
- shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
- push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。
- 将参数带入函数_0x4db1c,其实就是将_0x4818数组向前推进13位,
-
综上得到经过变换的字符串数组,结合R4算法,可以对整个数组进行解密。
入口点2 => 开头的匿名函数,代码如下
if (function() {
// 。。。。。省略代码
_0x5b6351();
try {
// return !!window['addEventListener'];
return true;
} catch (_0x35538d) {
// return ![];
return false;
}
}())
{
document[_0x55f3('0x33', 'V%YR')](_0x55f3('0x34', 'yApz'), l, ![]);
} else {
document[_0x55f3('0x36', 'yApz')](_0x55f3('0x37', 'L$(D'), l);
}
简单分析代码流程:
- 直接分析Ture还是False, 将0x33、0x34、0x36、0x37解密得到:addEventListener、DOMContentLoaded、attachEvent、onreadystatechange,粗糙一点的意思就是,当页面加载完了,开始执行
l函数
- DOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发
- onreadystatechange等同于onload事件
重点函数 => l函数,代码如下
var l = function() {
while (window[_0x55f3('0x1', 'XMW^')] || window['__phantomas']) {};
var _0x5e8b26 = _0x55f3('0x3', 'jS1Y'); //字符串 3000176000856006061501533003690027800375
String[_0x55f3('0x5', 'n]fR')][_0x55f3('0x6', 'Pg54')] = function(_0x4e08d8) {
// _0x55f3('0x5', 'n]fR') => prototype
// _0x55f3('0x6', 'Pg54')] => unsbox
var _0x5a5d3b = '';
for (var _0xe89588 = 0x0; _0xe89588 < this[_0x55f3('0x8', ')hRc')] && _0xe89588 < _0x4e08d8[_0x55f3('0xa', 'jE&^')]; _0xe89588 += 0x2) {
var _0x401af1 = parseInt(this[_0x55f3('0xb', 'V2KE')](_0xe89588, _0xe89588 + 0x2), 0x10);
var _0x105f59 = parseInt(_0x4e08d8[_0x55f3('0xd', 'XMW^')](_0xe89588, _0xe89588 + 0x2), 0x10);
var _0x189e2c = (_0x401af1 ^ _0x105f59)[_0x55f3('0xf', 'W1FE')](0x10);
if (_0x189e2c[_0x55f3('0x11', 'MGrv')] == 0x1) {
_0x189e2c = '0' + _0x189e2c;
}
_0x5a5d3b += _0x189e2c;
}
return _0x5a5d3b;
}
;
String['prototype'][_0x55f3('0x14', 'Z*DM')] = function() {
// _0x55f3('0x14', 'Z*DM') => hexXor
var _0x4b082b = [0xf, 0x23, 0x1d, 0x18, 0x21, 0x10, 0x1, 0x26, 0xa, 0x9, 0x13, 0x1f, 0x28, 0x1b, 0x16, 0x17, 0x19, 0xd, 0x6, 0xb, 0x27, 0x12, 0x14, 0x8, 0xe, 0x15, 0x20, 0x1a, 0x2, 0x1e, 0x7, 0x4, 0x11, 0x5, 0x3, 0x1c, 0x22, 0x25, 0xc, 0x24];
var _0x4da0dc = [];
var _0x12605e = '';
for (var _0x20a7bf = 0x0; _0x20a7bf < this['length']; _0x20a7bf++) {
var _0x385ee3 = this[_0x20a7bf];
for (var _0x217721 = 0x0; _0x217721 < _0x4b082b[_0x55f3('0x16', 'aH*N')]; _0x217721++) {
if (_0x4b082b[_0x217721] == _0x20a7bf + 0x1) {
_0x4da0dc[_0x217721] = _0x385ee3;
}
}
}
_0x12605e = _0x4da0dc['join']('');
return _0x12605e;
}
;
var _0x23a392 = arg1[_0x55f3('0x19', 'Pg54')](); // arg1为59F9A12F9BD5A868694981F035E39B5359D10E27【在原始js代码中给出】, _0x55f3('0x19', 'Pg54') 为unsbox
arg2 = _0x23a392[_0x55f3('0x1b', 'z5O&')](_0x5e8b26); // _0x55f3('0x1b', 'z5O&')为hexXor
setTimeout('reload(arg2)', 0x66a);
};
简单分析代码流程:
- RC4解密后的字符串已在代码注释中
- 两个String开头的函数,其实就是在给String原型对象增加两个方法: unsbox, hexXor
- 代码:
var _0x23a392 = arg1[_0x55f3('0x19', 'Pg54')](); arg2 = _0x23a392[_0x55f3('0x1b', 'z5O&')](_0x5e8b26);
其实就是调用arg1的unsbox函数再对结果进行hexXor函数转换,即hexXor(unsbox(arg1))
- 两个函数,从名字就可以知道是啥意思了,依据原始的js代码简单还原的python代码如下
def unsbox(arg1 = "59F9A12F9BD5A868694981F035E39B5359D10E27"):
box = [0xf, 0x23, 0x1d, 0x18, 0x21, 0x10, 0x1, 0x26, 0xa, 0x9, 0x13, 0x1f, 0x28, 0x1b, 0x16, 0x17, 0x19, 0xd, 0x6, 0xb, 0x27, 0x12, 0x14, 0x8, 0xe, 0x15, 0x20, 0x1a, 0x2, 0x1e, 0x7, 0x4, 0x11, 0x5, 0x3, 0x1c, 0x22, 0x25, 0xc, 0x24]
res = list(range(0, len(arg1)))
for i in range(0, len(arg1)):
j = arg1[i]
for k in range(0, 40):
if box[k] == i+1:
res[k] = j
res = "".join(res)
return res
def hexXor(arg2 = "6D90585EB9457E1F3A1D299F88359B296AF39051"):
box = "3000176000856006061501533003690027800375"
res = ""
for i in range(0, 40, 2):
arg_H = int(arg2[i:i+2], 16)
box_H = int(box[i:i+2], 16)
res += hex(arg_H ^ box_H)[2:].zfill(2)
print(res)
至此,整个js文件分析完毕
总结
总结啥呀总结,这么简单还要总结~ 还是要说明一下,小白一枚,务必轻喷~
没了。