案例网址: https://www.jd.com/
逆向对象:
2025-08-08T13:36:36.png

对于js逆向,确定目标后就是抓包尝试找出目标api,然后查看参数来进一步确定逆向目标。之后有多种方法来定位目标参数,基本的比如追踪堆栈,全局搜索,高级一点的比如hook注入。只有清楚地定位到目标参数之后才可以进一步找到目标参数的生成算法。之后再一系列断点调试确定目标参数的具体生成的函数来截取所需要的源码进入编辑器进行补环境,从而模拟输出结果。

抓包找出api
2025-08-08T13:45:36.png
发现输出base64编码的滑块图片接口,至于参数先不管继续查找。
2025-08-08T13:47:55.png
这个接口发现结果输出了validate值,这个值就是过码之后才给的,所以这个接口用来验证提交的位置参数密文是否通过机器校验。
之后再来看看具体要提交的参数,先看看图片接口:
2025-08-08T13:50:38.png

有几个参数,来回刷新观察,发现基本都是定值,除了回调不是(这个不需要管,从字面意思可以看出是随机数)。那么该怎么获取这些定值参数呢?因为有些时候虽然是定值,但是有一定时效性,就比如这里的j参数就有时效性,过一段时间就失效了。所以需要查找有没有相关的api,然而发现并没有。那么这里有两种方法,第一种就是继续逆向,第二种就是自动化获取。综合考虑还是自动化更方便且有效。主要是这个参数能用很久,写个初始化脚本,用之前初始化好了。
自动化选用playwright

import re
import json
import time
from playwright.sync_api import Playwright, sync_playwright, expect

config = {
    'jd_j': ''
}


def on_response(response):
    # print(response.url)
    if 'g.html' in response.url:
        j = re.search('&j=(.*?)&', response.url)
        if j:
            j = j.group(1)
            config['jd_j'] = j


def run(playwright: Playwright) -> None:
    # ---------------------京东
    browser = playwright.chromium.launch(headless=True)
    context = browser.new_context()
    page = context.new_page()

    def jd():
        page.goto("https://passport.jd.com/new/login.aspx?ReturnUrl=https%3A%2F%2Fre.jd.com%2Fsearch%3Fkeyword%3D%25e6%258b%25bc%25e5%25a4%259a%26ad_od%3D3%26traffic_source%3D1004%26re_dcp%3D202m0QjIIg%3D%3D%26bd_vid%3DnHf3njTvnjcYrHRdnWn3P10vPW-xnWcdg17xnH0sg1wxnH0dPj6kPWmkPHR1%26cu%3Dtrue%26utm_source%3Dbaidu-search%26utm_medium%3Dcpc%26utm_campaign%3Dt_262767352_baidusearch%26utm_term%3D925478929660_0_747a08c25521423d8a26f3d3273dd2ba")
        page.get_by_text("短信登录").click()
        page.get_by_placeholder("请输入手机号").click()
        page.locator("#mobile-number").fill("填你的手机号")
        page.on('response', on_response)
        page.get_by_role("button", name="获取验证码").click()
        time.sleep(5)
        page.close()

    jd()
    # ---------------------百度

    context.close()
    browser.close()


with sync_playwright() as playwright:
    run(playwright)

with open('config.json', 'w', encoding='utf-8') as f:
    json.dump(config, f, ensure_ascii=False, indent=4)

print(config)

再看返回validate的接口需要的参数:
2025-08-08T14:01:37.png
发现很长一段的d参数,初步猜测这个参数与滑动坐标有关。其它的参数除了c都一样。c参数刷新多次发现不一样,但是从其它的api中是能获取的:
2025-08-08T14:03:57.png
这个challenge就是c参数。而这个接口的参数都是分析过的,直接访问就行了。
解决了c参数,接下来就需要逆向d参数。

定位d参数
这里直接查看发起程序就行:
2025-08-08T14:06:33.png
点击第一个文件跳转到相应的js代码,审计代码,发现整个代码的框架是类+方法。这个对象是JDJRValidate,里面一堆方法。由于参数名是d所以局部搜索是不行的,那么直接往上找找看呗。代码没有混淆,变量名都很直接,很容易找到参数生成的位置:
2025-08-08T14:11:17.png

逆向分析d参数

在d下面打上断点调试并在控制台输出b:
2025-08-08T14:12:54.png
b是一个数组,里面的元素就是一个个点,也就是滑动过程中给的一个个点,这些点都有三个值,很明显是xy坐标和时间戳。
2025-08-08T14:15:18.png
跳转到这个方法内部。
2025-08-08T14:15:53.png
发现是这个,看着很像某种加密,再打上断点调试:
2025-08-08T14:16:41.png
发现几个方法,继续跳转断点,会发现就这几个方法实现了d参数:

