前端AES加密逆向实战:从4399游戏抓包到Python解密全解析
1. 项目缘起一次偶然的“寻宝”之旅那天我像往常一样在分析一些网络应用的交互逻辑无意间点开了一个基于4399平台的小游戏。我的目的很简单就是想看看它的资源加载和分数上传机制是怎么实现的。作为一名老“手艺人”我习惯性地打开了浏览器的开发者工具切换到Network网络面板准备看看HTTP请求的“庐山真面目”。很快我发现了一个有趣的请求。那是一个向服务器提交游戏分数的POST请求但一眼望去请求体Request Payload里不是预想中明明白白的score100这样的键值对而是一长串毫无规律、看起来像乱码的字符串。经验告诉我这背后肯定有故事——客户端对数据进行了某种加密处理以防止被轻易篡改或窥探。这瞬间勾起了我的好奇心一个看似简单的休闲小游戏为什么要对分数这种数据进行加密它用了什么加密方式密钥又藏在哪里这次“寻宝”解密之旅就这样开始了。这不仅仅是破解一个加密字符串那么简单。它涉及到对前端JavaScript代码的逆向分析、对加密算法的识别、对密钥查找逻辑的追踪最终目标是理解其完整的“加密-传输-解密”流程。这个过程对于学习Web安全、前端逆向工程甚至理解一些基础的密码学应用场景都大有裨益。无论你是对安全感兴趣的开发者还是想了解网络数据如何被保护的爱好者这次实战记录都能提供一个清晰的视角。2. 前期侦察抓包与初步分析动手之前得先搞清楚“战场”情况。我的首要工具就是浏览器的开发者工具特别是Network面板和Sources面板。2.1 锁定目标请求刷新游戏页面玩上一局然后在Network面板中仔细筛选。通常提交分数的请求会发生在游戏结束或暂停时。通过观察请求的URL路径可能包含submitScore、save、report等关键字、请求方法POST居多以及发起时机我很快定位到了那个“可疑”的请求。点击这个请求查看其Headers和Payload。在Payload里我看到了本次分析的核心目标一个名为data或encryptedData的字段其值就是一长串密文。它可能看起来像这样U2FsdGVkX12p73qRr5Np1wQv4lLmZzX7K9oPqA同时我也留意了请求的Content-Type常见的是application/x-www-form-urlencoded或application/json。这决定了数据是如何被组装的。注意有些加密可能会将其他参数如时间戳、用户ID也一起加密或者将加密后的数据作为某个JSON对象的一个属性值。务必查看完整的请求体结构。2.2 逆向JavaScript代码密文找到了加密逻辑必然写在网页加载的JavaScript文件里。接下来就是“大海捞针”找到负责加密的那几行关键代码。全局搜索关键词在Sources面板中打开所有加载的.js文件使用全局搜索CtrlShiftF。搜索的关键词可以包括密文字段名如data、encryptedData加密相关函数名如encrypt、encode、CryptoJS、AES、DES、RSA可能用于加密的库名如CryptoJS、forge、sjcl提交请求的函数如XMLHttpRequest、fetch、$.ajax设置断点动态调试如果全局搜索效果不佳或者代码被混淆得难以阅读动态调试是更有效的方法。在Network面板中找到目标请求右键选择“Replay XHR”在某些浏览器中或“Copy as fetch”然后修改后执行但这需要知道确切参数。更通用的方法是在发起该请求的代码行上设置断点。在Sources面板找到疑似发起请求的代码文件通常是一个主游戏JS或一个专门的API模块。在所有XMLHttpRequest.send()或fetch()调用附近或者在对参数进行JSON.stringify()的操作前设置断点。重新触发请求如再玩一局游戏代码执行会在断点处暂停。此时你可以查看调用堆栈Call Stack一步步回溯找到加密发生的位置。格式化混淆代码前端代码为了压缩和保护经常被混淆变量名变成a,b,c代码挤成一团。Chrome等浏览器的开发者工具通常自带一个{}Pretty Print按钮点击后可以将混淆的代码格式化得稍微易读一些虽然变量名无法恢复但代码结构会清晰很多便于设置断点和跟踪逻辑。3. 核心战场加密算法识别与密钥追踪经过一番搜索和调试我终于在某个庞大的、被混淆过的游戏主JS文件中找到了加密相关的代码片段。代码虽然被压缩但一些关键的结构和字符串常量仍然暴露了信息。3.1 识别加密算法常见的Web前端加密算法和库有其特征CryptoJS这是最常用的前端加密库之一。如果代码中存在CryptoJS.AES.encrypt()、CryptoJS.MD5()、CryptoJS.enc.Base64.stringify()这样的调用那就非常明确了。即使被混淆CryptoJS这个对象名通常会被保留或者你能看到AES、DES、TripleDES、PBKDF2等算法名作为字符串出现。AES加密特征AES加密通常需要密钥Key、初始化向量IV和模式如CBC、ECB。在代码中你可能会看到mode: CryptoJS.mode.CBCpadding: CryptoJS.pad.Pkcs7等配置对象。AES加密后的输出经过Base64编码经常会以U2FsdGVkX1开头这是OpenSSL格式的Salt头当使用CryptoJS的默认加密方式时会产生。我遇到的这个4399游戏的密文开头正是U2FsdGVkX1这几乎是指向CryptoJS AES加密的“指纹”。自定义或简单编码有时开发者会使用简单的XOR异或运算、自定义的字符替换表或者结合Base64进行“加密”。这类算法在代码中看起来逻辑比较简单可能就是一个循环处理每个字符。在我的案例中通过搜索encrypt和查看格式化后的代码逻辑我确认了它使用了CryptoJS.AES.encrypt方法。3.2 追踪密钥来源知道了算法下一步就是找到密钥Key和IV。这是整个解密过程中最具挑战性的一环。密钥不会硬编码在明显的位置虽然有时确实会那是最简单的情况它可能硬编码在代码中以字符串常量形式存在。搜索key、secret、iv等字符串查看其赋值。有时密钥会被拆分成几段然后用连接起来。从服务器动态获取游戏初始化时会通过一个单独的API请求从服务器获取一个临时密钥或密钥种子seed。你需要找到这个请求并查看其响应内容。由固定字符串推导而来密钥可能是某个固定字符串如游戏ID、一个常量的MD5或SHA256哈希值。在代码中你会看到类似CryptoJS.MD5(some_fixed_string).toString()的结果被用作密钥。与用户信息相关密钥可能由用户ID、会话Token等动态信息参与生成。我采用的方法是在找到的CryptoJS.AES.encrypt(data, key, config)调用处设置断点。当断点触发时在控制台Console中打印出key和config变量的值。通过这种方式我清晰地看到key是一个字符串看起来像a1b2c3d4e5f6g7h8此处为示例非真实密钥。config是一个对象包含了{ iv: someIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }。接下来我需要向上回溯这个key和iv是怎么来的。在调用堆栈中一步步向上查看发现key是由一个全局变量window.gameSecret赋值的。而window.gameSecret是在页面加载初期由另一个初始化脚本从HTML的script标签内的一段JSON配置中读取的。这属于上述第1种情况硬编码但藏得比较深。实操心得逆向过程中不要只看代码要多用调试器。在关键函数入口设置断点然后观察变量状态、单步执行F10、步入函数F11是理解程序流最直接的方法。对于混淆代码关注字符串常量和函数调用模式比试图理解每一行代码更高效。4. 解密验证从理论到实践拿到了密文、算法、密钥和IV就可以进行解密验证了。验证环境可以选择浏览器控制台也可以使用Node.js或Python等后端语言确保我们的理解是正确的。4.1 在浏览器控制台验证由于加密使用的是CryptoJS而游戏页面已经加载了CryptoJS库我们可以直接在浏览器的开发者工具Console面板中操作。提取必要信息确保你已从调试中获取了以下信息ciphertext加密后的字符串Base64格式。key加密使用的密钥字符串或WordArray。iv初始化向量字符串或WordArray。mode加密模式如CryptoJS.mode.CBC。padding填充方式如CryptoJS.pad.Pkcs7。执行解密在Console中输入以下命令假设使用CBC模式和Pkcs7填充// 假设密钥和IV是字符串 var key a1b2c3d4e5f6g7h8; var iv 1234567890123456; var ciphertext U2FsdGVkX12p73qRr5Np1wQv4lLmZzX7K9oPqA; // 将字符串转换为CryptoJS可用的格式 var keyWA CryptoJS.enc.Utf8.parse(key); var ivWA CryptoJS.enc.Utf8.parse(iv); // 解密 var decrypted CryptoJS.AES.decrypt(ciphertext, keyWA, { iv: ivWA, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 将解密结果转换为UTF-8字符串 var plaintext decrypted.toString(CryptoJS.enc.Utf8); console.log(解密结果, plaintext);分析结果如果解密成功plaintext将会是原始的明文数据很可能是一个JSON字符串例如{score: 1500, time: 120, uid: player123}。这证明你对加密流程的分析是完全正确的。4.2 使用Python进行离线解密为了更通用或者用于编写自动化脚本我们可以在Python环境中复现解密过程。这需要安装pycryptodome库。pip install pycryptodome然后编写Python脚本from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 def decrypt_4399_data(ciphertext_b64, key_str, iv_str): 解密4399游戏AES加密数据 :param ciphertext_b64: Base64编码的密文 :param key_str: 密钥字符串 :param iv_str: 初始化向量字符串 :return: 解密后的明文字符串 # 1. 将Base64密文解码为字节 ciphertext_bytes base64.b64decode(ciphertext_b64) # 注意如果密文以 U2FsdGVkX1 开头它是OpenSSL格式包含Salt。 # CryptoJS.encrypt 默认会产生这种格式。但我们在代码中指定了key和iv时它使用的是无Salt的格式。 # 我们之前分析的是指定了key和iv的AES-CBC所以这里直接按无Salt处理。 # 如果遇到带Salt的需要更复杂的处理使用CryptoJS的KDF推导密钥。 # 2. 将密钥和IV从字符串转换为字节并确保长度正确AES-128为16字节AES-256为32字节 key_bytes key_str.encode(utf-8) iv_bytes iv_str.encode(utf-8) # 检查长度如果不够可能需要用特定方式填充如用0补齐这取决于原JS代码的实现。 # 常见的是直接使用UTF-8字节如果长度不对CryptoJS内部可能会进行哈希处理。 # 最准确的方式是模拟CryptoJS的行为CryptoJS.enc.Utf8.parse(key) 会生成一个WordArray # 在作为key传入时CryptoJS会根据key的字节长度自动选择AES-128/192/256。 # 为了简单起见这里假设key_str和iv_str的长度已经是16/24/32字节或CryptoJS能正确处理的长度。 # 如果解密失败可能需要打印key_bytes和iv_bytes的长度进行调试。 # 3. 创建AES解密器 cipher AES.new(key_bytes, AES.MODE_CBC, iv_bytes) # 4. 解密并去除填充 decrypted_padded_bytes cipher.decrypt(ciphertext_bytes) decrypted_bytes unpad(decrypted_padded_bytes, AES.block_size) # 使用PKCS7去除填充 # 5. 将解密后的字节转换为字符串 plaintext decrypted_bytes.decode(utf-8) return plaintext # 示例使用 if __name__ __main__: # 替换成你找到的真实数据 ciphertext U2FsdGVkX12p73qRr5Np1wQv4lLmZzX7K9oPqA key a1b2c3d4e5f6g7h8 iv 1234567890123456 try: result decrypt_4399_data(ciphertext, key, iv) print(解密成功, result) except Exception as e: print(解密失败, e) print(请检查1. 密文是否为标准Base64。2. 密钥/IV长度和值是否正确。3. 加密模式/填充是否匹配。)注意事项Python的pycryptodome库和JavaScript的CryptoJS在默认行为上可能有细微差别特别是密钥处理上。CryptoJS的Utf8.parse会将字符串转换成WordArray一种特殊的字节数组而Python中我们直接使用字符串的UTF-8字节。如果遇到解密失败首要检查密钥和IV的字节表示是否完全一致。一个有效的调试方法是在JS控制台用CryptoJS.enc.Utf8.parse(key).toString()查看其16进制表示然后在Python中确保key.encode(utf-8).hex()得到相同的结果。5. 流程复盘与安全思考至此整个“寻密-解密”的流程就完整走通了。我们来复盘一下核心步骤抓包定位使用浏览器开发者工具找到携带加密数据的网络请求。代码逆向在Sources面板中搜索、调试JavaScript代码定位加密函数调用点。算法识别通过函数名、库名如CryptoJS、密文特征如U2FsdGVkX1确定加密算法本例为AES-CBC。密钥追踪通过断点调试、调用堆栈回溯找到密钥和IV的生成与来源本例来自页面内嵌的全局变量。解密验证在浏览器控制台或使用Python脚本使用获得的算法、密钥、IV对密文进行解密验证结果是否为可读的明文数据如JSON。这个过程揭示了一个常见的Web前端安全模型“防君子不防小人”。前端代码和密钥对用户是透明的任何有一定技术能力的用户都可以通过类似的方法找到密钥。因此这种前端加密的主要目的通常不是保证数据的绝对机密性而是增加篡改难度防止普通用户通过简单修改请求参数如把分数从100改成99999来作弊。要作弊必须先完成逆向分析。保证数据完整性加密过程往往也保证了数据在传输过程中不被意外损坏尽管哈希更常用于此目的。满足合规要求对传输中的敏感数据尽管在前端加密意义有限进行一定程度的保护。对于真正需要高安全性的场景如支付、核心用户信息依赖前端加密是远远不够的。必须采用HTTPS来保证传输层安全并且敏感操作应在后端使用非对称加密如RSA或建立安全的会话密钥协商机制。前端加密更像是整个安全链条中一个可被绕过、但能增加攻击成本的环节。6. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到各种坑。下面是我总结的一些常见问题及解决办法问题现象可能原因排查思路与解决方案在Console中使用CryptoJS解密报错1. CryptoJS库未加载或加载不完全。2. 密钥/IV格式不正确不是WordArray。3. 密文不是标准的Base64字符串可能包含URL编码字符如/。1. 确保在包含CryptoJS的页面执行。可以尝试在Console输入CryptoJS看是否定义。2. 使用CryptoJS.enc.Utf8.parse()或CryptoJS.enc.Hex.parse()将字符串密钥转换为WordArray。3. 对密文进行Base64解码测试atob(ciphertext)如果报错可能需要先进行URL解码decodeURIComponent(ciphertext)或替换掉-为_为/。Python解密失败提示Padding is incorrect或ValueError1. 密钥、IV或密文错误。2. JS和Python的密钥/IV字节表示不一致。3. 加密模式或填充方式不匹配。4. 密文包含SaltU2FsdGVkX1开头但Python代码按无Salt处理。1.核对字节在JS控制台打印CryptoJS.enc.Utf8.parse(key).toString()和CryptoJS.enc.Utf8.parse(iv).toString()得到16进制串。在Python中确保key.encode(utf-8).hex()和iv.encode(utf-8).hex()与之完全相同。2.检查模式填充确认JS代码中使用的mode和padding与Python代码中AES.new(modeAES.MODE_CBC)和unpad(..., AES.block_size)对应。3.处理Salt如果密文带Salt说明CryptoJS使用了基于密码的加密CryptoJS.AES.encrypt(plaintext, password)。这时需要用CryptoJS.kdf.OpenSSL.execute方式推导密钥或在Python中使用Crypto.Protocol.KDF.PBKDF2模拟。这种情况更复杂需要分析JS中是否传入了password字符串而非keyWordArray。找不到加密函数或密钥1. 代码混淆严重函数名和变量名无法识别。2. 加密逻辑被隐藏在WebAssembly或重度混淆的模块中。3. 密钥通过WebSocket或其它非HTTP方式动态获取。1.关注字符串和网络即使代码混淆用于加密算法标识的字符串如AES、encrypt和发送请求的URL字符串通常不会被混淆。以此作为突破口。2.XHR/Fetch断点在开发者工具的Sources面板切换到XHR/Fetch Breakpoints添加目标请求的URL部分。当任何请求匹配该URL时调试器会暂停直接跳到发起请求的代码处再向上回溯查找参数构造过程。3.Hook关键函数在Console中覆写XMLHttpRequest.prototype.send或fetch在函数被调用时打印其参数可以捕获到加密前的原始数据。解密出的明文是乱码1. 解密成功但明文不是UTF-8编码的JSON可能是其它二进制格式或编码。2. 实际上解密并未完全成功可能是密钥错误导致解密出了无意义的字节。1. 尝试将解密出的字节用hex()或repr()打印出来看是否有可识别的模式如JSON以大括号{开头。2. 检查解密后的字节长度如果恰好是16/32/48等AES块大小的倍数可能没有正确去除填充或者填充方式不对。尝试不同的unpad逻辑或直接查看未去填充的原始解密结果。独家避坑技巧从结果反推如果你已经通过抓包知道了明文大概是什么比如你提交了一个分数100那么你可以尝试用这个明文配合你找到的密钥算法去加密看生成的密文是否和抓到的包一致。这是验证你对加密流程理解是否正确的终极方法。善用“监听器”除了在请求发送时断点还可以在Object.defineProperty上设置断点来监听对特定对象属性如window.gameSecret的赋值操作这对于追踪动态生成的密钥非常有效。保持耐心记录过程逆向工程就像侦探破案把每一步的发现找到的变量名、函数调用关系、关键字符串都记录下来画个简单的流程图会极大帮助理清思路。

相关新闻