对某N博客网站的反爬虫机制分析
## 对某N博客网站的反爬虫机制分析偶尔用手机搜索CSDN文章的时候,广告太多了动不动就打开淘宝支付宝,因此就想做一个简单的代{过}{滤}理APP,代{过}{滤}理CSDN仅显示博客正文。遇到了js反爬机制,在这里简单分析下。友情提示,小白一枚,务必轻喷~
### 反爬虫机制 - 简单介绍
1. 在没有携带cookie的情况下访问CSDN不会返回html页面,返回的是一串混淆的js代码,如下图。
2. 如果对这串代码直接进行调试,会一直弹出新的调试窗口【反调试机制】;
### 分析思路
#### 1. 简单还原编码的js代码
因为网上的js解密,会导致复制下来的js代码报错【原因是没有对特殊字符如 “'”、“\”等进行处理】,因此自己写了一段python代码进行编码转换
```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)", b2str, c) # 最终返回替换好的字符串
with open("utf8.txt", "w+") as f:
f.write(res)
```
【坑一】:就是一定要先将js代码进行格式化之后再进行编码转换,不然转换之后的js代码不能进行格式化了。
【坑二】:对\字符进行处理,导致js的正则出错。
#### 2. 分析代码:格式化之后的代码有300多行,这里就不贴了,有兴趣的可以自己下载一下
分析代码发现,js共有三个入口点,即开始渲染网页就会被执行的代码。
##### 入口点0 => 反调试函数_0x4db1c(),代码如下:
```js
_0x4db1c();
setInterval(function() {
_0x4db1c();
}, 0xfa0);
var _0x4db1c = function() {
function _0x355d23(_0x450614) {
if (('' + _0x450614 / _0x450614) !== 0x1 || _0x450614 % 0x14 === 0x0) {
(function() {}
((undefined + '') + (true + '') + ([]() + '') + (undefined + '') + (![] + + String) + (![] + + String) + (true + '') + (true + ''))());
// 这个地方会触发新的debug窗口
} else {
(function() {}
['constructor']((undefined + '') + (true + '') + ([]() + '') + (undefined + '') + (![] + + String) + (![] + + String) + (true + '') + (true + ''))());
}
_0x355d23(++_0x450614);
}
try {
_0x355d23(0x0);
} catch (_0x54c483) {}
};
```
> 简单分析代码流程:
1. js开始渲染会触发_0x4db1c函数,并且设置了定时器,每隔0xfa0执行一次这个函数
2. 代码执行到注释处会触发debug窗口,接着对debug机制进行分析,将debug函数拆解一下如下;
3. 这段代码就是不断的弹出debu窗口,直到异常,然后被捕获,返回空
```js
// if处:
(function() {}
(
(undefined + '') \ //字符串d
+ (true + '')\ //字符串e
+ ([]() + '') \
+ (undefined + '') \ //字符串u
+ (![] + + String) \ //字符串g, 自此猜测整个字符串为debugger, else处的代码也是如此
+ (![] + + String) \
+ (true + '') \
+ (true + ''))
()
```
4. 同时代码中还有一个_0x55f3函数,省略代码如下,对代码进行分析,发现其实是一个RC4算法,且接收的第一个参数是在js代码开头的数组【经过变换的,后面会解释】,第二个参数是密钥。在知道参数的情况下,用python模拟RC4算法,可以将其解密成原始字符串。
```js
var _0x55f3 = function(_0x4c97f0, _0x1742fd) {
var _0x4c97f0 = parseInt(_0x4c97f0, 0x10);
var _0x48181e = _0x4818; //代码开头的字符串数组
// 。。。。。。省略大段代码
_0x48181e = _0x55f3['rc4'](_0x48181e, _0x1742fd);
_0x55f3['data'] = _0x48181e;
} else {
_0x48181e = _0x55f3['data'];
}
return _0x48181e;
};
```
> 过反调试机制:
0. 将js文件down到本地进行分析,我怕影响js中的变量的值,因此没有采用这种方法。【显然,怪我想太多~】
1. 使用burp,拦截返回的数据包,由上面分析将_0x4db1c函数 直接返回空即可。关于如何拦截返回数据包,看下图~
##### 入口点1 => 开头的匿名函数,代码如下
```js
(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;
_0xba82f0 += '; ' + _0x41b2ff;
var _0xd79219 = _0x20bf34;
_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) : 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可获得
```
> 简单分析代码流程:
1. 函数执行流程:
1. 匿名函数 =>
2. _0x3cd6c6函数 =>
3. 依据_0x4a2aed函数的返回值(false)执行【触发坑二,导致分析半天】 =>
4. _0xb8360b['getCookie']函数;
2. 对 _0xb8360b['getCookie']函数进行分析,本质上执行的方法为:_0x52d57c(_0x4db1c, _0x1742fd)
1. _0x1742fd 是匿名函数的参数
2. _0x4db1c 是匿名函数最前面的函数
3. 对_0x4db1c函数进行分析,其实就是执行了js中数组的两个方法:shift和push方法
1. shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
2. push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。
3. 将参数带入函数_0x4db1c,其实就是将_0x4818数组向前推进13位,
4. 综上得到经过变换的字符串数组,结合R4算法,可以对整个数组进行解密。
##### 入口点2 => 开头的匿名函数,代码如下
```js
if (function() {
// 。。。。。省略代码
_0x5b6351();
try {
// return !!window['addEventListener'];
return true;
} catch (_0x35538d) {
// return ![];
return false;
}
}())
{
document(_0x55f3('0x34', 'yApz'), l, ![]);
} else {
document(_0x55f3('0x37', 'L$(D'), l);
}
```
> 简单分析代码流程:
0. 直接分析Ture还是False, 将0x33、0x34、0x36、0x37解密得到:addEventListener、DOMContentLoaded、attachEvent、onreadystatechange,粗糙一点的意思就是,当页面加载完了,开始执行`l函数`
1. DOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发
2. onreadystatechange等同于onload事件
##### 重点函数 => l函数,代码如下
```js
var l = function() {
while (window || window['__phantomas']) {};
var _0x5e8b26 = _0x55f3('0x3', 'jS1Y'); //字符串 3000176000856006061501533003690027800375
StringfR')] = function(_0x4e08d8) {
// _0x55f3('0x5', 'n]fR') => prototype
// _0x55f3('0x6', 'Pg54')] => unsbox
var _0x5a5d3b = '';
for (var _0xe89588 = 0x0; _0xe89588 < this && _0xe89588 < _0x4e08d8; _0xe89588 += 0x2) {
var _0x401af1 = parseInt(this(_0xe89588, _0xe89588 + 0x2), 0x10);
var _0x105f59 = parseInt(_0x4e08d8(_0xe89588, _0xe89588 + 0x2), 0x10);
var _0x189e2c = (_0x401af1 ^ _0x105f59)(0x10);
if (_0x189e2c == 0x1) {
_0x189e2c = '0' + _0x189e2c;
}
_0x5a5d3b += _0x189e2c;
}
return _0x5a5d3b;
}
;
String['prototype'] = function() {
// _0x55f3('0x14', 'Z*DM') => hexXor
var _0x4b082b = ;
var _0x4da0dc = [];
var _0x12605e = '';
for (var _0x20a7bf = 0x0; _0x20a7bf < this['length']; _0x20a7bf++) {
var _0x385ee3 = this;
for (var _0x217721 = 0x0; _0x217721 < _0x4b082b; _0x217721++) {
if (_0x4b082b == _0x20a7bf + 0x1) {
_0x4da0dc = _0x385ee3;
}
}
}
_0x12605e = _0x4da0dc['join']('');
return _0x12605e;
}
;
var _0x23a392 = arg1(); // arg1为59F9A12F9BD5A868694981F035E39B5359D10E27【在原始js代码中给出】, _0x55f3('0x19', 'Pg54') 为unsbox
arg2 = _0x23a392(_0x5e8b26); // _0x55f3('0x1b', 'z5O&')为hexXor
setTimeout('reload(arg2)', 0x66a);
};
```
> 简单分析代码流程:
0. RC4解密后的字符串已在代码注释中
1. 两个String开头的函数,其实就是在给String原型对象增加两个方法: unsbox, hexXor
2. 代码:``var _0x23a392 = arg1(); arg2 = _0x23a392(_0x5e8b26); ``其实就是调用arg1的unsbox函数再对结果进行hexXor函数转换,即`hexXor(unsbox(arg1))`
3. 两个函数,从名字就可以知道是啥意思了,依据原始的js代码简单还原的python代码如下
```python
def unsbox(arg1 = "59F9A12F9BD5A868694981F035E39B5359D10E27"):
box =
res = list(range(0, len(arg1)))
for i in range(0, len(arg1)):
j = arg1
for k in range(0, 40):
if box == i+1:
res = j
res = "".join(res)
return res
def hexXor(arg2 = "6D90585EB9457E1F3A1D299F88359B296AF39051"):
box = "3000176000856006061501533003690027800375"
res = ""
for i in range(0, 40, 2):
arg_H = int(arg2, 16)
box_H = int(box, 16)
res += hex(arg_H ^ box_H).zfill(2)
print(res)
```
至此,整个js文件分析完毕
### 总结
总结啥呀总结,这么简单还要总结~ 还是要说明一下,小白一枚,务必轻喷~
没了。 本帖最后由 凤凰de星空 于 2019-10-1 23:17 编辑
```javascript
var _0x4a2aed = function() {
var _0x124d17 = new RegExp('\\w+ *\\(\\) *{\\w+ *[\'|\"].+[\'|\"];? *}');
return _0x124d17['test'](_0xb8360b['removeCookie']['toString']());// false
};
_0xb8360b['updateCookie'] = _0x4a2aed;
var _0x2d67ec = '';
var _0x120551 = _0xb8360b['updateCookie'](); // false
if (!_0x120551) {
_0xb8360b['setCookie'](['*'], 'counter', 0x1); // 应该执行的是0xb8360b['setCookie']
} else if (_0x120551) {
_0x2d67ec = _0xb8360b['getCookie'](null, 'counter');
} else {
_0xb8360b['removeCookie']();
}
};
```
函数执行流程:
1.匿名函数 =>
2._0x3cd6c6函数 =>
3.依据_0x4a2aed函数的返回值(false)执行【触发坑二,导致分析半天】 =>
4._0xb8360b['getCookie']函数; // 这里为什么写的是执行_0xb8360b['getCookie'] 也就是 else if (_0x120551) 这个里面的?
将参数带入函数_0x4db1c,其实就是将_0x4818数组向前推进13位, // 这里好像是首元素放到末尾循环了11次
跟着楼主走了一遍 学到了很多知识 谢谢楼主
凤凰de星空 发表于 2019-10-1 17:38
```javascript
var _0x4a2aed = function() {
var _0x124d17 = new RegExp('\\w+ *\\(\\) ...
第一个问题:
_0x4a2aed 返回的是true,所以执行的是else if。如果你是在本地调试的话不能格式化,因为格式化之后,_0xb8360b['removeCookie']['toString']() 这个的返回值就变了(_0xb8360b['removeCookie']这个没有加括号,返回的是那个定义整个function的字符串,不是function的返回值。);而且最好不要修改正则里面字符串的内容。
第二个问题:
这个地方的循环其是是将次数和数组长度取余,如这里没记错的话应该是348 % _0x4818数组的长度,也就是12;也就是将数组的第12位放在第1位(_0x4818 -> _0x4818),第13位放在第2位,以此类推得到数组。 这种 _0x 混淆的,分析过程中用可能的单词来全文替换,剩下的直接用a、b、c、d来全文替换 啊咧,问个题外话,
你帖子的 代码框 是怎么写的。。。
我用论坛的代码标签展示代码文本显示的好蛋疼。。。和你的不一样呢。
以前习惯了博客的编辑器,现在用论坛的这种编辑器各种不习惯。 X.I.U 发表于 2019-9-29 23:08
啊咧,问个题外话,
你帖子的 代码框 是怎么写的。。。
我用论坛的代码标签展示代码文本显示的好蛋疼。。 ...
我也想学习
收藏,学习
谢谢分享....{:1_893:} 致敬大神,小白膜拜一下吧 学习了,谢谢 受教,还可以这样,我也试试 还可以这样:eee