var JDJRValidate = {
    getCoordinate: function(a) {
        var b = this;
        var c = new Array();
        for (var d = 0x0; d < a['length']; d++) {
            if (d == 0x0) {
                c['push'](b['pretreatment'](a[d][0x0] < 0x3ffff ? a[d][0x0] : 0x3ffff, 0x3, !![]));
                c['push'](b['pretreatment'](a[d][0x1] < 0xffffff ? a[d][0x1] : 0xffffff, 0x4, !![]));
                c['push'](b['pretreatment'](a[d][0x2] < 0x3ffffffffff ? a[d][0x2] : 0x3ffffffffff, 0x7, !![]));
            } else {
                var e = a[d][0x0] - a[d - 0x1][0x0];
                var f = a[d][0x1] - a[d - 0x1][0x1];
                var g = a[d][0x2] - a[d - 0x1][0x2];
                c['push'](b['pretreatment'](e < 0xfff ? e : 0xfff, 0x2, ![]));
                c['push'](b['pretreatment'](f < 0xfff ? f : 0xfff, 0x2, ![]));
                c['push'](b['pretreatment'](g < 0xffffff ? g : 0xffffff, 0x4, !![]));
            }
        }
        return c['join']('');
    },
    pretreatment: function(a, b, c) {
        var d = this;
        var e = d['string10to64'](Math['abs'](a));
        var f = '';
        if (!c) {
            f += a > 0x0 ? '1' : '0';
        }
        f += d['prefixInteger'](e, b);
        return f;
    },
    string10to64: function(a) {
        var b = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'['split']('')
            , c = b['length']
            , d = +a
            , e = [];
        do {
            mod = d % c;
            d = (d - mod) / c;
            e['unshift'](b[mod]);
        } while (d);
        return e['join']('');
    },
    prefixInteger: function(a, b) {
        return (Array(b)['join'](0x0) + a)['slice'](-b);
    }
};

放入编辑器中运行会发现生成的字段符合浏览器给的。那么说明逆向的没问题。接下来就是传参的问题。

滑块轨迹模拟
模拟滑块轨迹是一个复杂的问题,主要是京东这个要传入整个轨迹参数,不像某些网站只是传入距离就行了,传入整个轨迹参数就说明服务器要检验这些点是不是程序模拟的。正常人滑动的时候不是稳定的直线,不是匀速。所以模拟的时候就需要制造一些偶然性,让程序看起来更像真人。这里是纯逆向,所以可以搜搜相关的纯协议滑块轨迹算法。

import random
import matplotlib.pyplot as plt
import numpy as np


