20. JS逆向

kurome / 2024-02-03 / 原文

一、什么是JS逆向

  JavaScript 是一种轻量级的编程语言。JavaScript 是可插入 HTML 页面的编程代码。JavaScript 插入 HTML 页面后,可由所有的现代浏览器执行。

  我们在使用爬虫获取网页数据时,可能会遇到通过 JavaScript 代码实现的反爬措施。JS 反爬技术的实现方式包括动态渲染、异步加载、验证码、IP限制等多种方式。这些技术可以有效地防止爬虫的抓取,保护网站的数据安全。

  此时,我们需要通过通过分析和破解 JavaScript 代码,获取反爬措施的规则和实现方式,从而绕过反爬措施,实现爬虫的抓取。JS 逆向需要具备一定的 JavaScript 编程能力和代码分析能力。

二、Node.js的安装与配置

  Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动的、非阻塞式 I/O 的模型,轻量又高效,它的底层是用 C/C++ 编写的。

2.1、Node.js的安装

  我们可以从官网上下载 Node.js,网址如下:https://nodejs.org/en/download/。它的安装步骤什么简单,一路下一步即可。

Nodejs的安装步骤1

Nodejs的安装步骤2

Nodejs的安装步骤3

2.2、Node.js的配置

  在执行例如 npm install webpack -g 等命令全局安装的时候,默认会将模块安装在 C:\Users\用户名\AppData\Roaming 路径下的 npmnpm_cache 中,不方便管理且占用 C 盘空间。因此,我们需要配置自定义的全局模块安装目录,在 Node.js 安装目录下新建两个文件夹 【node_global】 和 【node_cache】,然后在终端中执行如下两个命令:

npm config set prefix "D:\Nodejs\nodejs\node_global"
npm config set cache "D:\Nodejs\nodejs\node_cache"

其中,D:\Nodejs\nodejs 是 Node.js 的安装目录,node_globalnode_cache 是刚才创建的文件夹;

  执行成功之后,然后在环境变量中新建一个变量名为 【NODE_PATH】, 值为D:\Nodejs\nodejs\node_modules 的变量。【NODE_PATH】 变量就是 Node.js 中用来寻找模块所提供的路径注册环境变量。

NODE_PATH环境变量

  最后编辑环境变量 【Path】,将相应 【npm】 的路径改为:D:\Nodejs\nodejs\node_global

npm路径修改前

npm路径修改后

三、补充浏览器环境

  现在越来越多的 JavaScript 代码都加入了浏览器的特征,如果你用 Node.js 去运行扣下来的 JavaScript 源代码,可能会报错,也可能得到的结果与浏览器上的不一致,因此也就无法通过服务器的参数校验。因此,我们需要补充浏览器环境。

  我们可以通过 Node.js 的 jsdom 模块补充浏览器环境。在终端中通过 npm 安装 jsdom。

npm install -g jsdom
npm install -g node-gyp

  当我们安装 jsdom 出现如下问题时,可能是文件权限问题。

文件操作权限问题

  这是由于对文件夹操作的权限不够,右击 【Node.js 安装目录】 -> 【属性】-> 【安全】,点击 【编辑】,将所有权限都勾选即可。

修改文件权限

添加权限

  有时候由于网络问题,npm 默认使用的是官方源下载 jsdom 模块,但有时候官方源可能会有问题。这时,我们可以尝试切换到其他的镜像源,比如淘宝镜像。你可以使用以下命令进行切换:

npm config set registry https://registry.npmmirror.com

  切换完镜像源之后,我们还需要清除 npm 缓存。有时候 npm 缓存中的一些损坏文件可能会导致安装失败。

npm cache verify

  JS 代码如下:

const jsdom = require('jsdom');
const { JSDOM } = jsdom;

const resourceLoader = new jsdom.ResourceLoader({
    userAgent:"Mozilla/5.0 (Macintosh; Intel Mac 0S X 10_15_7) AppleWebKit/537.36 (KHTML,1ikeGecko) Chrome/96.0.4664.55 Safari/537.36"
});

// 创建一个模拟的浏览器窗口
const html = "<!DOCTYPE html><p>Hello world</p>";
const dom = new JSDOM(html,{
    url: "https://www.toutiao.com",
    referrer:"https://example.com/",
    contentType: "text/html",
    // resources: resourceLoader,
});

// window = {}
window = global;

const params = {
    location: {
        hash: "",
        host: "www.toutiao.com",
        hostname: "www.toutiao.com",
        href: "https://www.toutiao.com",
        origin: "https://www.toutiao.com",
        pathname: "/",
        port: "",
        protocol: "https:",
        search: "",
    },
    navigator: {
        appCodeName: "Mozilla",
        appName: "Netscape",
        appVersion: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/93.0.4577.82 Safari/537.36",
        cookieEnabled: true,
        deviceMemory: 8,
        doNotTrack: null,
        hardwareConcurrency: 4,
        language: "zh-CN",
        languages: ["zh-cN", "zh"],
        maxTouchPoints: 0,
        onLine: true,
        platform: "MacIntel",
        product: "Gecko",
        productSub: "20030107",
        userAgent: "Mozi1la/5.0 (Macintosh; Intel Mac 0S X 10_15_7) AppleWebKit/537.36 (KHTML,1ike Gecko) Chrome/93.0.4577.82 Safari/537.36",
        vendor: "Google Inc."
    }
}

