一:抓包分析

image-20260505213612988

image-20260505213623799

image-20260505213821877

翻页和查看堆栈信息,发现代码强混淆,没法单步调试

先使用xhr断住

image-20260505214046756

二:数组对象分析

直接查看目前存在的数据对象

image-20260505214236334

image-20260505214324389

从上面能看到body数据以及关键信息

1
{    "pageNumber": 1,    "challengeType": "jsvmp_challenge",    "authToken": "bkkW/CQe/DzxrZBA39sCWzujmUeTT4vNxQ3/Jf75al34+4rjCHHWzVIvErP1ASawrLTjO2TjogxbBiYOLI4wgwKlpQ2raAnMpQNESaEHjI/f4o77lY33+TWuH9Qjd1uMdxArEWv+t65HgP/9BNggOuV0W+m7w/WnBYtp0O9Oy00=",    "timestamp": 1777988368,    "fingerprint": {        "userAgent": "SA6K0D7HCozqkkMhZNvWd4EkywEaFTNmHGfuqQ+KhLLxhBHEFf5q1G7CQ76QmKUEGb4mUcYTCbU3csgCZX17Efss9oI04jVFFM7naINVIsGoBbOtJ2j/RQOfY3qZFkdy30VS/p+SFyP1msMvKJUW4U/LuzYVDvBg19rd44ECzEs=",        "language": "zh-CN",        "screenResolution": "1920x1080",        "colorDepth": "24",        "timezoneOffset": -480,        "cookiesEnabled": true,        "plugins": "PDF Viewer, Chrome PDF Viewer, Chromium PDF Viewer, Microsoft Edge PDF Viewer, WebKit built-in PDF",        "sessionStorage": true,        "javaEnabled": false        }}

对比api请求的Payload

1
•ü••••jsvmp_challenge•¬•ERY56cchTe0cL3mMXzwusXMCZPtZadL/iCGgu2MQXdedIKYXo9RiFEHNiQnC2GIuhXFcTeI14EKsPgxA6gNCVR+SX4N/WzIABScRLX6sPxhbU0LREL+/1TigeNU4kmCca6h2rDMf5ZSqMPfF4GVxiYG3RLPK0A6OcfutUsIWU98= ®åçÏ•*±•¬•DovL5XRHGnj83tLEfDROJ5FsYg4LgRGXQ1xqQj0EmM5aZ+RiVfMA05l0K46JbPm2zVhDOofT0pYD0Qfcjyu/NxN1Oq7nHVNdS0yFPqWKb3hMl1FYn3eed9n0x++VTjMKnpW5pb3UBMS6nZF6iWsGbmphKSx9b3mseZKgQnJRNk0=••zh-CN•1920x1080"•24( 0•:bPDF Viewer, Chrome PDF Viewer, Chromium PDF Viewer, Microsoft Edge PDF Viewer, WebKit built-in PDF@•H

是不是大致一致,转换body中的513位的数组

image-20260505215459973

发现也是这个值,直接protobuf反序列化一下

