【猿人学】jsvmp洞察先机逆向
errol发表于2025-07-05 | 分类为 编程 | 标签为js逆向猿人学

这是猿人学里的一道js逆向练习题目,但本文的目的并不在于解题,而只是逆向其请求数据所必要的加密参数v。

https://match.yuanrenxue.cn/match/18data?page=2&t=1751697204&v=H%2BSPxe96K6TZLKNGgVw1SqAKEyoxPqR3%2B4ycCiHexKUFhX7jCHbicP2jNR13e1Wl

image

图1 加密参数v

正常情况下,该接口会返回解题需要的数据

1、定位

首先通过断点定位到请求发送的位置,同时得知v参数是在重写过的XMLHttpRequest.prototype.open()方法里生成的参数,该方法极其复杂,且经过混淆,难以阅读。

image

图2 调用open()

image

图3 open()方法体

基本的流程为:调用open()生成加密参数v -> 拼接到url -> 请求服务器。

2、下载源码

将加密相关的代码保存至本地,以供后续使用代码生成加密参数。

虽然只显示为一行,单其实代码还挺多的。

image

图4 源码1

image

图5 源码2

3、检测环境

一般情况下,js逆向都需要进行该步骤,尤其是本文这种"黑箱环境",里面很可能就存在环境检测机制,比方说如果缺少某些参数时,终止运行代码。

只不过之前涉及到的js逆向,代码较为简单,也没有经过混淆,容易看出所依赖的参数,window、document、location等,可以直接补全。

本次的检测方法是通过Proxy来实现的,用于为指定对象创建代理后,劫持其属性的读取操作。

使用的方式是将该检测方法注入到目标网页中,在实际的环境中运行一遍,并查看输出,以便确认该代码所依赖的环境数据。

目标对象一般是document、window这种浏览器的顶级对象,比方说本案例是window。

image

图6 注入源码位置

因此,可以在断点处,注入检测环境代码,可以使用chrome的代码片段功能保存,并运行该片段,再往下执行。

function createProxy(obj, objName) {
    const fields = ["window"];
    return new Proxy(obj,{
        get(target, prop, receiver) {
            // 捕获属性读取
            console.log(`[TRACE] Accessing ${objName}.${String(prop)}`);
            // 可选:打印调用堆栈
            let value;
            try {
                if (fields.includes(prop)) {
                    value = target[prop];
                }
                else {
                    value = Reflect.get(target, prop, receiver);
                }
            }
            catch(e) {
                console.log(e);
                value = target[prop];
            }

            // 如果是函数,再代理函数调用
            if (typeof value === 'function') {
                return new Proxy(value,{
                    apply(fnTarget, thisArg, args) {
                        console.log(`[TRACE] Calling ${objName}.${String(prop)} with arguments: `, args);
                        // console.trace(); // 打印调用堆栈
                        // return Reflect.apply(fnTarget, thisArg, args);
                        const r = Reflect.apply(fnTarget, thisArg, args);
                        if (args.length && typeof args[0] == 'string' && args[0].startsWith('2|')) {
                            console.log(r);
                            debugger;
                        }
                        return r;
                    }
                });
            }

            return value;
        },

        set(target, prop, value, receiver) {
            // 捕获属性写入
            console.log(`[TRACE] Setting ${objName}.${String(prop)} = `, value);
            // console.trace();
            if (prop === '_$_') {
                value = createProxy(value, prop);
            }
            return Reflect.set(target, prop, value, receiver);
        }
    });
}

// 创建代理对象
___ = createProxy(window, 'window');

image

图7 浏览器环境

如图7所示,代码里确实用到了较多的属性(截图里只显示了部分)。

4、补环境

这种情况下,手动补环境着实比较麻烦,因此可借用一些现成的辅助工具来帮助完成,如jsdom。

它可以很好地模拟浏览器环境。

main.js

const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const fetch = require('node-fetch');

// doc文档随便输入
let { window } = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
function createProxy(obj, objName) {
    // ...
}

window = createProxy(window, 'window');
global = window;
require("./1");

将本地环境调试到与浏览器环境相同的输出,尽可能保证代码在同样的环境下运行。

image

图8 本地环境

5、执行代码

不出意外的话,其实到这里就这样执行主业务代码了。

跟浏览器环境一样,使用XMLHttpRequest发送请求,可以把代码直接拷贝下来。

但毕竟只是模拟浏览器环境,因此需要做一些修改。

image

图9 发送xhr请求代码

function getdata(page){
    page = (page || 1);
    var xml = new XMLHttpRequest();
    xml.onreadystatechange = function cback(){
        if (xml.readyState == 4){
            try {
                var data = JSON.parse(xml.response || xml.responseText) // xml.responseText 用于兼容 IE9
                if (data['state'] == 'success'){
                    data = data.data;
                    let html = '';
                    $.each(data, function(index, val) {
                        html += '<td>'+ val.value + '</td>'
                    });
                    $('.number').text('').append(html);
                }else{
                    if(window.page == 5){
                        alert('第五页不在前端展示,请使用协议处理并模拟参数');
                    }
                    else{
                        alert('因未知原因,数据拉取失败。可能是触发了风控系统');
                        alert('生而为虫,我很抱歉');
                    }

                    $('.page-message').eq(0).addClass('active');
                    $('.page-message').removeClass('active');
                }
            }
            catch (e) {
                alert('因未知原因,数据拉取失败。可能是触发了风控系统');
                alert('生而为虫,我很抱歉');
                $('.page-message').eq(0).addClass('active');
                $('.page-message').removeClass('active');
            }

        }
    };
    xml.open('GET', 'https://match.yuanrenxue.cn/match/18data?page=' + page, true);
    xml.send();
}
// page=1时,不会生成加密参数,故设置为2
window.page = 2;
getdata(window.page);

