1. 项目概述:从一次登录请求说起

最近在带新人入门JS逆向,发现很多朋友一上来就想挑战高难度目标,结果往往在第一步就卡住了。我的建议是,先从一些经典的、防护相对简单的站点入手,理解JS逆向的基本流程和核心思想。今天我们就拿国内知名的开发者社区osChina(开源中国)的登录密码加密作为练手案例。这个案例非常典型,它涉及了常见的加密算法、前端混淆以及如何定位关键加密函数,对于初学者来说,既能学到东西,又不会因为过于复杂的反爬机制而劝退。

当你打开osChina的登录页面,输入账号密码点击登录时,浏览器会向服务器发送一个POST请求。如果你打开开发者工具的Network面板,仔细查看这个请求的载荷(Payload),你会发现密码并不是明文传输的,而是一串看起来毫无规律的字符。我们的目标,就是弄清楚这串字符是如何由你的明文密码“123456”演变而来的。这个过程,就是JS逆向的核心——逆向推导前端JavaScript代码的加密逻辑,并用其他编程语言(如Python)复现这一过程,从而实现自动化登录或数据采集。通过这个案例,你将掌握如何分析网络请求、如何搜索和定位关键加密代码、如何理解简单的混淆,并最终写出可用的Python代码。

2. 逆向分析前的准备工作与思路拆解

在真正动手抠代码之前,做好充分的准备和思路规划能事半功倍。逆向不是瞎碰运气,而是一个有逻辑的推理过程。

2.1 工具与环境准备

工欲善其事,必先利其器。对于JS逆向,以下几样工具是必不可少的:

  1. 浏览器开发者工具 :推荐使用Chrome或Edge。核心功能在于 Network (网络)面板和 Sources (源代码)面板。 Network 面板用于捕获和分析所有的网络请求,是我们找到加密接口的入口。 Sources 面板则用于查看、调试页面加载的所有JavaScript文件。
  2. 代码编辑与调试工具 :对于找到的JS代码,我们经常需要格式化、搜索和简单调试。浏览器自带的调试器就非常强大。此外,像VS Code这样的编辑器,配合Prettier插件,可以快速格式化混乱的JS代码,提升可读性。
  3. Node.js环境 :有时,为了验证我们提取的加密函数是否正确,我们需要在本地用Node.js执行一下这段JS代码,看输出结果是否和浏览器一致。这是验证逻辑的关键一步。
  4. Python环境及相关库 :我们的最终目的是用Python复现加密。需要准备 requests 库用于发送网络请求, execjs 库或 PyExecJS 库用于执行我们提取出来的JavaScript加密函数。我个人更倾向于 execjs ,因为它调用本地安装的Node.js或JavaScript引擎,兼容性更好。

注意 :在安装 execjs 时,请确保你的系统已经安装了Node.js。你可以通过在命令行输入 node -v 来检查。没有的话,先去Node.js官网下载安装。

2.2 核心逆向思路与步骤规划

面对一个加密参数,标准的分析流程可以归纳为以下几步,这个流程适用于大多数简单的JS逆向场景:

  1. 抓包定位 :首先,通过浏览器开发者工具的Network面板,捕获到包含加密参数的请求。找到它,并确认加密参数的名字(本例中是 password )。
  2. 全局搜索 :在Sources面板中,对所有加载的JS文件进行全局搜索(快捷键 Ctrl+Shift+F ),搜索关键词可以是加密参数名 password ,也可以是加密后字符串的片段。这是定位加密代码最常用、最有效的方法。
  3. 断点调试 :在搜索到的疑似加密代码行设置断点,重新触发登录动作,让代码执行到断点处暂停。然后通过 Call Stack (调用堆栈)和单步调试( F10 步过, F11 步入),一步步追踪数据的流向,找到最核心的加密函数。
  4. 逻辑分析与提取 :理解核心加密函数的输入、输出和内部逻辑。将这个函数以及它依赖的其他函数、变量,从原JS文件中完整地提取出来。
  5. 本地验证与复现 :将提取的JS代码在Node.js环境中运行,输入测试密码,看输出是否与浏览器捕获的加密结果一致。确认无误后,用Python的 execjs 库调用这段JS代码,完成复现。

这个案例中,osChina的加密没有使用强混淆和复杂的反调试,非常适合用来建立这个标准流程的肌肉记忆。

3. 实战操作:一步步定位密码加密逻辑