1
import blackboxprotobuffrom loguru import logger# 前5位是空格及长度# Uint8Array的长度位513 body  array = [0, 0, 0, 1, 252, 8, 3, 18, 15, 106, 115, 118, 109, 112, 95, 99, 104, 97, 108, 108, 101, 110, 103, 101, 26, 172, 1, 87, 97, 106, 75, 110, 110, 114, 79, 89, 55, 80, 113, 84, 105, 120, 101, 55, 56, 113, 118, 56, 83, 86, 82, 47, 49, 107, 47, 121, 57, 97, 67, 122, 51, 111, 108, 114, 66, 69, 81, 106, 71, 108, 67, 70, 76, 103, 55, 49, 89, 117, 80, 49, 83, 110, 114, 78, 109, 65, 104, 89, 77, 120, 102, 104, 53, 51, 82, 86, 114, 87, 83, 81, 71, 90, 57, 69, 54, 54, 88, 85, 97, 76, 104, 83, 106, 50, 65, 78, 49, 112, 122, 79, 118, 114, 115, 83, 57, 76, 47, 101, 88, 43, 48, 117, 88, 82, 89, 118, 69, 81, 114, 72, 82, 88, 73, 43, 51, 81, 77, 74, 83, 89, 110, 98, 49, 70, 77, 80, 84, 43, 104, 88, 57, 115, 65, 72, 67, 74, 55, 49, 112, 119, 106, 49, 116, 105, 98, 85, 90, 74, 97, 65, 88, 113, 105, 85, 49, 88, 103, 51, 85, 88, 75, 117, 104, 83, 89, 111, 55, 103, 61, 32, 209, 160, 232, 207, 6, 42, 177, 2, 10, 172, 1, 78, 82, 106, 83, 114, 57, 75, 122, 70, 121, 97, 73, 90, 108, 113, 52, 67, 48, 79, 50, 82, 84, 74, 98, 99, 66, 78, 115, 54, 97, 55, 73, 79, 119, 116, 120, 54, 101, 85, 72, 56, 114, 89, 80, 84, 87, 79, 114, 84, 104, 67, 97, 105, 78, 55, 81, 88, 106, 107, 119, 69, 115, 48, 85, 73, 56, 117, 82, 87, 108, 121, 112, 79, 114, 103, 116, 76, 89, 118, 112, 76, 50, 99, 99, 49, 69, 97, 71, 83, 75, 69, 101, 69, 100, 121, 65, 83, 101, 70, 72, 98, 114, 50, 118, 56, 51, 77, 81, 77, 51, 75, 112, 100, 85, 74, 109, 53, 67, 54, 68, 79, 77, 119, 100, 88, 73, 69, 56, 76, 86, 47, 106, 79, 110, 120, 83, 115, 106, 67, 120, 99, 47, 66, 122, 108, 70, 87, 78, 118, 54, 99, 101, 82, 110, 47, 79, 121, 115, 112, 111, 101, 117, 119, 65, 78, 110, 122, 78, 102, 86, 85, 61, 18, 5, 122, 104, 45, 67, 78, 26, 9, 49, 57, 50, 48, 120, 49, 48, 56, 48, 34, 2, 50, 52, 40, 32, 48, 1, 58, 98, 80, 68, 70, 32, 86, 105, 101, 119, 101, 114, 44, 32, 67, 104, 114, 111, 109, 101, 32, 80, 68, 70, 32, 86, 105, 101, 119, 101, 114, 44, 32, 67, 104, 114, 111, 109, 105, 117, 109, 32, 80, 68, 70, 32, 86, 105, 101, 119, 101, 114, 44, 32, 77, 105, 99, 114, 111, 115, 111, 102, 116, 32, 69, 100, 103, 101, 32, 80, 68, 70, 32, 86, 105, 101, 119, 101, 114, 44, 32, 87, 101, 98, 75, 105, 116, 32, 98, 117, 105, 108, 116, 45, 105, 110, 32, 80, 68, 70, 64, 1, 72, 0]# 解析protobuf数据original_data, message_type = blackboxprotobuf.decode_message(bytes(array[5:]))logger.info(f'序列化结果==>>{original_data}')logger.info(f'序列化类型==>>{message_type}')

image-20260505223439162

观察序列化的结果

1
{'1': 3,'2': b 'jsvmp_challenge','3': b 'WajKnnrOY7PqTixe78qv8SVR/1k/y9aCz3olrBEQjGlCFLg71YuP1SnrNmAhYMxfh53RVrWSQGZ9E66XUaLhSj2AN1pzOvrsS9L/eX+0uXRYvEQrHRXI+3QMJSYnb1FMPT+hX9sAHCJ71pwj1tibUZJaAXqiU1Xg3UXKuhSYo7g=','4': 1777995857,'5': {'1': b 'NRjSr9KzFyaIZlq4C0O2RTJbcBNs6a7IOwtx6eUH8rYPTWOrThCaiN7QXjkwEs0UI8uRWlypOrgtLYvpL2cc1EaGSKEeEdyASeFHbr2v83MQM3KpdUJm5C6DOMwdXIE8LV/jOnxSsjCxc/BzlFWNv6ceRn/OyspoeuwANnzNfVU=','2': b 'zh-CN','3': {'6': 3474580101544292921},'4': b '24','5': 32,'6': 1,'7': b 'PDF Viewer, Chrome PDF Viewer, Chromium PDF Viewer, Microsoft Edge PDF Viewer, WebKit built-in PDF','8': 1,'9': 0}}

可以看到加密的只有

1
RQ2uOoeBESxr00dfMtXLYY5JTo9xivZeqw4QntFCMm4O8aI0C8wg3H3Ireh7/v+24P+hDAO6awsxrlS5mrBVh1C0psrUrYxPuCDo4LedmvH0o2zP0gC451OKD2IuuLWJ33i7IlwCuZY21R6C47T0PzZrsy3tjGPr6KaqsTJW+ro=CajsmTe3fPFGjxG1nZLHVFpuDVJ4JhyQPXQvomCuYiPX9o0BGK90/enrp4GD8ando7seUUbk8z6lCqUEeE5rcPemL9SPZ7lVLmAnVpNrt6b4B6vHd9sJZypgGmUBBD+sPt+EgOMdprsuOIOxjqOPgZFgzm+rW68gygfHIciKzS8=

三:加密参数分析

加密长度

image-20260505224147621

长度都为172

直接AI分析一波

image-20260505230325907

AI分析为RSA加密,那么直接全局搜索RSA的关键字encrypt和setPublicKey

