极验反爬虫防护分析之接口交互的解密方法
本帖最后由 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)》 中进行分析。
加密套路似曾相识, 客户端随机生成aes key,然后用key对内容进行加密, 接着用rsa公钥对aes key加密, 最后把aes加密后的内容和rsa加密后的key一并传给服务器,
服务器根据rsa私钥解密得到aes key, 然后用key对内容解密就得到了原始的数据 感谢楼主分享经验这就去试试 大佬就是大佬。。。。篇幅太大,直接连开三帖 感谢大佬分享 已收藏,用时再学。 路过看看2345 这一篇比较有料 厉害,,佩服佩服。。 大手,我觉得我看的有点懵,慢慢学习