class GTrace(object):
    def __init__(self):
        self.__pos_x = []
        self.__pos_y = []
        self.__pos_z = []

    def __set_pt_time(self):
        """
        设置各节点的时间
        分析不同时间间隔中X坐标数量的占比
        统计结果: 1. 80%~90%的X坐标在15~20毫秒之间
                2. 10%~15%在20~200及以上,其中 [-a, 0, x, ...] 这里x只有一个,取值在110~200之间
                    坐标集最后3~5个坐标取值再50~400之间,最后一个坐标数值最大

        滑动总时间的取值规则: 图片宽度260,去掉滑块的宽度剩下200;
                        如果距离小于100,则耗时1300~1900之间
                        如果距离大于100,则耗时1700~2100之间
        """
        __end_pt_time = []
        __move_pt_time = []
        self.__pos_z = []

        total_move_time = self.__need_time * random.uniform(0.8, 0.9)
        start_point_time = random.uniform(110, 200)
        __start_pt_time = [0, 0, int(start_point_time)]

        sum_move_time = 0

        _tmp_total_move_time = total_move_time
        while True:
            delta_time = random.uniform(15, 20)
            if _tmp_total_move_time < delta_time:
                break

            sum_move_time += delta_time
            _tmp_total_move_time -= delta_time
            __move_pt_time.append(int(start_point_time+sum_move_time))

        last_pt_time = __move_pt_time[-1]
        __move_pt_time.append(last_pt_time+_tmp_total_move_time)

        sum_end_time = start_point_time + total_move_time
        other_point_time = self.__need_time - sum_end_time
        end_first_ptime = other_point_time / 2

        while True:
            delta_time = random.uniform(110, 200)
            if end_first_ptime - delta_time <= 0:
                break

            end_first_ptime -= delta_time
            sum_end_time += delta_time
            __end_pt_time.append(int(sum_end_time))

        __end_pt_time.append(int(sum_end_time + (other_point_time/2 + end_first_ptime)))
        self.__pos_z.extend(__start_pt_time)
        self.__pos_z.extend(__move_pt_time)
        self.__pos_z.extend(__end_pt_time)

    def __set_distance(self, _dist):
        """
        设置要生成的轨迹长度
        """
        self.__distance = _dist

        if _dist < 100:
            self.__need_time = int(random.uniform(500, 1500))
        else:
            self.__need_time = int(random.uniform(1000, 2000))

    def __get_pos_z(self):
        return self.__pos_z

    def __get_pos_y(self):
        _pos_y = [random.uniform(-40, -18), 0]
        point_count = len(self.__pos_z)
        x = np.linspace(-10, 15, point_count - len(_pos_y))
        arct_y = np.arctan(x)

        for _, val in enumerate(arct_y):
            _pos_y.append(val)

        return _pos_y

    def __get_pos_x(self, _distance):
        """
        绘制标准的数学函数图像: 以 tanh 开始 以 arctan 结尾
        根据此模型用等比时间差生成X坐标
        """
        # first_val = random.uniform(-40, -18)
        # _distance += first_val
        _pos_x = [random.uniform(-40, -18), 0]
        self.__set_distance(_distance)
        self.__set_pt_time()

        point_count = len(self.__pos_z)
        x = np.linspace(-1, 19, point_count-len(_pos_x))
        ss = np.arctan(x)
        th = np.tanh(x)

        for idx in range(0, len(th)):
            if th[idx] < ss[idx]:
                th[idx] = ss[idx]

        th += 1
        th *= (_distance / 2.5)

        i = 0
        start_idx = int(point_count/10)
        end_idx = int(point_count/50)
        delta_pt = abs(np.random.normal(scale=1.1, size=point_count-start_idx-end_idx))
        for idx in range(start_idx, point_count):
            if idx*1.3 > len(delta_pt):
                break

            th[idx] += delta_pt[i]
            i+=1

        _pos_x.extend(th)
        return _pos_x[-1], _pos_x

    def get_mouse_pos_path(self, distance):
        """
        获取滑动滑块鼠标的滑动轨迹坐标集合
        """
        result = []
        _distance, x = self.__get_pos_x(distance)
        y = self.__get_pos_y()
        z = self.__get_pos_z()
        for idx in range(len(x)):
            result.append([int(x[idx]), int(y[idx]), int(z[idx])])
        # plt.plot(z,x)
        # plt.show()
        return int(_distance), result

传入距离就行了。

识别图片,计算距离
如何准确识别滑块缺口和目标位置呢?有很多种方法,经典的就是机器学习,github有很多,然后就是打码平台,需要付费。所以还是使用第一种,有个开源的很好用的库————ddddocr
使用ddddocr可以准确识别出终点坐标。

import ddddocr

ocr = ddddocr.Ddddocr()
result = ocr.slide_match(slide_img, bg_img, simple_target=True)

这样返回的就是终点坐标。x就是距离。

模拟生成d参数
比如:
bg
2025-08-08T14:30:43.png

patch
2025-08-08T14:31:02.png

通过ddddocr识别出坐标:
2025-08-08T14:38:54.png

所以x为173,y为3

传入173,生成轨迹参数:
2025-08-08T14:41:24.png

传入js代码,生成d参数:
这里x坐标要加916,y坐标要加135(具体怎么加调试的时候看b的第0个元素就行,基本上不怎么变,稍微变点也没事),时间戳要改成现在的时间再加上模拟的再传入。
2025-08-08T14:47:01.png

完整代码:

from Ta.slider_TA import GTrace
import ddddocr
import execjs
import time

ocr = ddddocr.DdddOcr()
with open('./image/bg.png', 'rb') as f:
    bg = f.read()
with open('./image/patch.png', 'rb') as f:
    patch = f.read()
result = ocr.slide_match(bg, patch, simple_target=True)
print(result)
gtrace = GTrace()
p = GTrace.get_mouse_pos_path(gtrace, result['target'][0])[1]
# print(p)
y = result['target'][1]
for i in range(len(p)):
    if i == 0:
        continue
    elif i == 1:
        p[i][0] = '916'
        p[i][1] = '135'
        p[i][2] = int(time.time() * 1000)
    else:
        p[i][0] += 916
        p[i][1] += 135 + y
        p[i][2] += int(time.time() * 1000)
p.pop(0)
with open('js/jd.js', 'r', encoding='utf-8') as f:
    js_code = f.read()
ctx = execjs.compile(js_code)
result = ctx.eval(f"JDJRValidate.getCoordinate({p})")
print(result)

之后再访问即可获得validate,只不过终归是模拟,所以有成功率,成功率不高,但是可以用。