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

  • 相关阅读:
    RBAC-实现不同用户拥有不同权限
    RBAC鉴权-通过聚合clusterrole实现集群权限控制
    kubernetes中Deployment和replicaset关系剖析
    常用php操作redis命令整理(四)SET类型
    LINUX 系统硬盘占满,找不到大文件,原来是进程问题
    c#使用System.Media.SoundPlayer播放资源文件中的wav文件
    Go语言中的并发安全和锁
    Go语言中的channel
    Go语言goroutine并发编程
    Sublime Text的这些快捷操作,你都会用吗
  • 原文地址:https://www.cnblogs.com/rnckty/p/12756005.html
Copyright © 2011-2022 走看看