zoukankan      html  css  js  c++  java
  • 极验反爬虫防护分析之接口交互的解密方法

    本文要分享的内容是去年为了抢鞋而分析 极验(GeeTest)反爬虫防护的笔记,由于篇幅较长(为了多混点CB)我会按照我的分析顺序,分成如下四个主题与大家分享:

    1. 极验反爬虫防护分析之交互流程分析
    2. 极验反爬虫防护分析之接口交互的解密方法
    3. 极验反爬虫防护分析之接口交互的解密方法补遗
    4. 极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

    本文是第二篇《接口交互的解密方法》,书接上文,上一篇中,我们遗留了两个问题:

    1. 所有用于验证的代码都是混淆之后的,如何进行代码还原或者调试分析。
    2. 请求与返回的关键数据都是加密的后的字符串如上述3、4、6等接口中的W参数,如何解密。

    下面进入正文~


    JS代码的还原与调试分析

    geetest的js代码不单是简单的压缩,应该是经过混淆加密的,下载一个geetest的js文件,格式化并还原编码后,会发现js加载之后直接执行了几个嵌套的大循环,猜测应该是通过decodeURI对关键信息进行解密,拼装成一个大数组,这样便达到隐藏关键代码和信息目的,如下图:

    如此,程序的代码便可以写成封装成一个方法,通过数组下标来拼接程序代码,达到隐藏关键信息,预防静态分析的目的。根据我的经验,目前市面上好多前端代码都采用了这种代码加密思路,比如 同花顺数据中心的页面数据,为了防止接口被cors,构造了自定义的cookie信息,对应的js构造代码也是这类加密思路。由于还原此JS代码并不是本文的目的,所以我们就直接说调试分析方法,对还原代码有兴趣的童鞋可以自己再深入分析。

    代码Hook及覆盖时机

    如上说明,由于官网加载的Geetest脚本都是压缩加密混淆之后的代码,并不方便分析和调试。受X86年代更换目标dll路径就可以达到dll劫持思路的启发,我们将脚本下载下来格式化、替换转义字符之后,通过浏览器控制台加载修改后的脚本覆盖原js代码来达到同样的效果(因证书验证的问题,将代码放在了我自己的空间里):

    var script = document.createElement('script');
    script.src = "https://xxx/static/js/fullpage.8.8.4.js";
    document.getElementsByTagName('head')[0].appendChild(script);
    
    var script = document.createElement('script');
    script.src = "https://xxxx/static/js/slide.7.6.0.js";
    document.getElementsByTagName('head')[0].appendChild(script);

    经过分析,代码在执行完上一篇文章的第三步之后开始加载对应的代码,第四步的请求参数中的内容才开始加密的,因此要分析加密代码可以再加载了相应js之后再用我们的js进行覆盖即可。

    加密代码的定位方法

    浏览器中定位前端代码,最有效的方法莫过于通过 浏览器事件 + 条件断点的方式来定位了,但是由于上面说的代码加密的原因,这种定位方法在这里失效了,不论什么事件最终都会进入到上面说的大数组解析的方法中去,如下图:

     所以只能通过xhr请求入手,通过栈回溯的方法定位代码。值得庆幸的是火狐浏览器原生就支持堆栈跟踪,省了我们不少时间,如下图:

     通过js下断点不断回溯,发现上图中红框部分是加密的代码部分,下断点,重新点击登录框,程序中断,如下图:

    其中变量e的值记录一下:

    46207!!219363!!CSS1Compat!!334!!-1!!-1!!-1!!-1!!1!!-1!!-1!!9!!60!!45!!9!!15!!-1!!-1!!-1!!-1!!-1!!1!!-1!!-1!!231!!2!!-1!!-1!!-1!!157!!23!!44!!23!!1396!!279!!1396!!877!!zh-CN!!zh-CN,zh,zh-TW,zh-HK,en-US,en!!-1!!1!!24!!Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0!!1!!1!!1680!!1050!!1636!!1027!!1!!1!!1!!-1!!MacIntel!!1!!-8!!805a6cdeadd4f48ade985597f74928cb!!cc03697d39800df1ef0d2229132a62e8!!!!0!!-1!!0!!4!!AndaleMono,Arial,ArialBlack,ArialNarrow,ArialRoundedMTBold,ArialUnicodeMS,ComicSansMS,Courier,CourierNew,Geneva,Georgia,Helvetica,HelveticaNeue,Impact,LUCIDAGRANDE,MicrosoftSansSerif,Monaco,Palatino,Tahoma,Times,TimesNewRoman,TrebuchetMS,Verdana!!1568168747074!!-1,-1,-15,0,0,0,0,97,230,2,134245,8,7,441,444,992,3151782,3151782,3151970,-1!!-1!!-1!!577!!75!!49!!222!!75!!false!!false

    RSA加的分析

    单步步入跟进变量r的计算过程,由于方法是通过数组转义的,所以需要步入多次才能进入到正确的变量中,如下图:

    到这里,需要分两步分析:
    i. 待加密的内容92c689c0f4282f4e是什么,怎么得到的。
    i. 跟进继续分析,看是否能得到RSA加密的公钥

    这里备份一下加密的结果:

    031d0a23604ba7778905403f8403e780224cdfa6854551f55d2efd84436434df3579139c391d0b34d2ff91cbb29bf24902cf2dc1b03e165db3601d5f6cdbb6a1f1ef81f03b8085c5606671b50f22db362f8ddfec89551f163f96e84b1e22387b6e229fe1ab2ab76f8dcc2a8a15b840ebad8c75b7afbf126f2b6f33f478774e8d

    待加密的那串字符串已经执行过了,需要再重新启动调试分析,所以这里不打断本次调试,继续分析RSA的公钥,如下图:

     得到的RSA的公钥信息为:

    publicExponent = 10001
    publicKey = 00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81

    至此,RSA加密需要的信息我们分析完毕,模拟RSA加密的python代码如下:

    import rsa
    from binascii import b2a_hex
    
    e = '010001'
    e = int(e, 16)
    
    n = '00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81'
    n = int(n, 16)
    
    pub_key = rsa.PublicKey(e=e, n=n)
    print(b2a_hex(rsa.encrypt(b"0fe524023c414bb5", pub_key)))

    AES加密的分析

    继续单步步入AES加密的方法之前,意外发现了上下文变量中的信息,如下图:

     至此,我们知道上面遗留待分析的字符串: 92c689c0f4282f4e, 是AES加密用的Key。一会儿代码中证实一下。

     由上图可知,
    a. key=n: 密钥key,对应的值: 959603510..., 转换为十六进制为: 0x39326336...,对应的ascii为: 92c6..., 说明上面的字符串确实是AES加密用的key。
    b. iv=a[$_BBIFN(344)]; 补位值, 对应的值是: 808464432..., 转换为十六进制为: 0x30303030...,说明补位以字符0000000000000000补齐
    c. mode=a[$_BBIFN(344)]; 加密模式,分析可知是CBC模式的加密
    d. blockSize=4; 切分区块大小为4字节
    e. ciphertext=i; 序列化后的代加密字符,原文是t,下面记录下代加密的原文内容:

    {
            "gt": "2328764cdf162e8e60cc0b04383fef81",
            "challenge": "960780255cdadcbdebde1fb646d5cb77",
            "offline": false,
            "product": "float",
            "width": "100%",
            "lang": "zh-hk",
            "protocol": "https://",
            "fullpage": "/static/js/fullpage.8.8.4.js",
            "beeline": "/static/js/beeline.1.0.1.js",
            "static_servers": ["static.geetest.com/", "dn-staticdown.qbox.me/"],
            "slide": "/static/js/slide.7.6.3.js",
            "maze": "/static/js/maze.1.0.1.js",
            "aspect_radio": {
                    "click": 128,
                    "pencil": 128,
                    "beeline": 50,
                    "voice": 128,
                    "slide": 103
            },
            "voice": "/static/js/voice.1.2.0.js",
            "pencil": "/static/js/pencil.1.0.3.js",
            "type": "fullpage",
            "click": "/static/js/click.2.8.5.js",
            "geetest": "/static/js/geetest.6.0.9.js",
            "cc": 4,
            "ww": true,
            "i":     "46207!!219363!!CSS1Compat!!334!!-1!!-1!!-1!!-1!!1!!-1!!-1!!9!!60!!45!!9!!15!!-1!!-1!!-1!!-1!!-1!!1!!-1!!-1!!231!!2!!-1!!-1!!-1!!157!!23!!44!!23!!1396!!279!!1396!!877!!zh-CN!!zh-CN,zh,zh-TW,zh-HK,en-US,en!!-1!!1!!24!!Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0!!1!!1!!1680!!1050!!1636!!1027!!1!!1!!1!!-1!!MacIntel!!1!!-8!!805a6cdeadd4f48ade985597f74928cb!!cc03697d39800df1ef0d2229132a62e8!!!!0!!-1!!0!!4!!AndaleMono,Arial,ArialBlack,ArialNarrow,ArialRoundedMTBold,ArialUnicodeMS,ComicSansMS,Courier,CourierNew,Geneva,Georgia,Helvetica,HelveticaNeue,Impact,LUCIDAGRANDE,MicrosoftSansSerif,Monaco,Palatino,Tahoma,Times,TimesNewRoman,TrebuchetMS,Verdana!!1568168747074!!-1,-1,-15,0,0,0,0,97,230,2,134245,8,7,441,444,992,3151782,3151782,3151970,-1!!-1!!-1!!577!!75!!49!!222!!75!!false!!false"
    }

    至此,AES加密分析结束,整理的python代码如下:

    import base64
    from Crypto.Cipher import AES
    from binascii import b2a_hex, a2b_hex
    
    class PrpCrypt(object):
        def __init__(self, key):
            self.key = key.encode('utf-8')
            self.mode = AES.MODE_CBC
    
        # 加密函数,如果text不足16位就用空格补足为16位,
        # 如果大于16当时不是16的倍数,那就补足为16的倍数。
        def encrypt(self, text):
            text = text.encode('utf-8')
            cryptor = AES.new(self.key, self.mode, b'0000000000000000')
            # 这里密钥key 长度必须为16(AES-128),
            # 24(AES-192),或者32 (AES-256)Bytes 长度
            # 目前AES-128 足够目前使用
            length = 16
            count = len(text)
            if count < length:
                add = (length - count)
                #  backspace
                # text = text + ('' * add)
                text = text + ('0' * add).encode('utf-8')
            elif count > length:
                add = (length - (count % length))
                # text = text + ('' * add)
                text = text + ('0' * add).encode('utf-8')
            self.ciphertext = cryptor.encrypt(text)
            # 因为AES加密时候得到的字符串不一定是ascii字符集的,输出到终端或者保存时候可能存在问题
            # 所以这里统一把加密后的字符串转化为16进制字符串
            return b2a_hex(self.ciphertext)
    
        # 解密后,去掉补足的空格用strip() 去掉
        def decrypt(self, text):
            cryptor = AES.new(self.key, self.mode, b'0000000000000000')
            plain_text = cryptor.decrypt(a2b_hex(text))
            # return plain_text.rstrip('')
            return bytes.decode(plain_text).rstrip('')
    
    if __name__ == '__main__':
        txt = "{"gt":"2328764cdf162e8e60cc0b04383fef81",...}"
        pc = PrpCrypt('92c689c0f4282f4e')  # 初始化密钥
        e = pc.encrypt(txt)  # 加密
        d = pc.decrypt(e)  # 解密
        print("加密:", e)
        print("解密:", d)

    后记

    对比发现,之前记录的e的值就是这里待加密内容中 i的值gt 和 challenge可以从上一章的分析中得到,AES加密之后,将加密的结果base64编码处理。

    至此,我们知道,其大致的交互方式为:

    1. 用时间戳生成AES的密钥,并将密钥通过RSA加密。
    2. 通过AES+密钥将要提交的内容加密。
    3. 将提交内容的密文与密钥的密文拼接成w参数进行提交,w参数的格式为: base64(aes(json))+rsa(aes的密钥)

    本章遗留了如下两个问题:

    1. AES的密钥如何生成。
    2. e的值是什么内容,如何计算得到。

    限于篇幅,这两个问题,待到下一篇《极验反爬虫防护分析之接口交互的解密方法补遗》 中进行分析。

    转载:https://www.52pojie.cn/thread-1162893-1-1.html

  • 相关阅读:
    什么样的代码称得上是好代码?
    九年程序人生 总结分享
    Docker入门 第一课 --.Net Core 使用Docker全程记录
    阿里云 Windows Server 2012 r2 部署asp.net mvc网站 平坑之旅
    Visual studio 2015 Community 安装过程中遇到问题的终极解决
    Activiti6.0 spring5 工作流引擎 java SSM流程审批 项目框架
    java 进销存 库存管理 销售报表 商户管理 springmvc SSM crm 项目
    Leetcode名企之路
    24. 两两交换链表中的节点
    21. 合并两个有序链表
  • 原文地址:https://www.cnblogs.com/rnckty/p/12756005.html
Copyright © 2011-2022 走看看