baymax0day 发表于 2019-9-29 22:28

对某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 17:38

本帖最后由 凤凰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次




跟着楼主走了一遍 学到了很多知识 谢谢楼主





baymax0day 发表于 2019-10-2 11:10

凤凰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位,以此类推得到数组。

夜泉 发表于 2019-9-29 22:43

这种 _0x 混淆的,分析过程中用可能的单词来全文替换,剩下的直接用a、b、c、d来全文替换

X.I.U 发表于 2019-9-29 23:08

啊咧,问个题外话,
你帖子的 代码框 是怎么写的。。。
我用论坛的代码标签展示代码文本显示的好蛋疼。。。和你的不一样呢。
以前习惯了博客的编辑器,现在用论坛的这种编辑器各种不习惯。

yike911 发表于 2019-9-29 23:14

蓝洛水深 发表于 2019-9-29 23:25

X.I.U 发表于 2019-9-29 23:08
啊咧,问个题外话,
你帖子的 代码框 是怎么写的。。。
我用论坛的代码标签展示代码文本显示的好蛋疼。。 ...

我也想学习

隐与匿 发表于 2019-9-29 23:28


收藏,学习
      谢谢分享....{:1_893:}

lcfinc 发表于 2019-9-29 23:41

致敬大神,小白膜拜一下吧

生如上善若水 发表于 2019-9-29 23:57

学习了,谢谢

xuegaoxiansen 发表于 2019-9-30 00:32

受教,还可以这样,我也试试

天少 发表于 2019-9-30 00:39

还可以这样:eee
页: [1] 2 3 4 5
查看完整版本: 对某N博客网站的反爬虫机制分析