Object.assign(global,params);

location = window.location

function func(arg){
    return arg + "\n" + location.href + "\n" + window.navigator.userAgent
}

  Python 代码如下:

import execjs

if __name__ == "__main__":
    with open("v1.js", mode="r", encoding="utf-8") as f:
        js_string = f.read()

    JS = execjs.compile(js_string, cwd=r"D:\Nodejs\nodejs\node_global\node_modules")
    sign = JS.call("func", "Sakura")
    print(sign)

四、Python执行JS代码

  在 Python 中,我们可以通过 pyexecjs 模块执行 JS 代码。但执行 JavaScript 的功能还需要依赖 JavaScript 的运行环境。这里,推荐使用 Node.js 环境。我们可以在终端中通过 pip 安装 pyexecjs 模块。

pip install pyexecjs

  Python 内容如下:

import execjs

print(execjs.get().name)

with open("v1.js", mode="r", encoding="utf-8") as f:
    js_string = f.read()

JS = execjs.compile(js_string)
sign= JS.call("func", "Sakura")
print(sign)

  JS 代码如下:

function func(arg){
    return arg;
}

var result = func("Sakura");
console.log(result);

如果配置完 Node.js 后, print(execjs.get().name) 输出的还是 JScript 不是 Node.js (V8) 的话,可能是环境还没切换好,我们可以重启以下项目。

五、用户密码登录

5.1、逻辑分析

  这里,我们登录基础教育教师培训网站, https://xuexi.chinabett.com,会发现它发送一个 Ajax 请求,携带请求参数。

Ajax请求

Ajax请求携带参数

  这里,我们可以发现,该网站对账号和密码进行了加密。我们需要跳转到源码,寻找它加密的函数。

跳转到加密逻辑源码

跳转后页面

  此时,我们可以向上寻找加密函数,并打上断点。

寻找加密函数

  重新执行,定位到 base64code() 加密函数。

跳转到加密函数

加密函数

5.2、代码实现

  我们在终端中通过 pip 安装相关的模块。

pip install requests
pip install pyexecjs
pip install bs4
pip install lxml
pip install ddddocr
pip install pillow==9.5.0
pip install numpy

  我们可以把 base64encode() 函数的内容 和密码再次加密的逻辑扣下来,保存到 v1.js 文件中。

function base64encode(str) {
    var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var base64DecodeChars = new Array(
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
    -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1);
    var out, i, len;
    var c1, c2, c3;
    len = str.length;
    i = 0;
    out = "";
    while (i < len) {
        c1 = str.charCodeAt(i++) & 0xff;
        if (i == len) {
            out += base64EncodeChars.charAt(c1 >> 2);
            out += base64EncodeChars.charAt((c1 & 0x3) << 4);
            out += "==";
            break;
        }
        c2 = str.charCodeAt(i++);
        if (i == len) {
            out += base64EncodeChars.charAt(c1 >> 2);
            out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
            out += base64EncodeChars.charAt((c2 & 0xF) << 2);
            out += "=";
            break;
        }
        c3 = str.charCodeAt(i++);
        out += base64EncodeChars.charAt(c1 >> 2);
        out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
        out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
        out += base64EncodeChars.charAt(c3 & 0x3F);
    }
    return out;
}

function s1() {
    var data = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    var r = Math.floor(Math.random() * 62);
    return data[r];
}

function encryptPwd(password){
    var newPwd = [];
    var pwdlength = password.length;
    for (i = 0; i < pwdlength; i++) {
        newPwd.push(password[i]);
        if (i < pwdlength - 1)
            newPwd.push(s1());

    }
    password = newPwd.join('');

    return password
}
import requests
import execjs
import ddddocr

from bs4 import BeautifulSoup
from urllib.parse import urljoin

if __name__ == "__main__":
    # 1,访问首页,获取Cookie
    url = "https://xuexi.chinabett.com"
    cookie_dict = {}
    response = requests.get(url)
    cookie_dict.update(response.cookies.get_dict())

    # 2.获取验证码
    soup = BeautifulSoup(response.text, features="html.parser")
    image_tag = soup.find(name="img", attrs={"id": "imgVerifity"})
    code_src = image_tag.attrs["src"]

    # 3.识别验证码
    response = requests.get(url=urljoin(url, code_src), cookies=cookie_dict)
    cookie_dict.update(response.cookies.get_dict())

    ocr = ddddocr.DdddOcr()
    code = ocr.classification(response.content)

    # 4.JS逆向加密逻辑
    with open("v1.js", mode="r", encoding="utf-8") as f:
        js_string = f.read()

    JS = execjs.compile(js_string)

    # 5.用户名和密码进行加密
    username = JS.call("base64encode", "sakura")
    password = JS.call("base64encode", "sakura")
    password = JS.call("encryptPwd", password)

    # 6.登录
    response = requests.post(
        url="https://xuexi.chinabett.com/Login/Entry",
        data={
            "userAccount": username,
            "password": password,
            "returnUrl": "/PersonalCenter",
            "proVing": code
        },
        cookies=cookie_dict
    )

    print(response.text)