现在,让我们打开osChina的登录页面(https://www.oschina.net/home/login),开始实战。

3.1 抓包与初步观察

首先,清空Network面板的记录,然后在登录框输入一个测试账号(可以随便编一个邮箱)和密码 123456 ,点击登录。在Network面板中,你会很快看到一个名为 login 的POST请求,状态码可能是200(成功)或其它(失败,这没关系,我们关注的是请求体)。

点击这个 login 请求,查看 Headers Payload 。在 Payload 里,我们能看到表单数据,其中 email 是你的账号,而 password 就是一长串加密后的字符串了,它看起来像 a3857b6c... 这样的十六进制格式。记下这个加密后的字符串,这是我们最终要验证的目标。

3.2 全局搜索与关键代码定位

接下来,进入 Sources 面板,按下 Ctrl+Shift+F 打开全局搜索框。我们应该搜索什么关键词呢?

  1. 搜索 password :这是一个最直接的尝试。因为参数名就是 password ,赋值语句很可能包含它。输入 password 进行搜索,结果可能会很多,因为 password 是一个常见变量名。我们需要在其中寻找那些看起来像是在进行加密或赋值的代码行。
  2. 搜索加密后的片段 :更精准的方法是,取加密后字符串的前6-8个字符(如 a3857b )进行搜索。如果加密逻辑是统一的,那么生成这串字符的代码里很可能就包含这个字面量或者相关的计算过程。在实际操作中,搜索 a3857b 可能直接定位到关键代码附近。

在osChina的案例中,通过搜索 password ,我们很快就能在某个JS文件(可能是一个名字包含 login common 的打包文件)中找到类似下面的代码片段:

var pwd = $('#password').val();
if (pwd) {
    pwd = hex_md5(pwd); // 或者可能是 CryptoJS.MD5(pwd).toString()
    // ... 可能还有其他处理
    $('input[name="password"]').val(pwd);
}

或者,更常见的是,密码在提交前,通过一个表单序列化或Ajax配置的函数被处理了。你可能会找到这样的代码:

data: {
    email: $('#email').val(),
    password: hex_md5($('#password').val())
}

看到 hex_md5 了吗?这就是关键线索!它强烈暗示密码使用了MD5算法进行哈希加密。MD5是一种不可逆的哈希函数,常用来做密码的摘要。但请注意,单纯的MD5并不安全,所以很多网站会进行“加盐”(salt)或多次哈希。我们需要确认这里是不是单纯的MD5。

3.3 深入调试与函数追踪

找到 hex_md5 这个函数名后,我们可以在该行代码左侧的行号上点击,设置一个断点。然后,再次点击登录按钮。代码执行会在断点处暂停。

此时,我们可以做几件事:

  • 查看变量值 :将鼠标悬停在 pwd $('#password').val() 上,可以看到此时明文密码 123456
  • 单步执行 :按 F10 (步过)执行 hex_md5(pwd) 这一行,然后再次查看 pwd 的值。你会发现它变成了一个32位的十六进制字符串,例如 e10adc3949ba59abbe56e057f20f883e
  • 核对 :立即去对比Network里捕获到的 password 参数值。如果一致,那么恭喜,加密逻辑就是一次简单的MD5!但很多时候,你会发现不一致。这说明在 hex_md5 之后,密码可能还经历了其他处理。

如果不一致,我们就需要继续追踪。看 hex_md5 执行后, pwd 这个变量又被用于什么计算?或者,在表单提交的 data 里, password 的值是否被另一个函数包裹了?比如 password: someFunction(hex_md5($('#password').val()))

通过 Call Stack 面板,我们可以查看 hex_md5 函数被谁调用,以及它自身的实现。点击 Call Stack 中的 hex_md5 ,可能会跳转到这个函数的定义处。这个函数可能是网站自己实现的,也可能是引用了像 CryptoJS 这样的第三方库。

在osChina的实际情况中,经过调试你可能会发现,最终的 password 值并不是直接的MD5结果,而是将这个MD5结果字符串进行了二次处理。例如,可能是将MD5字符串全部转换为大写,或者截取其中一部分,或者最经典的——将MD5结果再与某个固定字符串拼接后,再进行一次MD5(即MD5(MD5(pass)+salt))。这就需要我们仔细阅读 hex_md5 之后的代码逻辑。

实操心得 :在调试时,不要只看一处。有时加密逻辑分散在几个不同的函数或事件处理器里。关注数据流,从明文输入开始,一步一步跟到最终的网络请求参数生成。利用好 Watch 表达式,添加对关键变量(如加密后的密码值)的监视,可以更直观地看到它的变化过程。

4. 加密逻辑解析与代码提取

假设我们通过调试,最终确定了osChina的密码加密逻辑为: 对用户输入的明文密码进行一次标准的MD5哈希,并将结果转换为大写字母形式 。即 password = MD5(明文密码).toUpperCase()

4.1 理解MD5算法在其中的作用

MD5(Message-Digest Algorithm 5)是一种广泛使用的密码散列函数,可以产生一个128位(16字节)的哈希值,通常用32个十六进制数字表示。它的特点是:

  • 不可逆 :从哈希值无法反推出原始密码。
  • 雪崩效应 :原始密码哪怕只改动一位,产生的哈希值也会截然不同。
  • 固定长度 :无论输入多长,输出总是32位十六进制字符串。

在Web登录中,直接传输MD5值而非明文,可以避免密码在传输过程中被窃听(尽管MD5本身已被证明可碰撞,不再安全,但很多旧系统仍在使用)。 toUpperCase() 只是格式化,不影响哈希值本身,可能是为了后端校验时统一大小写。

4.2 提取并简化加密函数

现在,我们需要把浏览器中实现这个逻辑的JavaScript代码提取出来。在Sources面板,找到 hex_md5 函数的定义。它可能是一个长长的、经过压缩的、变量名被混淆的代码块。我们需要将它复制出来。

一个典型的、自包含的MD5 JavaScript实现可能长这样(这是经过格式化的常见版本):

function hex_md5(s) {
    function md5_RotateLeft(lValue, iShiftBits) {
        return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
    }
    function md5_AddUnsigned(lX, lY) {
        //... 加法运算实现
    }
    // ... 省略很多辅助函数和常量
    function md5_utf8_encode(string) {
        // ... 字符串转UTF-8编码
    }
    var x = [];
    var k, AA, BB, CC, DD, a, b, c, d;
    var S11=7, S12=12, S13=17, S14=22;
    // ... 核心计算循环
    // ... 最终将计算结果转换为十六进制字符串
    return temp.toLowerCase(); // 注意,这里可能是.toLowerCase(),但我们需要.toUpperCase()
}

我们的任务是将这个完整的 hex_md5 函数复制到一个新的JS文件中。然后,我们需要创建一个入口函数,来模拟网页中的调用过程。因为网页中可能直接调用 hex_md5(pwd) ,但在Node.js或 execjs 环境中,我们需要显式地导出这个函数。

创建一个名为 osc_password.js 的文件,内容如下:

// 粘贴完整的 hex_md5 函数实现到这里
function hex_md5(s) {
    // ... 上面那一大串MD5实现代码
    // 确保最终返回的是字符串,例如:
    // return temp.toLowerCase();
}

// 关键:根据我们调试的结果,修改或封装最终输出
function encryptPassword(password) {
    // 1. 进行MD5哈希
    var md5Hash = hex_md5(password);
    // 2. 转换为大写 (这是我们分析出的osChina逻辑)
    var finalPassword = md5Hash.toUpperCase();
    // 3. 返回最终结果
    return finalPassword;
}

// 为了能在Node.js或execjs中被调用,我们需要将函数暴露出去
// 方式一:适用于Node.js直接运行
// console.log(encryptPassword("123456"));
// 方式二:适用于execjs调用
if (typeof module !== 'undefined' && module.exports) {
    module.exports = encryptPassword;
}

重点 :你必须验证你粘贴的 hex_md5 函数在本地运行的结果。在Node.js环境中,用 node osc_password.js 测试,或者在该文件末尾加上 console.log(encryptPassword("123456")) ,看输出是否与浏览器捕获的 password 值完全一致。如果不一致,请返回浏览器调试阶段,确认是否遗漏了其他步骤(比如加盐、二次哈希等)。

5. 使用Python复现加密过程

本地JS验证通过后,我们就可以用Python来调用这个JS逻辑,实现自动化了。

5.1 使用execjs调用JS函数

首先安装必要的库:

pip install PyExecJS
# 或者
pip install execjs

我更喜欢使用 execjs ,因为它是一个更轻量级的封装。以下是Python代码示例:

import execjs

# 1. 读取我们刚才保存的JS文件
with open('osc_password.js', 'r', encoding='utf-8') as f:
    js_code = f.read()

# 2. 创建JS执行环境
ctx = execjs.compile(js_code)

# 3. 调用JS中暴露的函数
password = "123456"
encrypted_pwd = ctx.call("encryptPassword", password) # 调用我们封装的encryptPassword函数

print(f"明文密码: {password}")
print(f"加密后密码: {encrypted_pwd}")

# 4. 验证:可以与浏览器抓包得到的值进行比对
browser_captured_pwd = "E10ADC3949BA59ABBE56E057F20F883E" # 替换成你抓到的值
if encrypted_pwd == browser_captured_pwd:
    print("加密结果验证成功!")
else:
    print("加密结果验证失败!请检查JS逻辑。")

5.2 整合到登录请求中

加密函数复现成功后,将其整合到完整的登录请求中就非常简单了。我们使用 requests 库来模拟登录。

import requests
import execjs

class OSCLogin:
    def __init__(self):
        self.session = requests.Session()
        # 设置一些通用的请求头,模拟浏览器
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Referer': 'https://www.oschina.net/home/login',
            'Origin': 'https://www.oschina.net',
        })
        self.js_ctx = self._load_js_encryptor()

    def _load_js_encryptor(self):
        """加载JS加密函数"""
        with open('osc_password.js', 'r', encoding='utf-8') as f:
            js_code = f.read()
        return execjs.compile(js_code)

    def encrypt_password(self, plain_password):
        """加密密码"""
        return self.js_ctx.call("encryptPassword", plain_password)

    def login(self, email, password):
        """执行登录"""
        login_url = "https://www.oschina.net/action/user/hash_login?from="
        # 先获取必要的cookie或token(如果需要)
        # 有时需要先访问登录页,获取一个初始的cookie
        self.session.get("https://www.oschina.net/home/login")

        # 准备登录数据
        encrypted_pwd = self.encrypt_password(password)
        form_data = {
            'email': email,
            'pwd': encrypted_pwd, # 注意:这里参数名可能是'pwd'或'password',需根据实际抓包确认
            'verifyCode': '', # 如果有验证码,此处需要处理
            'save_login': '1',
        }
        # 根据实际抓包情况,可能还需要其他参数,如`from`, `token`等
        # 务必检查抓包到的Request Payload,确保所有参数名和值都一致

        print(f"提交的加密密码: {encrypted_pwd}")
        resp = self.session.post(login_url, data=form_data)

        # 检查登录结果
        print(f"状态码: {resp.status_code}")
        print(f"响应内容: {resp.text[:500]}") # 打印前500字符

        # 通常,登录成功会返回一个JSON,里面包含跳转信息或用户数据
        # 例如:if resp.json().get('code') == 1: print("登录成功")
        # 或者检查响应头中的Location或Set-Cookie
        if 'location' in resp.headers or 'Set-Cookie' in resp.headers:
            print("登录可能成功,请检查后续请求或用户状态。")
        else:
            print("登录可能失败。")

        return resp

