1. 项目概述与核心价值最近在搞一个自动化项目对接某个第三方服务时毫无意外地撞上了顶象的验证码。这玩意儿尤其是他们的第五代产品在金融、电商这些对安全要求高的场景里出场率相当高。我遇到的还不是简单的滑块而是那种带背景干扰、需要点选特定文字的“点选验证码”以及一种动态扭曲的“图标验证码”。直接上OCR或者常规的CNN模型识别率惨不忍睹因为验证码图片在传输到前端渲染之前已经被“打散”和“混淆”了。这就引出了我们这次要聊的核心图片还原。逆向分析顶象5代验证码最关键、最基础的一步就是把前端收到的、看似杂乱无章的图片数据还原成一张人眼或者机器能正常识别的、完整的验证码图片。这不仅是绕过验证的第一步更是理解其防御机制、评估其安全强度的关键。网上关于顶象逆向的资料不少但大多集中在某个特定类型的JS代码混淆破解上对于底层图片数据的还原原理和通用方法讲得透彻的不多。所以我花了些时间系统地逆向分析了顶象5代几种主流验证码类型包括滑块、点选、图标等的图片生成与传输逻辑并整理出了一套相对通用的图片还原方法。这篇文章我会把这些逆向分析的思路、核心的还原算法以及可以直接运行的Python源码分享出来。无论你是做安全研究、爬虫开发还是单纯对前端反逆向技术感兴趣相信都能从中获得一些实用的思路和代码。2. 顶象5代验证码图片混淆机制深度解析要还原图片首先得知道它是怎么被“弄乱”的。顶象5代的防御思路很清晰不直接传输完整的、渲染好的图片。如果直接传一张PNG或者JPEG那太容易被截获和识别了。他们的策略是将一张完整的验证码图片拆解成多个碎片或称为图元然后对这些碎片的信息进行编码、混淆再通过多个网络请求或一段数据包发送给前端。前端拿到这些数据后再根据另一套隐藏在混淆JS中的逻辑在Canvas上动态地“拼图”最终呈现出验证码。2.1 核心混淆技术拆解通过对多个顶象5代验证码案例的抓包和JS逆向分析我总结出其图片混淆主要依赖于以下几种技术组合图元Sprite切割与散列这是最基础的。服务器端有一张包含所有可能字符、图标或滑块背景的“大图”雪碧图。当需要生成验证码时服务端会从这张大图中随机选取需要的图元比如几个汉字、几个图标并计算每个图元在大图中的坐标和尺寸。关键来了这些坐标和尺寸信息不会明文传输。信息编码与置换坐标x, y、宽高w, h这类数字信息会被转换成一种自定义的编码。常见的做法是进行某种形式的进制转换如十进制转一个自定义字符集的字符串或者与一个随机生成的偏移量进行运算。更复杂的情况下这些信息会被打散分别存放在不同的数据字段或请求中。Canvas绘图指令混淆前端还原图片的核心是调用Canvas的drawImageAPI。顶象会对调用drawImage的参数进行动态混淆。你可能在JS代码里看不到直接的ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)而是看到一堆变量计算最终参数可能来源于多个数组的拼接、字符串的解码或者一个复杂函数的返回值。多阶段加载与依赖一张验证码的完整数据可能分多个请求加载。比如先加载一个包含配置信息的JSON里面有一些加密的坐标信息再加载一张或多个雪碧图最后一段动态执行的JS逻辑利用配置信息和雪碧图进行绘制。这种分离增加了直接抓取完整图片的难度。2.2 不同类型验证码的混淆特点虽然核心思想一致但不同类型的验证码在实现细节上有差异滑块验证码重点在于背景图和缺口图滑块的生成。背景图通常是完整的但缺口位置信息被混淆滑块图本身可能是一个从背景图中扣出的碎片其形状非矩形和位置信息被编码。还原的关键在于定位缺口坐标和重建滑块形状。点选文字验证码最常见。一张背景图上有多个汉字需要按顺序点击其中几个。混淆点在于a) 所有候选文字的坐标都被混淆b) 需要点击的“正确文字”的顺序或索引被加密。还原后我们不仅要知道每个字在哪还要知道哪几个是目标。图标点选验证码与文字点选类似但图元是图标。图标的雪碧图可能更复杂图标数量更多。混淆方式可能包括图标的ID映射、坐标变换等。注意顶象的JS代码混淆强度很高变量名、函数名通常被压缩成单字母逻辑控制流也被平坦化或虚拟化直接阅读几乎不可能。我们的逆向重点不应放在“读懂每一行JS”而应放在“定位关键数据流和函数调用”。3. 逆向分析的关键步骤与工具链逆向分析是一个系统工程不能只盯着一个点。下面是我总结的一套标准操作流程SOP适用于大多数顶象5代验证码场景。3.1 环境准备与数据捕获工欲善其事必先利其器。你需要一个可控的分析环境。浏览器与开发者工具首选 Chrome 或新版 Edge。其开发者工具F12是核心。Sources面板用于调试JavaScript设置断点。Network面板捕获所有网络请求这是数据的源头。务必勾选“Preserve log”保留日志并禁用缓存。Console面板执行临时JS代码探测对象。抓包工具虽然浏览器自带的Network面板很强但像Fiddler Everywhere或Charles这类专业抓包工具在过滤、重发、断点调试请求/响应方面更强大。它们可以作为浏览器的代理捕获所有进出流量。代码格式化与解混淆工具面对压缩成一团的JS你需要一个格式化工具。Chrome Sources面板自带的“Pretty print”{}图标是第一步。对于更复杂的混淆如obfuscator.io可以尝试一些在线的或本地的JS反混淆工具但不要抱太大希望顶象的混淆通常能抵御自动反混淆。3.2 核心逆向流程从请求到渲染我的分析通常遵循以下步骤形成一个闭环第一步定位图片渲染入口打开目标网站触发验证码。在Network面板中筛选Img、Media或XHR/Fetch请求寻找看起来像图片的请求可能后缀是.jpg/.png也可能是无后缀的接口。同时注意观察那些返回JSON或类似数据结构的请求它们很可能包含了图片的元信息坐标、索引等。第二步追踪Canvas绘图调用在Sources面板中搜索关键词drawImage或canvas。由于代码被混淆直接搜索可能找不到。更有效的方法是在验证码加载过程中在Console中执行document.getElementsByTagName(canvas)找到Canvas元素然后监听其上下文。或者在drawImage这个原生API上设置断点在Sources面板的“Event Listener Breakpoints” - “Canvas” - “drawImage”这是最致命的一招。一旦断点触发调用堆栈Call Stack就会清晰地告诉你是哪一段混淆的JS代码最终调用了绘图。第三步逆向数据流在drawImage的断点处停下来后观察其参数。这些参数sx, sy, sw, sh, dx, dy, dw, dh现在应该是具体的数字。然后在调用堆栈中逐层向上回溯Step Out, Step Over观察这些数字是如何计算出来的。它们可能来源于某个数组的索引、某个字符串的解码结果、或者某个函数的返回值。你的目标就是找到这些参数的“源头数据”也就是从网络请求中获取到的原始混淆数据。第四步关联网络数据与本地变量将Network面板中捕获到的关键请求响应数据通常是JSON与你在JS调试中看到的“源头数据”进行对比。你可能需要将响应内容复制出来在Console里写一小段代码模拟JS中的解码逻辑看是否能得到与断点处观察到的参数一致的数字。这个过程需要反复尝试和推测。第五步推导还原算法一旦你成功地将网络数据如{“a”: “KJ3H”, “b”: “8FDE”, ...}与最终的绘图参数如sx120, sy45关联起来你就掌握了还原算法。这个算法可能是一个简单的Base64解码位移也可能是一个查表映射或者一个自定义的字符串到数字的转换函数。实操心得不要试图一次性理解整个JS文件。我们的目标是“黑盒”理解其输入网络数据和输出绘图参数之间的关系。用断点把程序“定住”然后像法医一样观察现场变量状态再倒推过程。记录下每一个成功的关联用注释和草图记录下来。4. 通用图片还原算法实现与源码解读基于上面的逆向分析我们可以抽象出一个通用的还原流程并用Python实现。这里我以最常见的“点选文字验证码”为例假设我们已通过逆向得知了其数据格式和还原算法。4.1 算法流程设计假设我们逆向分析后发现数据结构和还原逻辑如下网络请求得到一个JSON其中有一个字段data是一个字符串数组如[a1b2, c3d4, e5f6...]每个字符串对应一个文字图元的信息。每个字符串如a1b2需要被解码。前两个字符a1通过一个自定义的字符映射表转换成数字作为雪碧图中的x坐标后两个字符b2同理作为y坐标。所有文字图元都来自同一张雪碧图sprite.png每个图元的大小是固定的例如 40x40 像素。前端Canvas的绘制位置dx, dy是根据另一个算法计算出来的但为了还原一张完整的、包含所有候选字的图片我们可以简单地将所有图元按解码后的坐标从雪碧图中裁剪出来然后平铺在一张新的大图上。4.2 Python源码实现详解下面是根据上述假设逻辑编写的Python还原脚本。你需要根据实际逆向结果修改decode_position函数和参数。import json from PIL import Image import re import os class DingXiangImageReconstructor: 顶象验证码图片还原器示例类 此类根据逆向分析得到的规则将混淆的坐标数据还原为图片。 实际使用时你需要根据具体目标调整解码函数、雪碧图路径和尺寸。 def __init__(self, sprite_path, tile_width40, tile_height40): 初始化还原器 :param sprite_path: 雪碧图包含所有图元的大图的路径 :param tile_width: 每个图元如文字的宽度 :param tile_height: 每个图元的高度 self.sprite_img Image.open(sprite_path).convert(RGBA) self.tile_w tile_width self.tile_h tile_height # 假设一个自定义的字符到数字的映射表这是逆向分析的关键结果之一 # 例如a-0, b-1, ..., 1-26, 2-27 ... # 这里用一个简化版示例实际映射可能更复杂。 self.char_map {} chars abcdefghijklmnopqrstuvwxyz0123456789 for i, c in enumerate(chars): self.char_map[c] i def decode_position(self, code_str): 解码混淆的坐标字符串。 示例将a1b2解码为(x0*?1?, y1*?2?)这里需要根据实际算法调整。 这是一个示例函数实际算法可能涉及进制转换、异或、加减偏移量等。 :param code_str: 混淆的坐标字符串如a1b2 :return: (x, y) 在雪碧图中的坐标 # 假设 code_str 长度为4前两位编码x后两位编码y if len(code_str) ! 4: raise ValueError(f编码字符串长度异常: {code_str}) x_code, y_code code_str[:2], code_str[2:] # 示例算法将每个字符映射为数字然后组合。例如“a1” - (0, 1) - 0 * 36 1 1 # 这里的36是字符集大小。实际算法需逆向确定。 def decode_pair(pair): if pair[0] in self.char_map and pair[1] in self.char_map: return self.char_map[pair[0]] * len(self.char_map) self.char_map[pair[1]] else: # 如果字符不在映射表可能是数字直接表示这里需要灵活处理 try: return int(pair, 16) # 尝试当作16进制数 except: return int(pair) # 尝试当作10进制数 x_index decode_pair(x_code) y_index decode_pair(y_code) # 假设索引值需要乘以图元尺寸得到实际像素坐标也可能就是直接坐标 x x_index * self.tile_w y y_index * self.tile_h return x, y def reconstruct_from_json(self, json_data_path, output_path): 从保存的JSON数据文件中读取混淆数据并还原图片。 :param json_data_path: 保存网络响应JSON的文件路径 :param output_path: 还原出的图片保存路径 with open(json_data_path, r, encodingutf-8) as f: data json.load(f) # 假设JSON结构为 {challenge: xxx, data: [a1b2, c3d4, ...]} code_strings data.get(data, []) if not code_strings: print(未找到有效的坐标数据。) return # 计算画布大小假设我们按5列平铺 cols 5 rows (len(code_strings) cols - 1) // cols # 向上取整 canvas_width cols * self.tile_w canvas_height rows * self.tile_h # 创建一张新的透明背景图 canvas Image.new(RGBA, (canvas_width, canvas_height), (255, 255, 255, 0)) for idx, code in enumerate(code_strings): try: # 1. 解码坐标 sprite_x, sprite_y self.decode_position(code) # 2. 从雪碧图中裁剪对应图元 tile self.sprite_img.crop((sprite_x, sprite_y, sprite_x self.tile_w, sprite_y self.tile_h)) # 3. 计算平铺位置 col idx % cols row idx // cols paste_x col * self.tile_w paste_y row * self.tile_h # 4. 粘贴到画布 canvas.paste(tile, (paste_x, paste_y)) print(f已处理图元 {idx}: 编码{code} - 雪碧图位置({sprite_x},{sprite_y}) - 画布位置({paste_x},{paste_y})) except Exception as e: print(f处理图元编码 {code} 时出错: {e}) continue # 保存还原后的图片 canvas.save(output_path) print(f图片还原完成已保存至: {output_path}) # 可选显示图片 # canvas.show() def reconstruct_from_api(self, api_response_json, output_path): 直接传入API响应的JSON字典进行还原。 适用于实时处理。 :param api_response_json: 字典格式的API响应数据 :param output_path: 输出图片路径 # 逻辑与 reconstruct_from_json 类似只是数据来源不同 code_strings api_response_json.get(data, []) # ... [此处省略重复的还原逻辑同上] ... # 实际编码中应复用核心还原代码避免重复。 # 使用示例 if __name__ __main__: # 1. 初始化传入雪碧图和图元尺寸这些信息需要从网页资源或JS中获取 reconstructor DingXiangImageReconstructor( sprite_path./captcha_sprite.png, # 你下载的雪碧图 tile_width40, tile_height40 ) # 2. 从文件还原适用于分析阶段 reconstructor.reconstruct_from_json( json_data_path./captcha_data.json, # 你抓包保存的JSON数据 output_path./reconstructed.png ) # 3. 或者从内存中的字典还原适用于集成到爬虫 # fake_api_response {challenge: xxx, data: [a1b2, c3d4, e5f6, g7h8]} # reconstructor.reconstruct_from_api(fake_api_response, ./reconstructed_live.png)4.3 关键代码段解析与适配decode_position函数这是整个还原器的灵魂也是你需要根据具体目标进行修改的部分。示例中给出了一个简单的“字符映射线性组合”的算法。现实中算法可能包括Base64解码混淆字符串可能是Base64编码后的二进制数据再转成的字符串。自定义进制转换比如一个36进制的字符串a-z0-9转换为十进制数字。异或XOR解密与一个固定或动态的密钥进行异或运算。位移操作解码后的数字需要左移或右移特定位数。查表法字符串直接作为一个键去查询一个在JS中定义好的、巨大的坐标映射字典。 你需要通过断点调试对比输入字符串和输出坐标来反推这个函数的具体形式。可以多收集几组数据用Python尝试不同的解码组合直到所有数据都能被正确映射。雪碧图sprite.png的获取这个文件通常是一个静态资源在验证码页面加载时通过一个单独的图片请求获取。你可以在Network面板的Img类型请求中找到它直接下载即可。注意雪碧图可能会更新但频率通常不高。图元尺寸tile_width,tile_height这个信息可以通过观察还原后的图片是否对齐来调整更准确的方法是在JS调试中查看drawImage的sw和sh参数源图裁剪的宽高这通常就是图元的固定尺寸。注意事项这个还原器生成的是将所有候选图元平铺出来的图片便于查看和后续的OCR识别。它并不完全等同于前端Canvas上最终的渲染效果前端可能有随机位置、旋转、叠加等。但对于识别来说平铺图已经包含了所有必要信息。5. 针对不同验证码类型的还原策略调整上面的示例主要针对点选文字。对于其他类型核心框架不变但还原策略需要微调。5.1 滑块验证码还原要点滑块验证码通常包含两张关键图片背景大图和滑块小图缺口形状。背景图通常是完整传输的但可能被切割成多个碎片再拼接。还原重点在于找到所有碎片可能通过多个请求并按照正确的顺序拼接。数据中可能包含每个碎片的索引和布局信息。滑块缺口图这个图是透明的PNG形状就是缺口。它的还原更关键。你需要找到生成这个缺口形状的算法。数据中可能包含缺口在背景图中的位置gapX,gapY这个坐标被混淆。缺口轮廓的路径数据如果形状不规则可能是一组被编码的坐标点。还原后你需要根据缺口位置和形状在背景图上模拟“抠图”生成滑块小图或者直接计算出缺口的位置用于轨迹模拟。Python实现调整除了继承之前的坐标解码你可能需要增加一个reconstruct_slider_gap函数专门处理缺口形状的生成。如果缺口是简单的矩形或圆角矩形那么只需要解码出位置和大小如果是不规则多边形则需要解码路径数据并用PIL的ImageDraw模块绘制出来。5.2 图标点选验证码还原要点图标验证码与文字点选极为相似主要区别在于雪碧图更复杂图标雪碧图可能包含数十甚至上百个图标排列更密集。映射关系数据中的编码字符串可能先映射到一个图标ID再由ID映射到雪碧图上的坐标。即编码 - 图标ID - (x, y)。你需要逆向出这两层映射关系。Python实现调整在decode_position函数中可能需要先解码出图标ID然后通过一个预定义的id_to_position字典来查找坐标。这个字典可以通过分析JS中定义图标的数组或对象来获得。5.3 动态扭曲验证码处理有些验证码的图元如文字在绘制时会有随机的旋转、缩放、透视变换。这些变换参数也可能被编码在传输的数据中。应对策略在还原平铺图后识别算法如CNN需要对这些形变有一定的鲁棒性。或者在还原过程中我们可以尝试解码出变换参数并在用PIL粘贴图元时预先对图元进行相应的仿射变换Image.transform。但这会大大增加逆向和还原的复杂度通常更经济的方法是增强识别模型的抗变换能力。6. 常见问题排查与实战技巧实录在实际操作中你肯定会遇到各种各样的问题。下面是我踩过的一些坑和解决方案。6.1 数据抓取不全或格式不符问题按照脚本运行但code_strings为空或者解码后坐标明显不对如负数、巨大数值。排查确认数据源再次检查Network面板确认你抓取的JSON就是包含坐标数据的那个请求。有时会有多个相似请求。检查数据路径JSON的结构可能不是data字段。可能是imgArr、block、pieces等。用浏览器的Console对抓到的JSON对象进行展开查看找到真正的数组所在路径。验证解码算法用你逆向推测的解码算法手动计算一两组数据与在drawImage断点处看到的真实坐标对比。如果对不上说明算法有误。6.2 雪碧图加载失败或尺寸不对问题还原出的图片一片空白或者图元错位。排查确认雪碧图版本验证码的雪碧图可能会变。确保你下载的雪碧图与当前请求是同一版本。可以通过对比文件哈希或观察图片内容来判断。核对图元尺寸在drawImage断点处仔细记录sw和sh的值确保与代码中的tile_width/height一致。检查坐标计算确认你的解码函数输出的坐标(x, y)是雪碧图中的左上角起始坐标。有时算法给出的可能是图元中心的坐标需要减去一半宽高进行转换。6.3 JS混淆强度高无法定位关键函数问题代码被混淆得一塌糊涂搜索drawImage也找不到或者调用栈极其复杂。技巧Hook原生API在Console中执行以下代码可以Hook住drawImage打印出每次调用的参数和堆栈这是终极武器。(function() { var originalDrawImage CanvasRenderingContext2D.prototype.drawImage; CanvasRenderingContext2D.prototype.drawImage function() { console.log(drawImage called:, arguments); console.trace(); // 打印调用堆栈 return originalDrawImage.apply(this, arguments); }; })();执行后再触发验证码Console会输出所有绘图信息。关注网络请求的触发时机在疑似包含图片数据的XHR请求上设置“XHR/Fetch Breakpoint”然后跟栈找到处理这个请求响应数据的函数。寻找特征常量有时虽然代码混淆了但一些固定的数字如图元尺寸40或字符串常量可能保留。可以尝试在格式化后的代码中搜索这些常量定位到相关代码区域。6.4 还原出的图片用于识别率仍然不高问题图片还原出来了但丢给Tesseract或自定义CNN模型识别效果还是不好。分析与解决前端后处理顶象在前端Canvas绘制时可能还添加了额外的干扰如随机颜色微调、高斯模糊、添加噪点等。这些操作是在图元绘制到Canvas之后进行的我们的还原过程只还原了“图元拼接”没有模拟这些后处理。解决方案考虑使用前端无头浏览器如Puppeteer直接截图Canvas区域这是最“保真”但最重的方法。或者在还原图片后用OpenCV/PIL模拟添加一些常见的噪声和滤波让训练数据更接近真实前端效果。字体问题文字验证码可能使用了特殊字体。确保你的OCR训练集或字库包含了该字体。更好的方法是用还原出的大量图片自制训练集训练一个专用于该验证码的识别模型。图标分类对于图标验证码本质上是一个多分类问题。你需要收集所有可能的图标可以从雪碧图中完整裁剪出来建立图标库然后对还原出的图标图元进行特征匹配或分类。6.5 代码集成与稳定性问题脚本在本地测试成功但集成到自动化流程中不稳定。建议异常处理像上面的示例代码一样在每个关键步骤解码、裁剪、粘贴都用try...except包裹并记录日志避免单次失败导致整个流程崩溃。参数可配置化将雪碧图路径、图元尺寸、解码映射表等写成配置文件方便适配不同网站或验证码版本的更新。模拟浏览器环境有些验证码的JS逻辑依赖于浏览器环境变量如window,document。如果你的还原脚本需要执行部分JS解码逻辑使用PyExecJS等要确保环境模拟足够真实。逆向分析是一个不断对抗和升级的过程。顶象的验证码也在持续迭代。今天有效的方法明天可能就会失效。因此理解其核心思想和掌握通用的分析、调试、逆向方法比拥有一份固定的源码更重要。这套图片还原方法为你提供了一把打开大门的钥匙门后的世界完整的验证码破解还需要结合行为轨迹模拟、深度学习识别等技术但那已经是另一个层面的挑战了。希望这篇长文和附带的源码能成为你应对顶象5代验证码时一份扎实的参考资料。
逆向顶象5代验证码:图片还原算法与Python实现
发布时间:2026/6/30 18:48:30
1. 项目概述与核心价值最近在搞一个自动化项目对接某个第三方服务时毫无意外地撞上了顶象的验证码。这玩意儿尤其是他们的第五代产品在金融、电商这些对安全要求高的场景里出场率相当高。我遇到的还不是简单的滑块而是那种带背景干扰、需要点选特定文字的“点选验证码”以及一种动态扭曲的“图标验证码”。直接上OCR或者常规的CNN模型识别率惨不忍睹因为验证码图片在传输到前端渲染之前已经被“打散”和“混淆”了。这就引出了我们这次要聊的核心图片还原。逆向分析顶象5代验证码最关键、最基础的一步就是把前端收到的、看似杂乱无章的图片数据还原成一张人眼或者机器能正常识别的、完整的验证码图片。这不仅是绕过验证的第一步更是理解其防御机制、评估其安全强度的关键。网上关于顶象逆向的资料不少但大多集中在某个特定类型的JS代码混淆破解上对于底层图片数据的还原原理和通用方法讲得透彻的不多。所以我花了些时间系统地逆向分析了顶象5代几种主流验证码类型包括滑块、点选、图标等的图片生成与传输逻辑并整理出了一套相对通用的图片还原方法。这篇文章我会把这些逆向分析的思路、核心的还原算法以及可以直接运行的Python源码分享出来。无论你是做安全研究、爬虫开发还是单纯对前端反逆向技术感兴趣相信都能从中获得一些实用的思路和代码。2. 顶象5代验证码图片混淆机制深度解析要还原图片首先得知道它是怎么被“弄乱”的。顶象5代的防御思路很清晰不直接传输完整的、渲染好的图片。如果直接传一张PNG或者JPEG那太容易被截获和识别了。他们的策略是将一张完整的验证码图片拆解成多个碎片或称为图元然后对这些碎片的信息进行编码、混淆再通过多个网络请求或一段数据包发送给前端。前端拿到这些数据后再根据另一套隐藏在混淆JS中的逻辑在Canvas上动态地“拼图”最终呈现出验证码。2.1 核心混淆技术拆解通过对多个顶象5代验证码案例的抓包和JS逆向分析我总结出其图片混淆主要依赖于以下几种技术组合图元Sprite切割与散列这是最基础的。服务器端有一张包含所有可能字符、图标或滑块背景的“大图”雪碧图。当需要生成验证码时服务端会从这张大图中随机选取需要的图元比如几个汉字、几个图标并计算每个图元在大图中的坐标和尺寸。关键来了这些坐标和尺寸信息不会明文传输。信息编码与置换坐标x, y、宽高w, h这类数字信息会被转换成一种自定义的编码。常见的做法是进行某种形式的进制转换如十进制转一个自定义字符集的字符串或者与一个随机生成的偏移量进行运算。更复杂的情况下这些信息会被打散分别存放在不同的数据字段或请求中。Canvas绘图指令混淆前端还原图片的核心是调用Canvas的drawImageAPI。顶象会对调用drawImage的参数进行动态混淆。你可能在JS代码里看不到直接的ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)而是看到一堆变量计算最终参数可能来源于多个数组的拼接、字符串的解码或者一个复杂函数的返回值。多阶段加载与依赖一张验证码的完整数据可能分多个请求加载。比如先加载一个包含配置信息的JSON里面有一些加密的坐标信息再加载一张或多个雪碧图最后一段动态执行的JS逻辑利用配置信息和雪碧图进行绘制。这种分离增加了直接抓取完整图片的难度。2.2 不同类型验证码的混淆特点虽然核心思想一致但不同类型的验证码在实现细节上有差异滑块验证码重点在于背景图和缺口图滑块的生成。背景图通常是完整的但缺口位置信息被混淆滑块图本身可能是一个从背景图中扣出的碎片其形状非矩形和位置信息被编码。还原的关键在于定位缺口坐标和重建滑块形状。点选文字验证码最常见。一张背景图上有多个汉字需要按顺序点击其中几个。混淆点在于a) 所有候选文字的坐标都被混淆b) 需要点击的“正确文字”的顺序或索引被加密。还原后我们不仅要知道每个字在哪还要知道哪几个是目标。图标点选验证码与文字点选类似但图元是图标。图标的雪碧图可能更复杂图标数量更多。混淆方式可能包括图标的ID映射、坐标变换等。注意顶象的JS代码混淆强度很高变量名、函数名通常被压缩成单字母逻辑控制流也被平坦化或虚拟化直接阅读几乎不可能。我们的逆向重点不应放在“读懂每一行JS”而应放在“定位关键数据流和函数调用”。3. 逆向分析的关键步骤与工具链逆向分析是一个系统工程不能只盯着一个点。下面是我总结的一套标准操作流程SOP适用于大多数顶象5代验证码场景。3.1 环境准备与数据捕获工欲善其事必先利其器。你需要一个可控的分析环境。浏览器与开发者工具首选 Chrome 或新版 Edge。其开发者工具F12是核心。Sources面板用于调试JavaScript设置断点。Network面板捕获所有网络请求这是数据的源头。务必勾选“Preserve log”保留日志并禁用缓存。Console面板执行临时JS代码探测对象。抓包工具虽然浏览器自带的Network面板很强但像Fiddler Everywhere或Charles这类专业抓包工具在过滤、重发、断点调试请求/响应方面更强大。它们可以作为浏览器的代理捕获所有进出流量。代码格式化与解混淆工具面对压缩成一团的JS你需要一个格式化工具。Chrome Sources面板自带的“Pretty print”{}图标是第一步。对于更复杂的混淆如obfuscator.io可以尝试一些在线的或本地的JS反混淆工具但不要抱太大希望顶象的混淆通常能抵御自动反混淆。3.2 核心逆向流程从请求到渲染我的分析通常遵循以下步骤形成一个闭环第一步定位图片渲染入口打开目标网站触发验证码。在Network面板中筛选Img、Media或XHR/Fetch请求寻找看起来像图片的请求可能后缀是.jpg/.png也可能是无后缀的接口。同时注意观察那些返回JSON或类似数据结构的请求它们很可能包含了图片的元信息坐标、索引等。第二步追踪Canvas绘图调用在Sources面板中搜索关键词drawImage或canvas。由于代码被混淆直接搜索可能找不到。更有效的方法是在验证码加载过程中在Console中执行document.getElementsByTagName(canvas)找到Canvas元素然后监听其上下文。或者在drawImage这个原生API上设置断点在Sources面板的“Event Listener Breakpoints” - “Canvas” - “drawImage”这是最致命的一招。一旦断点触发调用堆栈Call Stack就会清晰地告诉你是哪一段混淆的JS代码最终调用了绘图。第三步逆向数据流在drawImage的断点处停下来后观察其参数。这些参数sx, sy, sw, sh, dx, dy, dw, dh现在应该是具体的数字。然后在调用堆栈中逐层向上回溯Step Out, Step Over观察这些数字是如何计算出来的。它们可能来源于某个数组的索引、某个字符串的解码结果、或者某个函数的返回值。你的目标就是找到这些参数的“源头数据”也就是从网络请求中获取到的原始混淆数据。第四步关联网络数据与本地变量将Network面板中捕获到的关键请求响应数据通常是JSON与你在JS调试中看到的“源头数据”进行对比。你可能需要将响应内容复制出来在Console里写一小段代码模拟JS中的解码逻辑看是否能得到与断点处观察到的参数一致的数字。这个过程需要反复尝试和推测。第五步推导还原算法一旦你成功地将网络数据如{“a”: “KJ3H”, “b”: “8FDE”, ...}与最终的绘图参数如sx120, sy45关联起来你就掌握了还原算法。这个算法可能是一个简单的Base64解码位移也可能是一个查表映射或者一个自定义的字符串到数字的转换函数。实操心得不要试图一次性理解整个JS文件。我们的目标是“黑盒”理解其输入网络数据和输出绘图参数之间的关系。用断点把程序“定住”然后像法医一样观察现场变量状态再倒推过程。记录下每一个成功的关联用注释和草图记录下来。4. 通用图片还原算法实现与源码解读基于上面的逆向分析我们可以抽象出一个通用的还原流程并用Python实现。这里我以最常见的“点选文字验证码”为例假设我们已通过逆向得知了其数据格式和还原算法。4.1 算法流程设计假设我们逆向分析后发现数据结构和还原逻辑如下网络请求得到一个JSON其中有一个字段data是一个字符串数组如[a1b2, c3d4, e5f6...]每个字符串对应一个文字图元的信息。每个字符串如a1b2需要被解码。前两个字符a1通过一个自定义的字符映射表转换成数字作为雪碧图中的x坐标后两个字符b2同理作为y坐标。所有文字图元都来自同一张雪碧图sprite.png每个图元的大小是固定的例如 40x40 像素。前端Canvas的绘制位置dx, dy是根据另一个算法计算出来的但为了还原一张完整的、包含所有候选字的图片我们可以简单地将所有图元按解码后的坐标从雪碧图中裁剪出来然后平铺在一张新的大图上。4.2 Python源码实现详解下面是根据上述假设逻辑编写的Python还原脚本。你需要根据实际逆向结果修改decode_position函数和参数。import json from PIL import Image import re import os class DingXiangImageReconstructor: 顶象验证码图片还原器示例类 此类根据逆向分析得到的规则将混淆的坐标数据还原为图片。 实际使用时你需要根据具体目标调整解码函数、雪碧图路径和尺寸。 def __init__(self, sprite_path, tile_width40, tile_height40): 初始化还原器 :param sprite_path: 雪碧图包含所有图元的大图的路径 :param tile_width: 每个图元如文字的宽度 :param tile_height: 每个图元的高度 self.sprite_img Image.open(sprite_path).convert(RGBA) self.tile_w tile_width self.tile_h tile_height # 假设一个自定义的字符到数字的映射表这是逆向分析的关键结果之一 # 例如a-0, b-1, ..., 1-26, 2-27 ... # 这里用一个简化版示例实际映射可能更复杂。 self.char_map {} chars abcdefghijklmnopqrstuvwxyz0123456789 for i, c in enumerate(chars): self.char_map[c] i def decode_position(self, code_str): 解码混淆的坐标字符串。 示例将a1b2解码为(x0*?1?, y1*?2?)这里需要根据实际算法调整。 这是一个示例函数实际算法可能涉及进制转换、异或、加减偏移量等。 :param code_str: 混淆的坐标字符串如a1b2 :return: (x, y) 在雪碧图中的坐标 # 假设 code_str 长度为4前两位编码x后两位编码y if len(code_str) ! 4: raise ValueError(f编码字符串长度异常: {code_str}) x_code, y_code code_str[:2], code_str[2:] # 示例算法将每个字符映射为数字然后组合。例如“a1” - (0, 1) - 0 * 36 1 1 # 这里的36是字符集大小。实际算法需逆向确定。 def decode_pair(pair): if pair[0] in self.char_map and pair[1] in self.char_map: return self.char_map[pair[0]] * len(self.char_map) self.char_map[pair[1]] else: # 如果字符不在映射表可能是数字直接表示这里需要灵活处理 try: return int(pair, 16) # 尝试当作16进制数 except: return int(pair) # 尝试当作10进制数 x_index decode_pair(x_code) y_index decode_pair(y_code) # 假设索引值需要乘以图元尺寸得到实际像素坐标也可能就是直接坐标 x x_index * self.tile_w y y_index * self.tile_h return x, y def reconstruct_from_json(self, json_data_path, output_path): 从保存的JSON数据文件中读取混淆数据并还原图片。 :param json_data_path: 保存网络响应JSON的文件路径 :param output_path: 还原出的图片保存路径 with open(json_data_path, r, encodingutf-8) as f: data json.load(f) # 假设JSON结构为 {challenge: xxx, data: [a1b2, c3d4, ...]} code_strings data.get(data, []) if not code_strings: print(未找到有效的坐标数据。) return # 计算画布大小假设我们按5列平铺 cols 5 rows (len(code_strings) cols - 1) // cols # 向上取整 canvas_width cols * self.tile_w canvas_height rows * self.tile_h # 创建一张新的透明背景图 canvas Image.new(RGBA, (canvas_width, canvas_height), (255, 255, 255, 0)) for idx, code in enumerate(code_strings): try: # 1. 解码坐标 sprite_x, sprite_y self.decode_position(code) # 2. 从雪碧图中裁剪对应图元 tile self.sprite_img.crop((sprite_x, sprite_y, sprite_x self.tile_w, sprite_y self.tile_h)) # 3. 计算平铺位置 col idx % cols row idx // cols paste_x col * self.tile_w paste_y row * self.tile_h # 4. 粘贴到画布 canvas.paste(tile, (paste_x, paste_y)) print(f已处理图元 {idx}: 编码{code} - 雪碧图位置({sprite_x},{sprite_y}) - 画布位置({paste_x},{paste_y})) except Exception as e: print(f处理图元编码 {code} 时出错: {e}) continue # 保存还原后的图片 canvas.save(output_path) print(f图片还原完成已保存至: {output_path}) # 可选显示图片 # canvas.show() def reconstruct_from_api(self, api_response_json, output_path): 直接传入API响应的JSON字典进行还原。 适用于实时处理。 :param api_response_json: 字典格式的API响应数据 :param output_path: 输出图片路径 # 逻辑与 reconstruct_from_json 类似只是数据来源不同 code_strings api_response_json.get(data, []) # ... [此处省略重复的还原逻辑同上] ... # 实际编码中应复用核心还原代码避免重复。 # 使用示例 if __name__ __main__: # 1. 初始化传入雪碧图和图元尺寸这些信息需要从网页资源或JS中获取 reconstructor DingXiangImageReconstructor( sprite_path./captcha_sprite.png, # 你下载的雪碧图 tile_width40, tile_height40 ) # 2. 从文件还原适用于分析阶段 reconstructor.reconstruct_from_json( json_data_path./captcha_data.json, # 你抓包保存的JSON数据 output_path./reconstructed.png ) # 3. 或者从内存中的字典还原适用于集成到爬虫 # fake_api_response {challenge: xxx, data: [a1b2, c3d4, e5f6, g7h8]} # reconstructor.reconstruct_from_api(fake_api_response, ./reconstructed_live.png)4.3 关键代码段解析与适配decode_position函数这是整个还原器的灵魂也是你需要根据具体目标进行修改的部分。示例中给出了一个简单的“字符映射线性组合”的算法。现实中算法可能包括Base64解码混淆字符串可能是Base64编码后的二进制数据再转成的字符串。自定义进制转换比如一个36进制的字符串a-z0-9转换为十进制数字。异或XOR解密与一个固定或动态的密钥进行异或运算。位移操作解码后的数字需要左移或右移特定位数。查表法字符串直接作为一个键去查询一个在JS中定义好的、巨大的坐标映射字典。 你需要通过断点调试对比输入字符串和输出坐标来反推这个函数的具体形式。可以多收集几组数据用Python尝试不同的解码组合直到所有数据都能被正确映射。雪碧图sprite.png的获取这个文件通常是一个静态资源在验证码页面加载时通过一个单独的图片请求获取。你可以在Network面板的Img类型请求中找到它直接下载即可。注意雪碧图可能会更新但频率通常不高。图元尺寸tile_width,tile_height这个信息可以通过观察还原后的图片是否对齐来调整更准确的方法是在JS调试中查看drawImage的sw和sh参数源图裁剪的宽高这通常就是图元的固定尺寸。注意事项这个还原器生成的是将所有候选图元平铺出来的图片便于查看和后续的OCR识别。它并不完全等同于前端Canvas上最终的渲染效果前端可能有随机位置、旋转、叠加等。但对于识别来说平铺图已经包含了所有必要信息。5. 针对不同验证码类型的还原策略调整上面的示例主要针对点选文字。对于其他类型核心框架不变但还原策略需要微调。5.1 滑块验证码还原要点滑块验证码通常包含两张关键图片背景大图和滑块小图缺口形状。背景图通常是完整传输的但可能被切割成多个碎片再拼接。还原重点在于找到所有碎片可能通过多个请求并按照正确的顺序拼接。数据中可能包含每个碎片的索引和布局信息。滑块缺口图这个图是透明的PNG形状就是缺口。它的还原更关键。你需要找到生成这个缺口形状的算法。数据中可能包含缺口在背景图中的位置gapX,gapY这个坐标被混淆。缺口轮廓的路径数据如果形状不规则可能是一组被编码的坐标点。还原后你需要根据缺口位置和形状在背景图上模拟“抠图”生成滑块小图或者直接计算出缺口的位置用于轨迹模拟。Python实现调整除了继承之前的坐标解码你可能需要增加一个reconstruct_slider_gap函数专门处理缺口形状的生成。如果缺口是简单的矩形或圆角矩形那么只需要解码出位置和大小如果是不规则多边形则需要解码路径数据并用PIL的ImageDraw模块绘制出来。5.2 图标点选验证码还原要点图标验证码与文字点选极为相似主要区别在于雪碧图更复杂图标雪碧图可能包含数十甚至上百个图标排列更密集。映射关系数据中的编码字符串可能先映射到一个图标ID再由ID映射到雪碧图上的坐标。即编码 - 图标ID - (x, y)。你需要逆向出这两层映射关系。Python实现调整在decode_position函数中可能需要先解码出图标ID然后通过一个预定义的id_to_position字典来查找坐标。这个字典可以通过分析JS中定义图标的数组或对象来获得。5.3 动态扭曲验证码处理有些验证码的图元如文字在绘制时会有随机的旋转、缩放、透视变换。这些变换参数也可能被编码在传输的数据中。应对策略在还原平铺图后识别算法如CNN需要对这些形变有一定的鲁棒性。或者在还原过程中我们可以尝试解码出变换参数并在用PIL粘贴图元时预先对图元进行相应的仿射变换Image.transform。但这会大大增加逆向和还原的复杂度通常更经济的方法是增强识别模型的抗变换能力。6. 常见问题排查与实战技巧实录在实际操作中你肯定会遇到各种各样的问题。下面是我踩过的一些坑和解决方案。6.1 数据抓取不全或格式不符问题按照脚本运行但code_strings为空或者解码后坐标明显不对如负数、巨大数值。排查确认数据源再次检查Network面板确认你抓取的JSON就是包含坐标数据的那个请求。有时会有多个相似请求。检查数据路径JSON的结构可能不是data字段。可能是imgArr、block、pieces等。用浏览器的Console对抓到的JSON对象进行展开查看找到真正的数组所在路径。验证解码算法用你逆向推测的解码算法手动计算一两组数据与在drawImage断点处看到的真实坐标对比。如果对不上说明算法有误。6.2 雪碧图加载失败或尺寸不对问题还原出的图片一片空白或者图元错位。排查确认雪碧图版本验证码的雪碧图可能会变。确保你下载的雪碧图与当前请求是同一版本。可以通过对比文件哈希或观察图片内容来判断。核对图元尺寸在drawImage断点处仔细记录sw和sh的值确保与代码中的tile_width/height一致。检查坐标计算确认你的解码函数输出的坐标(x, y)是雪碧图中的左上角起始坐标。有时算法给出的可能是图元中心的坐标需要减去一半宽高进行转换。6.3 JS混淆强度高无法定位关键函数问题代码被混淆得一塌糊涂搜索drawImage也找不到或者调用栈极其复杂。技巧Hook原生API在Console中执行以下代码可以Hook住drawImage打印出每次调用的参数和堆栈这是终极武器。(function() { var originalDrawImage CanvasRenderingContext2D.prototype.drawImage; CanvasRenderingContext2D.prototype.drawImage function() { console.log(drawImage called:, arguments); console.trace(); // 打印调用堆栈 return originalDrawImage.apply(this, arguments); }; })();执行后再触发验证码Console会输出所有绘图信息。关注网络请求的触发时机在疑似包含图片数据的XHR请求上设置“XHR/Fetch Breakpoint”然后跟栈找到处理这个请求响应数据的函数。寻找特征常量有时虽然代码混淆了但一些固定的数字如图元尺寸40或字符串常量可能保留。可以尝试在格式化后的代码中搜索这些常量定位到相关代码区域。6.4 还原出的图片用于识别率仍然不高问题图片还原出来了但丢给Tesseract或自定义CNN模型识别效果还是不好。分析与解决前端后处理顶象在前端Canvas绘制时可能还添加了额外的干扰如随机颜色微调、高斯模糊、添加噪点等。这些操作是在图元绘制到Canvas之后进行的我们的还原过程只还原了“图元拼接”没有模拟这些后处理。解决方案考虑使用前端无头浏览器如Puppeteer直接截图Canvas区域这是最“保真”但最重的方法。或者在还原图片后用OpenCV/PIL模拟添加一些常见的噪声和滤波让训练数据更接近真实前端效果。字体问题文字验证码可能使用了特殊字体。确保你的OCR训练集或字库包含了该字体。更好的方法是用还原出的大量图片自制训练集训练一个专用于该验证码的识别模型。图标分类对于图标验证码本质上是一个多分类问题。你需要收集所有可能的图标可以从雪碧图中完整裁剪出来建立图标库然后对还原出的图标图元进行特征匹配或分类。6.5 代码集成与稳定性问题脚本在本地测试成功但集成到自动化流程中不稳定。建议异常处理像上面的示例代码一样在每个关键步骤解码、裁剪、粘贴都用try...except包裹并记录日志避免单次失败导致整个流程崩溃。参数可配置化将雪碧图路径、图元尺寸、解码映射表等写成配置文件方便适配不同网站或验证码版本的更新。模拟浏览器环境有些验证码的JS逻辑依赖于浏览器环境变量如window,document。如果你的还原脚本需要执行部分JS解码逻辑使用PyExecJS等要确保环境模拟足够真实。逆向分析是一个不断对抗和升级的过程。顶象的验证码也在持续迭代。今天有效的方法明天可能就会失效。因此理解其核心思想和掌握通用的分析、调试、逆向方法比拥有一份固定的源码更重要。这套图片还原方法为你提供了一把打开大门的钥匙门后的世界完整的验证码破解还需要结合行为轨迹模拟、深度学习识别等技术但那已经是另一个层面的挑战了。希望这篇长文和附带的源码能成为你应对顶象5代验证码时一份扎实的参考资料。