极验反爬虫防护分析之接口交互的解密方法

本文要分享的内容是去年为了抢鞋而分析 极验(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)
            # \0 backspace
            # text = text + ('\0' * add)
            text = text + ('0' * add).encode('utf-8')
        elif count > length:
            add = (length - (count % length))
            # text = text + ('\0' * 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('\0')
        return bytes.decode(plain_text).rstrip('\0')

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

posted @ 2020-04-22 21:10  贱书生  阅读(2584)  评论(1编辑  收藏  举报