image-20260505230501320

image-20260505230529698

image-20260505230618051

断住后重新请求

image-20260505230701668

可以看到已经断住,继续走

image-20260505230833443

看到断到加密位置了,字符加密有点像base64

image-20260505231014212

可以看到实际为base64加密的拼接字符,然后Rsa加密获得结果,直接交给AI帮我们生成python代码

image-20260505233703420

1
import rsaimport base64# 你的公钥 PEM 字符串PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCHshK1NixnImx08tq3V6VjvfwkARCOJbnPMZr4Ogh16ORkC45CFUB+2gu+IN2vjH3oJaY2e/6y0shD2fURMB3urxKE3gewRZMm5/T7noAXs4DJTOxJGwAJFHFfd9Zp73vXEcac4DKrvItOkjLx9P5JkA3947brgsMObYGdG3HOdwIDAQAB-----END PUBLIC KEY-----"""def rsa_encrypt(plaintext: bytes) -> str:    # 从 PEM 格式加载公钥    pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(PUBLIC_KEY_PEM.encode())    # 加密(rsa 库自动使用 PKCS#1 v1.5 填充)    ciphertext = rsa.encrypt(plaintext, pub_key)    # 返回 Base64 编码的密文    return base64.b64encode(ciphertext).decode('utf-8')# 示例if __name__ == "__main__":    message = b'MTc3Nzk5MzY5N19qc3ZtcF9zZWNyZXRfa2V5XzIwMjQ='    encrypted = rsa_encrypt(message)    print(encrypted)

image-20260505233748654

对比输出长度,也是172

image-20260505233733538

后续执行能看到另一个值是user-agent加密实现

image-20260505234114524

image-20260506005406420

继续执行后可以看到加密值实际的位置

image-20260506005905229

四:函数调用

经过上面的分析,目前的主要就是拼接出protobuf序列化的结果,在反序列化拼接出请求中的protobuf类型。核心代码如下

1
timestamp = int(time.time())auth_token = rsa_encrypt(base64.b64encode(f'{timestamp}_jsvmp_secret_key_2024'.encode('utf-8')).decode('utf-8'))ua = rsa_encrypt("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)")original_data = {    '1': i,    '2': b'jsvmp_challenge',    '3': auth_token.encode(),    '4': timestamp,    '5': {        '1': ua.encode(),        '2': b'zh-CN',        '3': {        '6': 3474580101544292921        },        '4': b'24',        '5': 32,        '6': 1,        '7': b"PDF Viewer, Chrome PDF Viewer, Chromium PDF Viewer, Microsoft Edge PDF Viewer, WebKit built-in PDF",        '8': 1,        '9': 0    }}form_data = bytes(blackboxprotobuf.encode_message(original_data, message_type))response = requests.post(url, headers=headers, data=bytes([0, 0, 0, 1, 252]) + form_data)

执行结果如下所示,获得的是protobuf数据

image-20260506095228757

解密,直接通过binascii.hexlify转换成16进制,然后剥离 gRPC 帧头。也就是从第10为开始取

1
def hexlify(response):    """    直接通过binascii.hexlify转换成16进制,然后从第10位开始取,因为前面10位都是空的,是剥离 gRPC 帧头    @param response:    @return:    """    response = binascii.hexlify(response.content).decode('utf-8')    byte_data = bytes.fromhex(response[10:])    data = blackboxprotobuf.protobuf_to_json(byte_data)    return data

image-20260506101453441

image-20260506123600484

替换掉所有的<span class=\'context_kw{i}\'></span>将它作为一个空字符

能得到 _673、1_17、_054、_686、6_67、51_6、617_、4_24、8_74、9_96

image-20260506124316621

第二页的数字为:8673、1517、1054、6686、6967、5156、6178、4424、8674、9596

结合转换的base64值58,57,53,56,61,57,58,54,56,57能得出

1
8 ---> 585 ---> 571 ---> 536 ---> 569 ---> 618 ---> 584 ---> 54

得出的结论是,奇数减52,偶数减50得到缺值

1
def get_data(response_data):    """    获取替换后的实际值    @param response_data:    @return:    """    response = json.loads(response_data[0])    html = response['6']    for i in range(10):        html = html.replace(f"<span class='context_kw{i}'></span>", '_')    html_list = html.split(' ')    jiem = base64.b64decode((response['7']).encode('utf-8')).decode('utf-8')    jiem = jiem.split(',')    numbers = [int(html_list[i].replace('_', f'{int(jiem[i]) - 50}')) if int(jiem[i]) % 2 == 0 else int(        html_list[i].replace('_', f'{int(jiem[i]) - 52}')) for i in range(len(jiem))]    return numbers

image-20260506131748121

更多内容也在公众号更新:码字的秃猴

tuhou