吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 13392|回复: 40
上一主题 下一主题
收起左侧

[Web逆向] 对某N博客网站的反爬虫机制分析

  [复制链接]
跳转到指定楼层
楼主
baymax0day 发表于 2019-9-29 22:28 回帖奖励

对某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) {}
};

简单分析代码流程:

  1. js开始渲染会触发_0x4db1c函数,并且设置了定时器,每隔0xfa0执行一次这个函数
  2. 代码执行到注释处会触发debug窗口,接着对debug机制进行分析,将debug函数拆解一下如下;
  3. 这段代码就是不断的弹出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])
()
  1. 同时代码中还有一个_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可获得

简单分析代码流程:

  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 => 开头的匿名函数,代码如下
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);
}

简单分析代码流程:

  1. 直接分析Ture还是False, 将0x33、0x34、0x36、0x37解密得到:addEventListener、DOMContentLoaded、attachEvent、onreadystatechange,粗糙一点的意思就是,当页面加载完了,开始执行l函数
    1. DOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发
    2. 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);
};

简单分析代码流程:

  1. RC4解密后的字符串已在代码注释中
  2. 两个String开头的函数,其实就是在给String原型对象增加两个方法: unsbox, hexXor
  3. 代码:var _0x23a392 = arg1[_0x55f3('0x19', 'Pg54')](); arg2 = _0x23a392[_0x55f3('0x1b', 'z5O&')](_0x5e8b26);其实就是调用arg1的unsbox函数再对结果进行hexXor函数转换,即hexXor(unsbox(arg1))
  4. 两个函数,从名字就可以知道是啥意思了,依据原始的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文件分析完毕

总结

    总结啥呀总结,这么简单还要总结~ 还是要说明一下,小白一枚,务必轻喷~ 
    没了。

免费评分

参与人数 19威望 +2 吾爱币 +24 热心值 +17 收起 理由
xyfs + 1 + 1 热心回复!
52pojie666z + 1 + 1 热心回复!
天秤 + 1 + 1 用心讨论,共获提升!
Hmily + 2 + 7 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
爱流量 + 1 + 1 用心讨论,共获提升!
喵喵爱吃鱼 + 1 + 1 热心回复!
ol416 + 1 我很赞同!
笙若 + 1 + 1 谢谢@Thanks!
O丶ne丨柒夜彡 + 1 + 1 热心回复!
cqchen + 1 我很赞同!
lookerJ + 1 热心回复!
mmnnbbvv005 + 1 + 1 我很赞同!
XhyEax + 1 + 1 我很赞同!
liyonghaod + 1 + 1 热心回复!
槑头熊 + 1 谢谢@Thanks!
王星星 + 1 热心回复!
hiodis + 1 + 1 谢谢@Thanks!
sky59998 + 1 + 1 热心回复!
CrazyNut + 3 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
凤凰de星空 发表于 2019-10-1 17:38
本帖最后由 凤凰de星空 于 2019-10-1 23:17 编辑
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次




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





免费评分

参与人数 1热心值 +1 收起 理由
liangzhihao + 1 我很赞同!

查看全部评分

推荐
 楼主| baymax0day 发表于 2019-10-2 11:10 |楼主
凤凰de星空 发表于 2019-10-1 17:38
[md]```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[11] -> _0x4818[0]),第13位放在第2位,以此类推得到数组。
沙发
夜泉 发表于 2019-9-29 22:43
这种 _0x 混淆的,分析过程中用可能的单词来全文替换,剩下的直接用a、b、c、d来全文替换
3#
X.I.U 发表于 2019-9-29 23:08
啊咧,问个题外话,
你帖子的 代码框 是怎么写的。。。
我用论坛的代码标签展示代码文本显示的好蛋疼。。。和你的不一样呢。
以前习惯了博客的编辑器,现在用论坛的这种编辑器各种不习惯。
头像被屏蔽
4#
yike911 发表于 2019-9-29 23:14
提示: 作者被禁止或删除 内容自动屏蔽
5#
蓝洛水深 发表于 2019-9-29 23:25
X.I.U 发表于 2019-9-29 23:08
啊咧,问个题外话,
你帖子的 代码框 是怎么写的。。。
我用论坛的代码标签展示代码文本显示的好蛋疼。。 ...

我也想学习
6#
隐与匿 发表于 2019-9-29 23:28

收藏,学习
        谢谢分享....
7#
lcfinc 发表于 2019-9-29 23:41
致敬大神,小白膜拜一下吧
8#
生如上善若水 发表于 2019-9-29 23:57
学习了,谢谢
9#
xuegaoxiansen 发表于 2019-9-30 00:32
受教,还可以这样,我也试试
10#
天少 发表于 2019-9-30 00:39
还可以这样
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-25 12:31

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表