if __name__ == '__main__':
    login_client = OSCLogin()
    # 替换为你的测试账号密码
    login_client.login('your_test_email@example.com', '123456')

6. 常见问题与排查技巧实录

在实际操作中,你几乎一定会遇到各种问题。下面是我总结的一些常见坑点和排查方法。

6.1 加密结果不一致的排查流程

这是最常见的问题。你的Python代码输出的加密字符串,和浏览器抓包得到的不一样。

  1. 第一步:隔离验证JS 。先不要管Python,直接在Node.js命令行里运行你的 osc_password.js ,用 console.log(encryptPassword("123456")) 输出结果。与浏览器抓包的值对比。

    • 如果Node.js结果就与浏览器不一致 :问题出在JS代码本身。说明你提取的 hex_md5 函数可能不是网页实际使用的那个版本,或者加密逻辑不止MD5这一步。 解决方案 :回到浏览器调试。在 hex_md5 函数被调用后,继续单步跟踪,看结果是否被其他函数处理了。或者,在 hex_md5 函数内部最后 return 的地方打上断点,看它返回的值是什么,是否已经是最终值。
    • 如果Node.js结果与浏览器一致 :问题出在Python调用环节。
  2. 第二步:检查Python调用细节

    • 编码问题 :确保Python传给JS的字符串编码正确。对于中文密码尤其要注意。可以尝试在JS函数入口处 console.log(typeof s, s) ,看看接收到的参数类型和值是否正确。
    • 函数名和调用方式 :确认 ctx.call 的第一个参数是你JS代码中暴露的函数名(例如 encryptPassword )。检查JS代码最后是否正确地导出了函数( module.exports = ... )。
    • execjs环境 :有时 execjs 默认的JavaScript引擎可能有问题。可以指定引擎: execjs.get('Node') 。确保系统Node.js可用。
  3. 第三步:核对完整加密流程 。密码加密可能涉及以下环节,请逐一确认:

    • 字符编码 :明文密码在哈希前,是否被转成了UTF-8或GBK?MD5通常对字节数组操作,字符串需要先编码。大多数JS MD5实现内部会做 UTF-8 编码,但有些特殊场景可能需要指定。
    • 加盐(Salt) :是不是 MD5(密码+固定盐值) 或者 MD5(固定盐值+密码) ?盐值可能是一个隐藏的输入框值,或者一个全局变量。在设置密码的JS附近搜索 salt key 等关键词。
    • 多次哈希 :是不是 MD5(MD5(密码)) MD5(MD5(密码)+salt) ?这需要跟踪MD5后的结果是否又被当作输入送入了另一个哈希函数。
    • 格式化 :除了 toUpperCase() ,是否还有 substr slice 截取?或者将十六进制转换成了Base64?

