逆向工程师的日常:拆解一个结合了JSVMP、Protobuf和CSS混淆的spiderdemo挑战
逆向工程师的日常拆解JSVMPProtobufCSS三重防护的Web迷宫当浏览器开发者工具中闪现出一串Uint8Array二进制数据而页面元素却显示着残缺不全的数字时你知道又遇到了一个精心设计的数字堡垒。这不是普通的网页抓取挑战而是融合了JSVMP虚拟化保护、Protobuf二进制传输和CSS动态干扰的三重防御体系。作为逆向工程师我们需要像侦探破案一样从蛛丝马迹中重建完整的逻辑链条。1. 初探战场逆向工程师的犯罪现场调查打开Chrome开发者工具的那一刻整个逆向工程就已经开始。现代Web反爬虫技术早已不是简单的参数加密而是形成了多层次的防御体系网络层XHR请求中流动的不再是明文的JSON而是被Protobuf序列化的二进制数据流代码层关键JavaScript逻辑被JSVMPJavaScript Virtual Machine Protection虚拟化形成喵喵盾般的保护壳表现层返回的HTML中嵌入了CSS动态生成的干扰元素肉眼可见的数字与实际传输数据完全不同专业工具链准备除了常规的Chrome DevTools建议配置Python 3.8环境安装blackboxprotobuf、pycryptodome等库以及Hex Editor等二进制分析工具。首次遇到这种案例时我的工作台通常会变成这样# 基础工具库准备 import blackboxprotobuf from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 import binascii2. 突破二进制防线Protobuf数据流的解码艺术在本次案例中第一个突破口来自网络请求中的异常Content-Type。当发现application/x-protobuf这个不常见的MIME类型时逆向工程师的直觉就会警铃大作。2.1 捕获和解析原始数据流通过浏览器开发者工具的Network面板我们可以导出原始请求的二进制数据// 示例从XHR响应中获取Uint8Array const xhr new XMLHttpRequest(); xhr.responseType arraybuffer; xhr.onload function() { const array new Uint8Array(xhr.response); console.log(原始二进制数据:, array); };使用Python进行Protobuf解码时关键是要处理头部填充字节def decode_protobuf(raw_bytes): # 前5字节为长度标识和填充 header raw_bytes[:5] payload raw_bytes[5:] # 使用blackboxprotobuf进行动态解析 data, msg_type blackboxprotobuf.decode_message(payload) print(f解码结果: {data}) print(f消息结构: {msg_type}) return data, msg_type2.2 动态消息结构的逆向工程Protobuf的棘手之处在于没有.proto定义文件时需要逆向消息结构。通过多次请求采样我们发现该案例的消息格式如下字段编号类型含义示例值1int页码12string挑战类型jsvmp_challenge3bytes认证令牌RSA加密的Base64字符串4int时间戳17612022595message设备指纹嵌套结构嵌套的设备指纹结构包含字段编号类型含义1bytes加密的User-Agent2string语言3string屏幕分辨率4string色彩深度5int时区偏移3. 攻克JSVMP虚拟化代码的逆向技巧面对JSVMP保护的代码传统的断点调试几乎失效。我们需要转变思路采用黑盒分析关键点拦截的策略。3.1 识别加密入口通过监控典型加密操作的特征可以定位关键代码段Base64编码识别搜索btoa()、atob()调用RSA特征识别查找setPublicKey、encrypt等关键词大整数操作注意BigInt、modPow等运算在本次案例中通过拦截XHR请求的构造过程我们发现了两处关键加密// 伪代码展示加密逻辑 function generateAuthToken(timestamp) { const plaintext ${timestamp}_jsvmp_secret_key_2024; return RSA.encrypt(base64.encode(plaintext)); } function encryptUserAgent(ua) { // 只加密UA的前缀部分 const prefix ua.split(()[0]; return RSA.encrypt(prefix); }3.2 构建Python加密模拟在Python中复现前端加密逻辑class JSVMPSimulator: def __init__(self, public_key): self.key RSA.import_key(public_key) self.cipher PKCS1_v1_5.new(self.key) def rsa_encrypt(self, text): encrypted self.cipher.encrypt(text.encode()) return base64.b64encode(encrypted).decode() def generate_token(self, timestamp): plain f{timestamp}_jsvmp_secret_key_2024 b64_plain base64.b64encode(plain.encode()).decode() return self.rsa_encrypt(b64_plain) def encrypt_ua(self, user_agent): prefix user_agent.split(()[0].strip() return self.rsa_encrypt(prefix)4. 破解CSS数字迷宫前端表现的逆向逻辑当终于拿到响应数据时却发现数字显示异常——这是典型的CSS动态干扰技术。我们需要分析DOM和CSS规则的交互逻辑。4.1 解析HTML-CSS联动机制观察到的响应数据结构{ 6: 8span classcontext_kw0/span35 51span classcontext_kw1/span2..., 7: 58,61,55,61,58,50,59,54,58,58 }对应的补全算法表现为将context_kw0到context_kw9的span作为占位符每个缺失数字由Base64解码后的值经过特定计算得出奇偶性决定使用50还是52作为减数4.2 Python实现数字还原def restore_numbers(html_part, base64_code): # 解码CSS规则 decoded base64.b64decode(base64_code).decode() modifiers [int(x) for x in decoded.split(,)] # 替换所有span标签为占位符 for i in range(10): html_part html_part.replace( fspan classcontext_kw{i}/span, _ ) # 分割数字段 segments html_part.split() restored [] # 应用补全算法 for seg, mod in zip(segments, modifiers): if mod % 2 0: # 偶数 digit mod - 50 else: # 奇数 digit mod - 52 restored.append(seg.replace(_, str(digit))) return .join(restored)5. 构建完整自动化流程将各个破解环节串联起来形成端到端的解决方案初始化阶段config { public_key: -----BEGIN PUBLIC KEY-----\nMIGfMA0GCSq...\n-----END PUBLIC KEY-----, base_url: https://www.spiderdemo.cn/authentication/jsvmp_challenge/, user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } solver JSVMPSimulator(config[public_key])请求构造阶段def build_protobuf_request(page1): timestamp int(time.time()) auth_token solver.generate_token(timestamp) encrypted_ua solver.encrypt_ua(config[user_agent]) message { 1: page, 2: bjsvmp_challenge, 3: auth_token.encode(), 4: timestamp, 5: { 1: encrypted_ua.encode(), 2: bzh-CN, 3: b1536x864, 4: b24, 5: -480, 6: 1, 7: bPDF Viewer, Chrome PDF Viewer..., 8: 1, 9: 0 } } return message响应处理阶段def process_response(protobuf_response): # 提取二进制数据 raw_data protobuf_response.content # 跳过前5字节头部 payload raw_data[5:] # 解析Protobuf data, _ blackboxprotobuf.decode_message(payload) # 还原CSS干扰的数字 if 6 in data and 7 in data: numbers restore_numbers(data[6], data[7]) data[restored_numbers] numbers return data在真实项目中这样的三重防护体系往往需要3-5天的深度分析才能完全破解。每个环节都可能暗藏玄机——比如JSVMP中可能混入虚假的加密调用Protobuf字段可能动态变化CSS规则可能采用更复杂的计算逻辑。逆向工程师需要保持对异常数据的敏锐嗅觉像解谜游戏一样享受这个抽丝剥茧的过程。