datochan 发表于 2020-4-22 16:49

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

本帖最后由 besterChen 于 2020-4-22 18:24 编辑

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

1. [极验反爬虫防护分析之交互流程分析](https://www.52pojie.cn/thread-1162853-1-1.html)
2. [极验反爬虫防护分析之接口交互的解密方法](https://www.52pojie.cn/thread-1162893-1-1.html)
3. [极验反爬虫防护分析之接口交互的解密方法补遗](https://www.52pojie.cn/thread-1162951-1-1.html)
4. [极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路](https://www.52pojie.cn/thread-1162979-1-1.html)

本文是第二篇《接口交互的解密方法》,书接上文,上一篇中,我们遗留了两个问题:
1. 所有用于验证的代码都是混淆之后的,如何进行代码还原或者调试分析。
2. 请求与返回的关键数据都是加密的后的字符串如上述3、4、6等接口中的W参数,如何解密。

下面进入正文~

---

## JS代码的还原与调试分析

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



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

### 代码Hook及覆盖时机

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

``` javascrpt
var script = document.createElement('script');
script.src = "https://xxx/static/js/fullpage.8.8.4.js";
document.getElementsByTagName('head').appendChild(script);

var script = document.createElement('script');
script.src = "https://xxxx/static/js/slide.7.6.0.js";
document.getElementsByTagName('head').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代码如下:

``` 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,下面记录下代加密的原文内容:

``` json
{
      "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代码如下:

```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-1162951-1-1.html)》 中进行分析。

4everlove 发表于 2020-5-2 22:46

加密套路似曾相识, 客户端随机生成aes key,然后用key对内容进行加密, 接着用rsa公钥对aes key加密,   最后把aes加密后的内容和rsa加密后的key一并传给服务器,
服务器根据rsa私钥解密得到aes key, 然后用key对内容解密就得到了原始的数据

q6378561 发表于 2020-4-22 17:09

感谢楼主分享经验这就去试试

苏晓宇c 发表于 2020-4-22 18:32

大佬就是大佬。。。。篇幅太大,直接连开三帖

愤怒的平头哥 发表于 2020-4-22 20:16

感谢大佬分享

xxscwsrym 发表于 2020-4-23 06:29

已收藏,用时再学。

sitiger 发表于 2020-4-23 09:55

路过看看2345

sitiger 发表于 2020-4-23 09:57

这一篇比较有料

hua111 发表于 2020-4-23 13:02

戈壁独行 发表于 2020-4-24 09:28

厉害,,佩服佩服。。

huanwuying 发表于 2020-4-24 09:57

大手,我觉得我看的有点懵,慢慢学习
页: [1] 2 3
查看完整版本: 极验反爬虫防护分析之接口交互的解密方法