6.2 其他典型问题与解决思路

问题现象 可能原因 排查与解决思路
全局搜索搜不到 password 或加密串 1. 代码被严重混淆,变量名替换。
2. 加密逻辑在异步加载的JS中,未在初始搜索范围。
3. 加密是WebAssembly或其它非JS技术实现(此案例较简单,应不是)。
1. 尝试搜索更通用的关键词,如 md5 encrypt decode
2. 在登录动作触发后(XHR断点),再搜索。
3. 查看Network中登录请求的 Initiator (发起者),点击跳转到发起请求的JS代码行。
断点无法命中或一闪而过 1. 代码被压缩在一行,行号不准。
2. 有反调试机制(如 debugger 语句循环)。
3. 事件监听器不是通过 click 直接绑定。
1. 使用 {} 格式化代码(Sources面板左下角 {} 图标)。
2. 在反调试的 debugger 语句行右键选择“Never pause here”。
3. 在 Event Listener Breakpoints 中勾选 Mouse -> click 事件进行断点。
execjs UnicodeDecodeError 或语法错误 1. JS文件编码不是UTF-8。
2. JS代码中包含ES6+语法,而默认引擎不支持。
3. 提取的JS代码不完整,缺少依赖变量或函数。
1. 保存JS文件时选择UTF-8编码。
2. 使用Node.js环境(支持ES6)。 execjs 指定引擎为 Node
3. 回溯调用栈,将依赖的上级函数或变量一并复制出来。可以尝试将整个IIFE(立即执行函数表达式)包裹的代码块复制出来。
登录请求返回“验证码错误”或“参数缺失” 1. 登录需要验证码。
2. 提交的Form Data缺少隐藏字段(如 csrf_token , from )。
3. Cookie或Session状态不正确。
1. 首次访问登录页可能需获取一个包含token的Cookie或页面内的隐藏input值。
2. 仔细比对浏览器成功登录请求的 Payload ,一个参数都不能少。
3. 使用 requests.Session() 保持会话,先 get 登录页,再 post 登录接口。