果不其然,不出意外的话,意外就来了。。

image

图10 无法正常发送xhr请求

与浏览器环境对比发现,劫持属性输出的内容有些不一样。

image

图11 正常发送xhr请求所生成的参数

看起来,像是本地环境缺少了一些参数,导致代码没能正常执行。

很明显,"2|678m79,677m79,677d79,677u79,772u20","2"指的是当前页码,而"|"是分隔符,至于后面的"678m79,677m79,677d79,677u79,772u20",经调试分析后猜测,应该是一些点击分页时,鼠标移动的轨迹参数。

比如:678、677、772这样的数据,是点击第二页时,x轴的轨迹参数;而79、20是y轴的轨迹参数。

image

图12 网页交互

最终得出了一个简易版生成模拟数据的算法。

// 模拟鼠标点位数据
// 以第三页的坐标为基准,保持y轴不变,x轴前后加减(页码 * 50)
function generatePoint(paganation) {
    let x = 721 + (paganation - 3) * 50;
    return `${x}m128,${x}m128,${x}m127,${x}d127,${x}u127`;
}

createProxy()也要同步修改:使用劫持钩子设置轨迹参数。

function createProxy(obj, objName) {
    return new Proxy(obj, {
        get(target, prop, receiver) {
            // 捕获属性读取
            console.log(`[TRACE] Accessing ${objName}.${String(prop)}`);

            const value = Reflect.get(target, prop, receiver);
            // 如果是函数,再代理函数调用
            const type = typeof value;
            if (type === 'function') {
                return new Proxy(value, {
                    apply(fnTarget, thisArg, args) {
                        console.log(`[TRACE] Calling ${objName}.${String(prop)} with arguments: `, args);
                        // 若prop为encodeURIComponent,则保存参数
                        if (prop === 'encodeURIComponent') {
                            saving = true;
                            if (/\d+\|/.test(args.toString())) {
                                args[0] = args[0] + generatePoint(+args[0].replace('|', ''));
                            }
                        }
                        const r = Reflect.apply(fnTarget, thisArg, args);
                        return r;
                    }
                });
            }
            return value;
        },
        set(target, prop, value, receiver) {
            // 捕获属性写入
            let type = typeof value;
            if (type === 'string') {
                console.log(`[TRACE] Setting ${objName}.${String(prop)} = `, value.slice(0, 200));
            }
            else {
                console.log(`[TRACE] Setting ${objName}.${String(prop)} = `, value);
            }
            return Reflect.set(target, prop, value, receiver);
        }
    });
}

更新后,再次运行。

image

图13 再次发送失败

但发现还是无法正常发送请求,猜测是设置轨迹参数的方式不合法,导致了内部代码执行某些逻辑的时候,出现了错误。

不过,也并不是没有收获,而且是个重量级的东西,就在图13中,已经用红框标注了...

事实上,它正是加密参数v。

所以,虽然没能正常发送xhr请求,但其实本文的目的已经达到了。

此时只需将代码稍作修改,也能达到正常发送请求的效果。

调整createProxy()方法代码如下:

function createProxy(obj, objName) {
    return new Proxy(obj, {
        get(target, prop, receiver) {
            // ....
            if (type === 'function') {
                return new Proxy(value, {
                    apply(fnTarget, thisArg, args) {
                        console.log(`[TRACE] Calling ${objName}.${String(prop)} with arguments: `, args);
                        let saving;
                        // 若prop为encodeURIComponent,则保存参数
                        if (prop === 'encodeURIComponent') {
                            saving = true;
                            if (/\d+\|/.test(args.toString())) {
                                args[0] = args[0] + generatePoint(+args[0].replace('|', ''));
                            }
                        }
                        const r = Reflect.apply(fnTarget, thisArg, args);
                        if (saving) {
                            if (!receiver.output) {
                                receiver.output = [];
                            }
                            receiver.output.push(args);
                        }
                        return r;
                    }
                });
            }
            return value;
        },
        set(target, prop, value, receiver) {
            // ...
        }
    });
}

同步调整getdata()方法:

即将xhr.open()当做是获取加密数据的方法,生成链接后,再使用其他xhr库请求,axios、node-fetch等。

function getdata(page){
    page = page || 2;
    const xhr = new window.XMLHttpRequest();
    const href = `https://match.yuanrenxue.cn/match/18`;
    const _url = `${href}data?page=${page}`;
    xhr.open('GET', _url, true);
    console.log(window.output);
    const [a, b, c, d] = window.output;
     // 获取秒数
    const t = parseInt(a[0].slice(0, a[0].length / 2), 16);
    const url = `${_url}&t=${t}&v=${d[0]}`;
    console.log(url);
}
getdata(2);

再次执行代码:

[TRACE] Accessing window.output
[TRACE] Accessing window.output
[TRACE] Accessing window.output
[
  [ '687cb384687cb384' ],
  [ '687cb384687cb384' ],
  [ '2|671m128,671m128,671m127,671d127,671u127' ],
  [
    'sT68wsFnWIFX7L7jEbaUzcI1WpgGQPQ72dRZgoyXBTXw6dtiX+WJeydXeNqXuYRn'
  ]
]
[TRACE] Accessing window.output
https://match.yuanrenxue.cn/match/18data?page=2&t=1753002884&v=sT68wsFnWIFX7L7jEbaUzcI1WpgGQPQ72dRZgoyXBTXw6dtiX+WJeydXeNqXuYRn

格式跟浏览器环境一模一样,应该没啥问题。

但如果使用该url请求服务器的话,会失败,这是由于网站做了限制,需要登录后才可以操作,不过,这已经不是本文的讨论范围了。

本文完。

返回