逆向工程实战:破解AES加密滑动验证码的前端攻防 1. 项目概述当滑动验证码遇上AES加密在当前的网络安全攻防体系中验证码是区分人机行为的第一道重要防线而滑动验证码因其良好的用户体验和一定的安全性被广泛应用于各类网站的登录、注册和防刷场景。然而当滑动验证码的验证逻辑被前端JavaScript代码承载并且关键参数如滑动轨迹、时间戳、位置信息被使用AES这类对称加密算法加密后提交时整个验证过程就从简单的“图片比对”升级为一场前端加密逻辑的攻防战。这恰恰是“逆向工程”大显身手的领域。我们这次要实战解析的就是这样一个典型的、结合了密码学的前端安全案例破解一个采用AES加密滑动验证数据的验证码系统。这不仅仅是“找到加密函数”那么简单。一个完整的实战过程涉及到对前端代码的静态分析与动态调试定位关键加密函数理解其调用链和参数构造过程最终在脱离浏览器环境例如用Python下复现整个加密流程从而实现对验证码的自动化通过。对于安全研究人员、渗透测试工程师或是自动化开发人员来说掌握这套方法意味着能够深入理解前端安全机制的实现与局限评估其真实防护强度。同时这个过程本身也是对JavaScript逆向、密码学应用和网络协议分析的绝佳综合训练。2. 核心思路与技术选型面对一个加密的滑动验证码我们的核心目标非常明确在不依赖浏览器渲染和人工操作的前提下程序化地生成能被服务端成功验证的请求数据包。而数据包中的加密参数就是我们要攻克的堡垒。2.1 逆向工程的基本路径整个逆向工程的路径可以清晰地划分为四个阶段我称之为“观察-定位-剖析-复现”四步法。第一步网络行为观察。这是所有逆向的起点。我们需要使用抓包工具如Burp Suite、Charles、Fiddler或浏览器开发者工具的Network面板完整捕获一次成功的人工滑动验证过程。关键不在于滑动本身而在于滑动后浏览器向服务器发送了哪些请求特别是那个携带了加密参数的POST请求。我们需要记录下这个请求的URL、Headers以及最重要的Form Data或Request Payload。通常你会看到一个名为token、data、encryptedData或类似字段其值是一长串看似随机的Base64编码字符串或十六进制字符串这就是我们的核心目标——被AES加密后的验证数据。第二步关键代码定位。知道加密数据在哪下一步就是找到生成它的代码。由于加密操作必然发生在浏览器端客户端因此所有逻辑都封装在发送给浏览器的JavaScript文件中。我们的任务是从可能多达数百个、经过混淆和压缩的JS文件中找到负责加密的那几行关键代码。这里主要依赖两种技术搜索关键字和XHR/ Fetch断点。我们可以尝试在JS文件中搜索“AES”、“CryptoJS”、“encrypt”、“滑动”、“slide”等关键词。更有效的方法是在开发者工具的Sources面板中对捕获到的那个加密请求的URL添加“XHR/ Fetch Breakpoint”这样当JavaScript代码发起该请求时执行流会自动暂停我们就能直接看到调用堆栈Call Stack从而逆向定位到加密函数所在的代码块。第三步加密逻辑剖析。找到加密函数后真正的挑战才开始。现代前端开发普遍使用Webpack等打包工具并可能进行代码混淆变量名可能变成a, b, c, _0x1a2b3c等形式。我们需要耐心地分析这段代码它调用了哪个加密库通常是CryptoJS也可能是自实现的AES加密模式是什么如CBC, ECB填充方式是什么如PKCS7密钥Key和初始化向量IV从哪里来是硬编码在代码里还是通过某个接口动态获取加密前的原始数据明文是什么结构通常它是一个JSON字符串包含了滑动轨迹的坐标数组、滑动耗时、滑块位置等。第四步本地化复现。这是将逆向成果转化为实际能力的一步。我们需要使用一种服务端或脚本语言如Python完全复现前端的加密流程。这意味着1. 用同样的方式生成或模拟滑动轨迹数据2. 以同样的方式获取或构造密钥和IV3. 使用相同的AES算法、模式和填充对原始数据进行加密4. 将加密结果以同样的格式如Base64编码并放入请求体中发送。成功复现后我们的程序就具备了“伪造”合法滑动验证请求的能力。2.2 为什么是AES以及工具选型考量目标系统使用AES加密并不令人意外。AES高级加密标准作为对称加密算法的标杆其本身的安全性在已知密钥不泄露的前提下是极高的。前端使用AES加密其安全假设是加密密钥和逻辑对客户端不可见或难以提取。但前端代码的开放性本质使得这个假设在逆向工程面前变得非常脆弱。这与“前端RSAAES加密安全吗”这个热词中的讨论息息相关——单纯依赖前端加密无法保证绝对安全它更多是增加攻击成本和难度防止明文传输被直接窥探或篡改。在工具选型上我倾向于以下组合抓包与调试Burp Suite 浏览器开发者工具。Burp Suite作为代理可以拦截和修改所有HTTP/HTTPS流量其Repeater模块对重放测试至关重要。浏览器开发者工具Chrome DevTools则是动态调试JS的不二之选它的断点、单步执行、作用域查看功能无可替代。逆向分析辅助油猴脚本与Hook技术。对于简单的、密钥硬编码的场景直接阅读代码即可。对于复杂的、密钥动态生成的场景可以编写油猴脚本或使用Fiddler的AutoResponder功能在关键函数如CryptoJS.AES.encrypt执行前将其重写Hook打印出输入参数和密钥这是一种“动态插桩”的高效手段。复现语言Python。Python拥有极其丰富的密码学库如pycryptodome、cryptography和网络请求库如requests语法简洁非常适合快速实现加密逻辑的复现和自动化测试。Node.js也是一个不错的选择因其与浏览器JS环境更接近但在集成到自动化流程时Python的生态通常更友好。注意所有逆向工程实践必须严格在合法授权范围内进行仅用于安全研究、学习或个人所属系统的安全加固测试。未经授权对他人的系统进行此类操作是违法的。3. 实战拆解一步步定位并分析加密核心理论说得再多不如一次真枪实弹的演练。我们假设目标网站有一个常见的滑动验证码滑动成功后会向/api/verify_slide发送一个POST请求其中包含一个加密字段enc_data。3.1 网络抓包与请求分析首先打开浏览器开发者工具F12切换到Network网络面板并勾选“Preserve log”保留日志。然后在目标页面完成一次滑动验证操作。操作完成后在Network面板中通过过滤/verify_slide或观察最新的POST请求我们能迅速找到目标。点击该请求查看其“Headers”和“Payload”。一个典型的请求负载可能如下POST /api/verify_slide HTTP/1.1 ... Content-Type: application/x-www-form-urlencoded; charsetUTF-8 session_idxxxxenc_dataU2FsdGVkX1%2B8vLxq5T2K7b1...很长一串Base64编码字符串这里enc_data就是我们的重点。它看起来是Base64编码但直接解码可能得到乱码因为它是AES加密后的二进制数据再进行Base64编码的结果。关键观察点请求头注意Content-Type和可能的自定义头部如X-Requested-With。其他参数除了enc_data通常还会有session_id、timestamp、captcha_id等参数。这些参数很可能在构造加密明文时被用到或者用于服务端会话校验。响应查看服务器返回的成功或失败信息这有助于后续验证我们复现的加密是否正确。3.2 逆向追踪加密函数接下来我们在该请求上右键选择“Copy” - “Copy as cURL (bash)”将其保存下来以备重放测试。然后开始逆向追踪。方法一搜索关键词。在开发者工具的Sources面板按CtrlShiftFWindows或CmdOptFMac打开全局搜索。搜索关键词如“encrypt”、“AES”、“CryptoJS”、“.encrypt”、“slide”。如果代码混淆不严重可能会直接找到相关函数。方法二XHR/ Fetch断点更高效。在Sources面板右侧找到“XHR/ Fetch Breakpoints”区域点击“”号添加一个包含部分请求URL的断点例如verify_slide。然后重新滑动一次验证码。当浏览器即将发送这个请求时代码执行会立即暂停。此时查看右侧的“Call Stack”调用堆栈。堆栈的最顶部通常是send或fetch这类原生函数往下找你会看到属于网站自身脚本文件的函数调用它们通常以.js结尾并且函数名可能被混淆。点击这些函数就能跳转到具体的代码行。方法三事件监听器断点。如果滑动验证码的触发是通过鼠标事件也可以在Sources面板的“Event Listener Breakpoints”中展开“Mouse”事件勾选click、mousedown、mouseup等然后进行滑动操作。这可能会在生成滑动轨迹数据的地方暂停从而顺着代码找到加密处。一旦在代码中疑似加密的位置暂停我们需要仔细分析上下文。一个典型的AES加密调用以CryptoJS库为例可能长这样// 混淆后的代码可能类似 var _0x123abc CryptoJS.AES.encrypt(_0x456def, _0x789ghi, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: _0xijk101 }).toString();即使变量名被混淆CryptoJS.AES.encrypt这个关键调用通常是可识别的。我们需要记录下三个关键信息明文_0x456def、密钥_0x789ghi、加密选项mode, padding, iv。3.3 深入剖析加密参数与明文构造找到加密函数只是开始理解每个参数的来源才是核心。1. 密钥Key和IVInitialization Vector的来源分析这是安全性的关键。通常有以下几种情况硬编码在JS中最简单也最不安全。在加密函数附近搜索字符串或常量可能会发现一个固定的字符串被用作密钥。有时密钥会被拆散或经过简单运算如Base64解码、Hex解码后使用。从初始接口响应中获取在页面加载时一个初始的GET请求如/api/init_captcha的响应中可能就包含了本次会话使用的密钥或密钥的“种子”。需要关联分析多个请求。动态计算生成密钥可能由session_id、timestamp等参数通过一个固定的算法如HMAC-SHA256计算得出。这需要找到这个生成函数。2. 明文Plaintext的构造分析加密前的明文通常是一个JSON字符串。我们需要在加密函数之前找到组装这个JSON对象的地方。它很可能包含以下字段{ track: [[12,45],[15,48],...], // 滑动轨迹的[x,y]坐标数组 slide_time: 2450, // 滑动总耗时毫秒 start_s: 1234567890, // 开始滑动的时间戳 end_s: 1234567890, // 结束滑动的时间戳 background_width: 300, // 背景图宽度 slider_left: 5 // 滑块最终停留的X轴位置相对于背景图 }我们需要弄清楚这些数据是如何采集的。track数组通常由监听鼠标移动事件mousemove来收集。slide_time和timestamp通过Date.now()计算。slider_left是滑块最终位置与起始位置的差值。3. 加密选项Options确认AES加密需要指定模式如ECB, CBC, CFB和填充方式如PKCS7。在CryptoJS中如果不指定默认是CBC模式和PKCS7填充。但为了确保复现准确必须从代码中确认。iv初始化向量在CBC等模式下是必需的需要找到它的值或生成方式。实操心得在动态调试时充分利用开发者工具中“Console”面板的实时执行功能。当代码在断点处暂停时你可以将当前作用域中的可疑变量如_0x456def用鼠标悬停查看其值或者直接在Console中输入变量名来打印其完整内容。这是快速理解数据结构的利器。4. 本地化复现用Python还原加密流程经过逆向分析我们假设已经获得了以下信息明文构造规则一个包含轨迹、时间、位置的JSON字符串。密钥一个固定的字符串this_is_a_secret_key示例实际可能是Base64编码的。IV一个固定的16字节字符串1234567890123456。加密算法AES-128-CBCPKCS7填充。现在我们用Python来复现这一过程。4.1 环境准备与依赖安装首先确保安装了必要的Python库。我们使用pycryptodome它是PyCrypto的一个流行分支功能强大且维护活跃。pip install pycryptodome requestsrequests库用于后续发送网络请求。4.2 模拟滑动轨迹生成为了通过验证我们需要模拟人类滑动的轨迹。完全匀速的直线滑动很容易被识别为机器行为。一个简单的模拟策略是生成一个带有轻微波动和加速度的轨迹。import json import time import random def generate_slide_track(distance): 生成模拟人类滑动的轨迹数组。 distance: 需要滑动的总距离像素。 返回: 一个列表每个元素是[时间偏移量, x坐标偏移量]。 track [] current 0 start_time int(time.time() * 1000) # 模拟初始加速、中间匀速、末端减速 while current distance: # 剩余距离 remaining distance - current # 根据剩余距离调整速度开始快中间稳结尾慢 if current distance * 0.2: speed random.uniform(3, 6) # 加速段 elif remaining distance * 0.2: speed random.uniform(1, 3) # 减速段 else: speed random.uniform(2, 4) # 匀速段 step min(speed, remaining) # 这一步移动的距离 current step elapsed int(time.time() * 1000) - start_time track.append([elapsed, round(current, 2)]) # [时间戳偏移 x位置] # 添加一个小的随机间隔更拟人 time.sleep(random.uniform(0.01, 0.05)) # 确保最后一步精确到达终点 if track[-1][1] ! distance: track[-1][1] distance return track # 假设滑动距离是200像素 slide_distance 200 mock_track generate_slide_track(slide_distance) print(f生成的轨迹点数量{len(mock_track)}) print(f最后一点位置{mock_track[-1]})4.3 AES加密函数复现根据逆向得到的参数编写AES加密函数。这里特别注意密钥和IV的处理。前端CryptoJS通常接受字符串作为密钥并内部处理。在Python中我们需要确保密钥和IV是字节串bytes且长度符合AES-12816字节、AES-19224字节或AES-25632字节的要求。from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def aes_encrypt(data_string, key_str, iv_str): 使用AES-CBC模式加密数据并返回Base64编码的字符串。 参数: data_string: 要加密的明文字符串。 key_str: 密钥字符串。 iv_str: 初始化向量字符串。 返回: Base64编码的加密字符串。 # 将字符串转换为bytes data data_string.encode(utf-8) key key_str.encode(utf-8) iv iv_str.encode(utf-8) # 检查密钥长度如果不足CryptoJS可能会用某种方式补齐这里简单用空格补齐到16/24/32字节 # 更严谨的做法是逆向确认CryptoJS的密钥处理方式例如使用CryptoJS.enc.Utf8.parse if len(key) not in [16, 24, 32]: # 这里假设CryptoJS使用UTF-8解析密钥长度不足时行为可能不一致。 # 一个常见情况如果key_str是16个字符的字符串UTF-8编码后可能就是16字节。 # 如果逆向发现是Hex或Base64解码后的字节这里需要相应调整。 # 本例假设key_str就是16字节的ASCII字符串。 if len(key) 16: key key.ljust(16, b\0) # 用零字节填充 elif len(key) 24: key key.ljust(24, b\0) elif len(key) 32: key key.ljust(32, b\0) else: key key[:32] # 截断 # 创建AES cipher对象使用CBC模式和PKCS7填充 cipher AES.new(key, AES.MODE_CBC, iv) # 对数据进行PKCS7填充并加密 encrypted_bytes cipher.encrypt(pad(data, AES.block_size)) # 将加密后的字节进行Base64编码并转换为字符串 encrypted_b64 base64.b64encode(encrypted_bytes).decode(utf-8) return encrypted_b64 # 测试加密 plaintext_json json.dumps({ track: mock_track, slide_time: mock_track[-1][0], # 总耗时 start_s: int(time.time() * 1000) - mock_track[-1][0], end_s: int(time.time() * 1000), background_width: 300, slider_left: slide_distance }, separators(,, :)) # 压缩JSON去除空格与前端生成格式一致 secret_key this_is_a_secret_key # 从逆向分析中获得 iv 1234567890123456 # 从逆向分析中获得 encrypted_data aes_encrypt(plaintext_json, secret_key, iv) print(f明文{plaintext_json[:100]}...) print(f加密后(Base64){encrypted_data[:100]}...)4.4 组装请求并验证最后我们使用requests库模拟浏览器发送请求。import requests # 假设从初始请求获取的会话ID session_id your_session_id_from_previous_request captcha_id your_captcha_id # 目标URL verify_url https://target-website.com/api/verify_slide # 构造请求头模仿浏览器 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, X-Requested-With: XMLHttpRequest, # 常见于Ajax请求 # 可能还需要其他头部如Referer, Origin等需根据抓包结果补充 } # 构造请求体 payload { session_id: session_id, captcha_id: captcha_id, enc_data: encrypted_data, # 这是我们复现加密得到的数据 # 可能还有其他固定参数 } # 发送POST请求 response requests.post(verify_url, datapayload, headersheaders) print(f状态码{response.status_code}) print(f响应内容{response.text}) # 解析响应通常是一个JSON try: result response.json() if result.get(code) 200 or result.get(success): print(*** 验证成功 ***) else: print(f验证失败{result.get(message)}) except: print(f响应解析失败{response.text})5. 常见问题与深度排查技巧在实际逆向和复现过程中几乎不可能一帆风顺。下面是我总结的一些典型问题及其排查思路。5.1 加密结果不一致问题这是最常遇到的问题。Python加密的结果和前端JavaScript加密的结果对不上。排查步骤确认明文完全一致这是最容易出错的地方。确保Python中组装的JSON字符串与前端JavaScript中的完全一致包括字段顺序、空格、缩进。建议在前端加密代码处设置断点将待加密的明文变量复制出来与Python生成的字符串进行逐字符比对。可以使用在线Diff工具。特别注意JavaScript的JSON.stringify默认会产生空格而很多前端代码为了节省体积会使用JSON.stringify(obj)或JSON.stringify(obj, null, 0)。在Python中使用json.dumps(obj, separators(‘,’, ‘:’))可以生成最紧凑的无空格JSON这通常是前端混淆后的代码所采用的格式。确认密钥和IV的字节表示这是核心难点。CryptoJS的CryptoJS.enc.Utf8.parse(“key”)和Python的”key”.encode(‘utf-8’)在大多数情况下是等价的。但如果密钥本身是Base64或Hex编码的字符串呢你需要在前端代码中查看密钥在被parse之前的原始形式。如果它是”ABCDEFG123456”这样的Base64那么在前端它会被先Base64解码成字节再用作密钥。在Python中你需要做同样的事key base64.b64decode(key_str_b64)。对于IV也是如此。确认加密参数模式CBC/ECB、填充PKCS7/ZeroPadding、密钥长度128/192/256必须完全一致。AES-128对应16字节密钥AES-256对应32字节密钥。如果密钥字符串长度不符合CryptoJS可能会通过某种哈希函数如MD5派生密钥。你需要查看加密函数附近是否有类似CryptoJS.MD5(key).toString()的代码。输出格式CryptoJS的.toString()默认输出一个特殊的OpenSSL格式的字符串以”U2FsdGVkX1”开头。如果你需要原始的Base64应该使用CryptoJS.enc.Base64.stringify(ciphertext)。在Python中我们直接输出Base64。确保前后端比较的是同一种格式。可以在前端将加密结果分别用toString()和CryptoJS.enc.Base64.stringify()输出看哪个与Python的Base64结果匹配。5.2 密钥动态获取问题如果密钥不是硬编码的而是通过接口获取或动态计算你需要找到其来源。从接口获取在Network面板中仔细查看滑动验证码加载时或之前的所有请求。寻找响应体中包含key、secret、token等字段的请求。可能需要将session_id与密钥进行关联。动态计算在JS代码中搜索密钥变量名查看它的赋值语句。它可能由window.xxx、localStorage、或者一个函数调用结果赋值。使用断点调试追踪这个变量的值是如何计算出来的。常见算法包括对固定字符串进行HMAC-SHA256哈希、对session_id和某个salt进行拼接后哈希等。5.3 反调试与代码混淆对抗一些防护较强的网站会采用反调试手段例如在开发者工具打开时无限debugger、代码混淆得面目全非等。无限Debugger在Sources面板找到触发debugger的代码行右键选择“Never pause here”或者通过条件断点将其禁用。高强度混淆对于变量名混淆如_0x1a2b耐心是关键。重点关注字符串常量和函数调用。混淆通常不会改变字符串和API调用如CryptoJS.AES.encrypt、JSON.stringify、XMLHttpRequest.send。通过搜索这些字符串和API名可以定位到关键代码区域。然后通过断点观察这些函数被调用时传入的参数来理解周围的混淆逻辑。代码流平坦化与控制流混淆这是更高级的混淆将代码逻辑打乱成巨大的switch-case结构。对付这种动态调试断点、单步执行比静态分析更有效。耐心地跟着程序流走记录下关键的分支和变量变化。5.4 请求校验与风控绕过即使加密通过了服务端还可能有多重校验。请求签名除了enc_data请求头或请求体中可能还有一个signature字段由其他所有参数包括enc_data按照特定规则排序后加上一个密钥进行HMAC-SHA256等哈希生成。你需要逆向出签名算法。时间戳与时效性timestamp参数可能被要求与服务器时间差在一定范围内。你的程序需要获取服务器时间或使用同步的时间戳。请求频率与行为指纹过于频繁的请求、完全一致的滑动轨迹、缺少正常浏览器指纹如User-Agent,Accept-Language等会被风控系统拦截。需要在自动化脚本中模拟更真实的人类行为并管理好请求间隔。可以考虑使用selenium或playwright等浏览器自动化工具来执行部分操作以携带更完整的浏览器环境。深度排查技巧建立一个“比对验证”流程。在本地用Node.js搭建一个简单的CryptoJS加密环境将前端抓取到的密钥、IV、明文分别在Node.js环境和Python环境中进行加密比对结果。这能帮你快速定位是密钥处理问题、加密库差异问题还是其他问题。因为Node.js的Crypto模块或直接使用CryptoJS库其行为与浏览器环境几乎一致可以作为“标准答案”来校准你的Python复现代码。逆向工程破解AES加密的滑动验证码是一个系统性的工程考验的是耐心、细心和对细节的把握。从网络抓包到代码分析再到算法复现每一步都可能遇到坑。成功的关键在于严谨的比对和逻辑推理——确保你的复现环境在每一个输入和参数上都与原始前端环境保持绝对一致。这个过程不仅能让你成功绕过某个具体的验证码更能深刻理解前端安全措施的实现与局限提升你的整体安全技术视野与分析能力。记住技术的提升来自于对每一个技术细节的深究和攻克。