6.3 我的实操心得与建议

  1. 从易到难 :osChina这类加密简单的站点是绝佳的“第一课”。不要轻视它,把这里的每个步骤(抓包、搜索、断点、提取、复现)都练到形成条件反射。
  2. 善用“搜索”和“调用栈” :90%的逆向问题可以通过关键词搜索找到入口。找到入口后,“调用栈”是你理清逻辑脉络的地图。
  3. 保持耐心,注重细节 :一个字符的大小写、一个加号顺序的差别,都可能导致加密失败。调试时,多用 console.log Watch 表达式输出中间变量的值,与你的预期进行比对。
  4. 备份与验证 :每当你认为找到关键函数时,就把它复制到一个干净的JS文件中,在Node环境里验证输入输出。确保这块“积木”是正确的,再去找下一块。
  5. 理解大于复制 :最终目标不是仅仅复制出一段能跑的代码,而是理解“他们为什么这么做”。是简单的哈希?还是加了盐?为什么要大写?这能帮你举一反三,应对更复杂的场景。

这个osChina密码加密的案例,虽然简单,但完整走通了这个流程,你就已经拿到了JS逆向世界的入场券。接下来,你可以用同样的方法论,去尝试分析其他站点的登录、数据接口参数,逐步增加难度。记住,逆向的本质是理解和模仿,工具和技巧只是辅助。当你能够独立分析并复现一个中等难度站点的加密时,你会发现很多看似复杂的技术,其核心思路都是相通的。

更多推荐