1. 项目概述:从加密算法到网页逆向的实战桥梁
在网页逆向分析这个领域里,你经常会遇到一堆看似杂乱无章的字符串,它们可能是登录时的密码参数,也可能是请求数据包里的一个神秘签名。很多时候,这些字符串就是由MD5、SHA、HMAC这类加密算法处理后的结果。不理解它们,逆向工作就像在黑暗中摸索,寸步难行。今天,我们不谈高深的理论,就从一名逆向工程师的实战视角,拆解这些最常见的加密算法,看看它们是如何在网页中“兴风作浪”,而我们又该如何“见招拆招”。
简单来说,MD5、SHA、HMAC都属于密码学中的哈希函数或消息认证码范畴。它们不是用来“加密”后还能“解密”的(那是AES、RSA的活儿),而是将任意长度的输入,通过一个不可逆的数学变换,映射成一个固定长度的、看起来像乱码的输出,我们称之为“摘要”或“哈希值”。在网页中,它们常被用于密码存储、数据完整性校验、生成API签名或请求令牌。逆向分析的核心任务之一,就是识别出前端JavaScript代码中使用了哪种算法、密钥是什么、对哪些数据进行了运算,从而在本地复现这个计算过程,模拟出合法的请求。
2. 核心加密算法原理与逆向特征拆解
要逆向,先得正向理解。我们得知道这些算法在“正常”情况下是怎么工作的,才能在被混淆、压缩的代码中找到它们的蛛丝马迹。
2.1 MD5算法:曾经的王者与如今的“指纹”
MD5(Message-Digest Algorithm 5)生成一个128位(16字节)的哈希值,通常表示为32个十六进制字符。它的计算过程包括填充、分块、循环压缩等。虽然因其抗碰撞性已被攻破(即能找到两个不同的输入产生相同的MD5值),不再推荐用于密码等安全场景,但在网页逆向中,它依然无处不在。
逆向识别特征:
- 常量特征:MD5算法内部使用4个固定的32位幻数(A=0x67452301, B=0xEFCDAB89, C=0x98BADCFE, D=0x10325476)。在JavaScript代码中,如果看到这四个常量的定义,极大概率是MD5。
- 函数名特征:常见的JavaScript MD5库会暴露函数如
md5()、hex_md5()、b64_md5()等。 - 输出长度:32位十六进制字符串是最明显的标志。
实战注意事项:
MD5是单向哈希,理论上不能解密。网上所谓的“MD5解密”网站,实际上是庞大的“彩虹表”查询,即预先计算好海量明文和其MD5的对应关系,通过查询哈希值反推明文。对于加了“盐”(salt)的MD5,这种查询就失效了。在逆向时,我们的目标不是“解密”,而是找到生成这个MD5值的原始数据和算法代码。
2.2 SHA家族算法:更安全的哈希标准
SHA(Secure Hash Algorithm)系列比MD5更安全,常见的有SHA-1(160位输出)、SHA-256(256位输出)、SHA-512(512位输出)等。数字越长,安全性一般越高,计算也稍慢。
逆向识别特征:
- 常量特征:SHA-256使用8个初始化哈希值(如
0x6a09e667),SHA-512使用8个64位初始化哈希值。这些固定的魔数是代码中的路标。 - 函数名:如
sha1(),sha256(),sha512(),CryptoJS.SHA256()(如果使用CryptoJS库)。 - 输出长度:SHA-1是40位十六进制,SHA-256是64位,SHA-512是128位。这是快速区分的利器。
实操心得:在逆向中,看到长度40位的哈希,先怀疑SHA-1;看到64位的,优先考虑SHA-256。很多网站的API签名已经从MD5升级到了SHA-256,因为它更抗碰撞。如果代码中引用了crypto-js或forge这类库,搜索SHA256关键词往往能快速定位加密点。
2.3 HMAC算法:带密钥的哈希验证
HMAC(Hash-based Message Authentication Code)是关键所在。它不是独立的算法,而是利用MD5、SHA-1、SHA-256等哈希函数,结合一个密钥,来生成消息认证码。它可以确保数据在传输过程中未被篡改,并且发送方拥有正确的密钥。公式可以简化为:HMAC = Hash( (Key XOR opad) + Hash( (Key XOR ipad) + Message ) )。你不用记这个,但要知道核心是密钥(Key)。
逆向识别特征与核心挑战:
- 函数调用:常见形式如
CryptoJS.HmacSHA256(message, key)、hmac-sha256库的调用。 - 寻找密钥:这是逆向HMAC最困难也最关键的一步。密钥可能被硬编码在JavaScript文件里(经过混淆),也可能由前端通过其他接口动态获取,甚至是由用户密码等派生而来。你需要仔细追踪生成HMAC的函数的第二个参数来源。
- 流程固定:HMAC一定是先有原始数据(消息)和密钥,然后调用特定哈希函数生成结果。在代码中寻找这种“二元输入”的哈希计算点。
一个关键技巧:在Chrome DevTools的Sources面板中,对疑似加密函数(如encrypt、sign、getToken)设置断点,然后触发网络请求。当断点命中时,观察调用栈(Call Stack)和该函数的参数(Scope),密钥和待加密的原始数据很可能就在眼前。这是动态调试优于静态分析的地方。
3. 网页逆向中的加密算法实战定位与分析
理论懂了,我们进入实战环节。假设我们要对一个登录请求进行逆向,发现其密码或一个sign参数被加密了。
3.1 第一步:网络抓包与初步判断
打开浏览器的开发者工具(F12),切换到Network(网络)选项卡,勾选Preserve log(保留日志)。进行登录操作,观察发出的POST请求。
- 查看
Form Data或Payload,寻找可疑参数。例如,password可能是一串32位十六进制数(MD5),signature可能是一串64位十六进制数(SHA-256/HMAC-SHA256)。 - 特别注意像
_t(时间戳)、nonce(随机数)这类参数,它们经常是生成签名的一部分。
3.2 第二步:全局搜索与静态分析
在Sources面板中,对整个网页的JavaScript代码进行搜索(Ctrl+Shift+F)。
- 搜索关键词:从网络热词和算法特征入手。例如:
md5、sha256、hmac、encrypt、sign、CryptoJS(一个非常流行的前端加密库)、hex、digest。也可以直接搜索你抓包到的那个加密字符串,看它是否作为常量出现在代码中。 - 分析代码结构:找到相关函数后,不要只看那几行。向上追溯,看这个函数的参数从哪里来,密钥是如何定义的。向下查看,看生成的哈希值被用在了哪里(通常是赋值给某个请求参数)。
3.3 第三步:动态调试与参数追踪
静态分析遇到混淆代码时,动态调试是唯一出路。
- XHR/Fetch断点:在Network面板,找到那个加密的请求,右键点击,选择“Break on” -> “XHR/fetch Breakpoints”。这样当JavaScript发起这个特定请求时,会自动暂停。
- 事件监听器断点:在Sources面板的右侧,展开“Event Listener Breakpoints”,勾选“Script”下的“Script First Statement”,或者“Mouse”下的“click”事件(如果加密由点击触发)。这有助于从用户交互点开始跟踪。
- 手动断点:在静态分析找到的疑似加密函数入口处打上断点(点击行号)。 当代码暂停后,利用Step Over(F10)、Step Into(F11)逐行执行,同时在Scope面板和Console面板观察变量值的变化。重点关注传递给加密函数的明文和密钥。
3.4 第四步:本地复现与验证
一旦找到了加密算法、密钥和明文组合方式,就需要在本地(比如用Python的hashlib、hmac库,或Node.js环境)复现这个计算过程。
import hashlib import hmac # 示例:复现一个可能的HMAC-SHA256签名 def generate_sign(api_key, secret, timestamp, request_body): message = f"{api_key}{timestamp}{request_body}" sign = hmac.new(secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest() return sign # 使用抓包到的参数进行测试 my_sign = generate_sign('your_api_key', 'your_secret_key', '1648886400000', '{"page":1}') print(my_sign) # 对比抓包到的sign值如果本地计算的结果与抓包到的值一致,恭喜你,逆向成功。你可以用这个函数生成任意请求所需的签名了。
4. 复杂场景与组合加密策略的应对
网页开发者为了增加逆向难度,不会只使用单一的MD5。他们会组合使用多种技术,这正是开头搜索片段中提到的“几种加密方式配合次序”的实战体现。
4.1 常见组合模式解析
- 哈希加盐(Salt):
md5(password + salt)。盐是一个随机字符串,与密码拼接后再哈希。这彻底杜绝了彩虹表攻击。逆向时,你需要找到这个salt,它可能藏在另一个接口的响应里,或者是一个前端固定的值。 - 多次哈希与迭代:
sha256(sha256(password)),或者进行上千次的迭代哈希以对抗暴力破解。这在密码存储中常见。逆向登录时,你需要模拟同样的迭代次数。 - HMAC与动态参数:
sign = hmac-sha256(排序后的所有参数 + 密钥)。这是API签名的经典模式。难点在于参数排序规则(通常按字典序)和拼接格式(如key1=value1&key2=value2)。 - 非对称加密管理对称密钥:正如搜索片段所言,这是更高级的模式。例如,前端用RSA公钥加密一个随机生成的AES密钥,然后用这个AES密钥加密请求体。服务器用私钥解密出AES密钥,再解密请求。逆向这种模式,关键在于找到RSA公钥和AES密钥的生成逻辑。
4.2 逆向组合加密的思维导图
面对复杂加密,需要一个清晰的排查思路:
- 确定最终输出:抓包,确定需要破解的那个加密参数(如
encryptedData,sign)。 - 识别算法家族:通过输出长度、特征初步判断是MD5、SHA还是HMAC。
- 定位加密函数:通过搜索关键词、断点追踪,找到生成该参数的JavaScript函数入口。
- 逆向输入流:从加密函数出发,反向追踪每一个参数的来源。
- 明文数据从哪里来?(表单输入、页面元素、其他接口返回值)
- 密钥从哪里来?(硬编码、接口获取、本地计算派生)
- 盐(Salt)或初始向量(IV)从哪里来?
- 是否有额外的处理步骤?(Base64编码、十六进制转换、字符串排序、截取部分结果)
- 绘制加密流程图:在纸上或脑图中画出数据流动和变换的完整路径。
- 分步复现验证:按照流程图,从第一步开始,在本地逐步实现,每一步都与调试时看到的中途结果对比,确保无误。
5. 常用工具链与调试技巧实录
工欲善其事,必先利其器。除了浏览器自带的DevTools,还有一些工具能极大提升逆向效率。
5.1 浏览器插件
- ReRes:可以拦截浏览器请求,将线上JavaScript文件替换为本地的修改版,方便你插入调试代码或直接修改函数逻辑进行测试。
- EditThisCookie:直接查看和编辑Cookie,有时签名或令牌会放在Cookie里。
- JavaScript Source Tracker(或类似概念):有些插件能帮你可视化JavaScript文件的依赖关系和函数调用链,对理解大型项目结构有帮助。
5.2 本地模拟环境
- Node.js:对于复杂的、依赖Node环境或npm包的前端代码,直接在Node里运行调试是最准确的。你可以把关键的加密函数片段提取出来,在Node中运行并打印中间变量。
- Python + ExecJS/PyExecJS:如果你更熟悉Python,可以用这个库来执行JavaScript代码片段。这样你的爬虫或自动化脚本可以直接调用前端的加密逻辑,无需完全用Python重写。
import execjs # 假设你提取出了关键的js加密函数,保存为 sign.js with open('sign.js', 'r', encoding='utf-8') as f: js_code = f.read() ctx = execjs.compile(js_code) result = ctx.call('generateSign', 'your_data', 'your_key') print(result)
5.3 高级调试技巧
- Hook函数:在控制台直接重写关键函数,让其输出调用参数和结果。这是“暴力”但有效的方法。
// 假设原加密函数是 window.encrypt var old_encrypt = window.encrypt; window.encrypt = function(data, key) { console.log('encrypt被调用,数据:', data, '密钥:', key); var result = old_encrypt(data, key); console.log('加密结果:', result); return result; }; - 内存断点(Watchpoint):如果你知道密钥存储在某个变量或对象属性中,可以在DevTools的Memory面板或通过代码对其设置“访问/修改”断点,当代码读取或修改这个密钥时就会暂停。
- 格式化混淆代码:面对压缩成一行的代码,点击DevTools源代码面板左下角的
{}(美化)按钮是第一步。对于更复杂的混淆(变量名替换、控制流平坦化),可能需要使用专门的去混淆工具或耐心分析。
6. 常见问题排查与避坑指南
在实际操作中,你肯定会遇到各种问题。这里记录一些典型的坑和解决方案。
6.1 问题:本地复现的签名和服务器的总是对不上
排查思路:
- 编码问题(最常见):确保你本地和前端JavaScript处理字符串的编码一致。前端通常使用UTF-8。在Python中,在哈希前明确使用
.encode('utf-8')。特别注意中文等非ASCII字符。 - 参数顺序或格式:检查拼接参数的顺序、中间是否有连接符(如
&、|)、是否对键值对进行了URL编码。服务器端的拼接逻辑必须和你本地完全一致。 - 密钥或盐错误:再次确认你找到的密钥是否正确、完整。是否在传输前经过了Base64解码或其他变换?动态获取的密钥是否已过期?
- 算法或迭代次数错误:确认哈希算法是否准确(是SHA-256还是SHA-256-224?)。确认哈希迭代了 exactly 多少次。
- 时间戳或随机数的同步:如果签名包含时间戳,检查本地生成的时间戳和服务器时间是否在允许的误差范围内。随机数(nonce)是否每次请求都重新生成。
6.2 问题:断点打上了,但代码不暂停
可能原因与解决:
- 代码被动态加载或eval执行:代码可能通过
eval()、setTimeout或动态创建<script>标签加载,导致源映射丢失。尝试在“Script First Statement”事件上设置断点,或者搜索代码片段在Network中对应的JS文件,直接在该文件上打点。 - 断点被反调试技巧绕过:有些网站会检测开发者工具,并跳入无限debugger循环或使用异步函数扰乱调试。可以尝试使用“停用断点”功能先跳过无限循环,或者使用插件禁用反调试。更高级的方法是修改JavaScript文件,直接注释掉反调试代码。
- 断点位置不对:你可能在压缩后的代码行上打了断点,但实际执行的是另一份源文件。确保在正确的文件(通常是经过美化后的主文件)上操作。
6.3 问题:密钥被严重混淆,难以追踪
应对策略:
- 动态拦截:如果密钥是在运行时通过Ajax请求获取的,在Network面板找到那个请求,直接查看响应体。这比从混淆的JS代码里找容易得多。
- 内存搜索:如果密钥最终会被加载到内存中的一个变量里,可以在代码运行到某个阶段后,在Console中使用
Object.keys(window)查看全局对象,或者对已知的容器对象进行递归搜索,寻找符合密钥特征(长度、字符集)的字符串。 - 日志大法:如果能在关键位置插入代码(通过ReRes替换文件),就在所有可能的密钥赋值语句前后加入
console.log,进行“地毯式”输出。
逆向分析是一场与开发者斗智斗勇的过程。MD5、SHA、HMAC这些算法是工具,理解它们是基础。真正的功夫在于耐心、细致的代码追踪和逻辑推理。每一次成功的逆向,都是对网页应用安全机制的一次深刻理解。记住,我们的目的不是破坏,而是学习和实现合法的自动化交互。随着经验的积累,你会逐渐形成自己的直觉和工具箱,面对新的加密挑战时也能更加从容。