某x滑块补环境分析

文章正文
发布时间:2025-11-17 07:29

本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。

研究对象

image.png (167.65 KB, 下载次数: 6)

下载附件

2025-8-9 18:16 上传

目标

补环境代码与实现

提交参数验证成功

抓包分析 获取验证码接口

通过刷新验证码结果可以发现带有 cap_union_prehandle 的接口返回值含有验证码相关参数:带缺口的背景图、滑块等参数。

由于图片大小问题,仅展示部分参数图片。

image-1.png (115.47 KB, 下载次数: 6)

下载附件

1

2025-8-9 18:14 上传

返回值分析:

sess : 该参数不做处理,用于提交时携带

data :

comm_captcha_cfg:

pow_cfg  : md5 和 prefix 参数会用于后续计算,相当重要!!!

tdc_path : js 文件,每次验证码请求时都会刷新,其中有两个重要参数在提交接口用到,也是本次补环境的目标,后续会说明。

dyn_show_info:

bg_elem_cfg : 这里背景图的相关参数,关注 img_url 即可。

sprite_url  : 滑块图片,但其中还含有拖动条等,需要裁剪出滑块图片。

验证码提交接口

手动提交时,会请求一个验证接口,参数如下:

image-2.png (222.47 KB, 下载次数: 7)

下载附件

2

2025-8-9 18:14 上传

请求参数分析:

collect      : tdc.js 文件产生,也是本次的目标。包含轨迹和环境的校验。

tlg          : collect 的长度

eks          : 也是通过 tdc.js 文件产生。当collect参数能正常产出,该值也能正常产出。

sess         : 获取验证码接口返回的值

ans          : 滑块的缺口值

pow_answer   : md5 和 prefix计算所产生的值,后续说明。

pow_calc_time: md5 和 prefix计算完成时所消耗的时间。

补环境

这里仅给出部分补环境代码与思路,按照思路来是没有问题的。

堆栈分析

通过验证请求接口分析,查看请求堆栈:

image-3.png (78.48 KB, 下载次数: 5)

下载附件

3

2025-8-9 18:14 上传


发现有关键字样:e.verify,这里是请求组装完成的地方。点击进入,并添加断点。

image-4.png (58.84 KB, 下载次数: 4)

下载附件

4

2025-8-9 18:14 上传


重新发起请求,进入断点,发现c参数已经生成好了。

image-5.png (111.9 KB, 下载次数: 6)

下载附件

5

2025-8-9 18:14 上传


往上分析,发现c参数在这里生成,同时可以发现collect参数通过o.getTdcData生成

image-6.png (75.59 KB, 下载次数: 6)

下载附件

6

2025-8-9 18:14 上传


进入o.getTdcData方法,可以发现最终的返回值是由window.TDC.getData(!0)产生,添加断点,并重新请求。

image-7.png (41.03 KB, 下载次数: 6)

下载附件

7

2025-8-9 18:14 上传


进入断点,鼠标放在window.TDC.getData(!0)上面,可以发现,该方法的实现在tdc.js文件中。

image-8.png (50.11 KB, 下载次数: 4)

下载附件

8

2025-8-9 18:14 上传


进入该js文件,会发现是jsvmp。至于什么是jsvmp,问问度娘吧~

开始之前

在开始之前,可能会有问题:补环境,该怎么补?该补哪些属性?这个问题的答案跟着本文走一遍,相信你应该会有答案。

首先需要一个代理器(Proxy),这是ChatGPT5对于Proxy的解释:

Proxy 是 JavaScript 中一个强大的内建对象,它用于创建一个 代理对象,通过它可以拦截并定义基本操作(如属性读取、赋值、函数调用等)的自定义行为。

简单的理解就是:通过Proxy可以知道对象调用什么了方法、属性等信息。

下面是本次目标需要的代理器实现:

const printLog = true; // 控制是否输出 function log() {     if (printLog) {         console.log(...arguments);     } } function watch(obj, name) {     return new Proxy(obj, {         get: function (target, property, receiver) {             try {                 if (typeof target[property] === "function") {                     log(`监控对象 get => ${name} ,读取属性:`, property + `,值为:` + 'function' + `,类型为:` + (typeof target[property]));                 } else {                     log(`对象 => ${name} ,读取属性:`, property + `,值为:` + target[property] + `,类型为:` + (typeof target[property]));                 }             } catch (e) { }             return Reflect.get(...arguments);         },         set: function (target, property, newValue, receiver) {             try {                 log(`监控对象 set => ${name} ,设置属性:`, property + `,值为:` + newValue + `,类型为:` + (typeof newValue));             } catch (e) { }             return Reflect.set(target, property, newValue, receiver);         },         // 监控 `in` 操作符         has: function (target, prop) {             log(`监控对象 has => ${name} , 属性 => ${prop} , 是否存在 => ${prop in target}`);             return Reflect.has(target, prop);;  // 如果属性存在,返回 true,否则返回 false         },         getPrototypeOf: function (target) {             log(`监控对象 prototype => ${name}`, `Object.getPrototypeOf(${name})`);             return Reflect.getPrototypeOf(target);         },         ownKeys: function (target) {             log("监控对象 ownKeys =>", name);             return Reflect.ownKeys(target);         },         getOwnPropertyDescriptor: function (target, prop) {             log("监控对象 getOwnPropertyDescriptor =>", name, "属性值:", prop, `Object.getOwnPropertyDescriptor(${name},"${prop}")`);             return Reflect.getOwnPropertyDescriptor(target, prop);         },     }); }

有了上述代码,来实现一个简单的补环境。

document = {     cookie: "", }; screen = {     height: 100, }; // Proxy代理 document = watch(document, "document"); screen = watch(screen, "screen"); document.cookie; screen.width;

在终端运行命令

node test.js > test.log

在test.log文件中查看日志,会发现document.cookie;和 screen.width;操作都被记录下来了。

image-9.png (39.07 KB, 下载次数: 6)

下载附件

9

2025-8-9 18:14 上传

正式开始

将所有对象开始监听,下列是主要代码:

window = globalThis; document = {}; location = {}; navigator = {}; window = watch(window, "window"); location = watch(location, "location"); document = watch(document, "document"); navigator = watch(navigator, "navigator"); require("./tdc.js"); function getCollect(){     return decodeURIComponent(window.TDC.getData(!0)); } const collect = getCollect(); log("输出结果",collect); log("输出长度",collect.length);

运行代码,会发现日志输出中有些是undefined,这个时候就需要注意了,与浏览器进行对比,浏览器有才补,没有不补。

image-10.png (109.17 KB, 下载次数: 6)

下载附件

10

2025-8-9 18:14 上传


image-11.png (69.6 KB, 下载次数: 4)

下载附件

11

2025-8-9 18:14 上传


这里添上screen,但同时不要忘记使用Proxy,只要是对象就使用,防止错过某些重要的值!

window.screen = {}; window.screen = watch(window.screen,"window.screen");

接着运行代码,查看日志,随着环境补的越来越多,就需要仔细分析日志。

image-12.png (166.74 KB, 下载次数: 6)

下载附件

12

2025-8-9 18:14 上传


还有一个小问题,会发现有需要地方调用了toString。

image-13.png (153 KB, 下载次数: 7)

下载附件

13

2025-8-9 18:14 上传


这里需要拿本地运行的效果与浏览器控制台运行的效果看是否一致。

image-14.png (28.44 KB, 下载次数: 7)

下载附件

14

2025-8-9 18:14 上传


image-15.png (13.69 KB, 下载次数: 7)

下载附件

15

2025-8-9 18:14 上传


不难发现,本地运行的返回[object global],浏览器运行的返回[object Window],明显不一样,这里需要重写toString方法。

window.toString = function toString(){     return '[object Window]'; }

在此运行代码,发现返回的与浏览器一致了,但是衍生出一个新问题,如果运行下面代码会发生什么呢?

log(window.toString.toString());

image-16.png (29.61 KB, 下载次数: 5)

下载附件

16

2025-8-9 18:14 上传


image-17.png (18.47 KB, 下载次数: 8)

下载附件

17

2025-8-9 18:14 上传


这里与浏览器运行的结果又不一样了,是不是也可以拿来作为检测点对不对!不用怕,也有应对方法。

function safeFunction(func) {     Function.prototype.$call = Function.prototype.call;     const $toString = Function.toString;     const myFunction_toString_symbol = Symbol('('.concat('', ')'));     const myToString = function myToString() {         return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this);     }     const set_native = function set_native(func, key, value) {         Object.defineProperty(func, key, {             "enumerable": false,             "configurable": true,             "writable": true,             "value": value         });     }     delete Function.prototype['toString'];     set_native(Function.prototype, "toString", myToString);     set_native(Function.prototype.toString, myFunction_toString_symbol, "function toString() { [native code] }");     const safe_Function = function safe_Function(func) {         set_native(func, myFunction_toString_symbol, "function" + (func.name ? " " + func.name : "") + "() { [native code] }");     }     return safe_Function(func) }

通过safeFunction函数来保护window.toString方法,应用代码并在此运行检测,发现已经与浏览器的一致了。

safeFunction(window.toString);

image-18.png (25.81 KB, 下载次数: 6)

下载附件

18

2025-8-9 18:14 上传

这里在讲解一下原型的补法,执行下列代码并分析。

document.__proto__.toString(); document.__proto__.__proto__.toString();

image-19.png (99.8 KB, 下载次数: 7)

下载附件

19

2025-8-9 18:14 上传


image-20.png (26.89 KB, 下载次数: 7)

下载附件

20

2025-8-9 18:14 上传


发现本地环境这边直接报错了,因为在创建document对象时,给的就是{},这种方式创建的对象默认在Object下,已经是最顶层了,所以无法在调用__proto__.__proto__,所以这里需要补原型了。
document的原型是HTMLDocument,下面给出代码:

class HTMLDocument{}; safeFunction(HTMLDocument); HTMLDocument.prototype.toString = function toString(){     return "[object HTMLDocument]"; } safeFunction(HTMLDocument.prototype.toString); document = new HTMLDocument();

按照这种思路一边看日志分析一边与浏览器进行对比,按照这个思路来慢慢补就行了,我补了大概500多行左右,就可以正常出值了。下面给出部分补好的环境代码,剩下的仿照继续补。

class EventTarget {     constructor() {         safeFunction(this.addEventListener);         safeFunction(this.dispatchEvent);         this.listeners = {}; // 存储事件监听器     }     // 模拟 addEventListener 方法     addEventListener(type, callback) {         if (!this.listeners[type]) {             this.listeners[type] = [];         }         log("注册监听器", type, callback.toString());         this.listeners[type].push(callback);     }     // 模拟 dispatchEvent 方法     dispatchEvent(event) {         const listeners = this.listeners[event.type];         if (listeners) {             listeners.forEach(listener => listener(event));         }     } } window = global; window.top = window; window.self = window; window.Buffer = Buffer; delete window.navigator; delete global delete Buffer delete process; delete __dirname; delete __filename; class HTMLDocument extends EventTarget { }; HTMLDocument.prototype.characterSet = "UTF-8"; HTMLDocument.prototype.charset = "UTF-8"; HTMLDocument.prototype.cookie = ""; HTMLDocument.prototype.URL = "https://turing.captcha.gtimg.com/1/template/drag_ele.html"; document = new HTMLDocument(); screen = {}; screen.toString = function toString() {     return "[object Screen]"; }; safeFunction(screen.toString); location = {     href: "https://turing.captcha.gtimg.com/1/template/drag_ele.html" }; location.toString = function toString() {     return "https://turing.captcha.gtimg.com/1/template/drag_ele.html"; } safeFunction(location.toString); class Navigator { }; Navigator.prototype.platform = "MacIntel"; Navigator.prototype.languages = ["zh-CN", "zh"]; Navigator.prototype.vendor = "Google Inc."; Navigator.prototype.appName = "Netscape"; Navigator.prototype.hardwareConcurrency = 10; Navigator.prototype.webdriver = false; Navigator.prototype.cookieEnabled = true; Navigator.prototype.appVersion = ""; Navigator.prototype.userAgent = ""; Navigator.prototype.serviceWorker = watch({}, "navigator.serviceWorker"); Navigator.prototype.requestMIDIAccess = function requestMIDIAccess() {     debugger; }; safeFunction(Navigator.prototype.requestMIDIAccess); Navigator.prototype.toString = function toString() {     return "[object Navigator]"; }; safeFunction(Navigator.prototype.toString); navigator = new Navigator(); 滑块裁剪

滑块的原本图像为这样

image-21.png (67.19 KB, 下载次数: 5)

下载附件

21

2025-8-9 18:14 上传


这里就需要裁剪,裁剪的代码,

img = cv2.imread("slide.png", cv2.IMREAD_UNCHANGED) x_start, y_start = 140, 490 width, height = 120, 120  # 假设你要切割 100x100 的区域 cropped_image = img[y_start : y_start + height, x_start : x_start + width] 缺口位置识别

接着提取滑块的缺口位置,这里使用ddddocr来实现

from ddddocr import DdddOcr det = DdddOcr(det=False, ocr=False) res = det.slide_match(     open("slide.png", "rb").read(),     open("bg.png", "rb").read(),     simple_target=True )["target"] ans = f'[{{"elem_id":1,"type":"DynAnswerType_POS","data":"{res[0]},{res[1]}"}}]' pow_answer 和 pow_calc_time 实现

pow_answer本质上是通过 验证码接口返回的 prefix 加上数字在经过哈希算法的结果与返回的md5的值一致就是。下面是通过ChatGPT5生成的计算代码。

def get_workload_result(nonce, target, timeout_ms=30000):     """     通过暴力枚举,找到一个整数 u 使得 MD5(nonce + u) == target     :param nonce: 字符串前缀     :param target: 目标 MD5 值     :param timeout_ms: 超时限制,单位毫秒     :return: 字典 {'ans': u, 'duration': 耗时毫秒}     """     start_time = time.time()     u = 0     while True:         # 计算 MD5         hash_result = md5_hash(f"{nonce}{u}")         # 如果匹配,返回结果         if hash_result == target:             return {                 "ans": f"{nonce}{u}",                 "duration": int((time.time() - start_time) * 1000),             }         # 检查超时         if (time.time() - start_time) * 1000 > timeout_ms:             break         # 增加 u         u += 1     # 如果超时,返回当前 u 和已消耗时间     return {"ans": f"{nonce}#{u}", "duration": (time.time() - start_time) * 1000}

注意有个问题,如果这里的pow_answer计算随机的话,接口也会给你返回正确的参数,但是!将该参数提交过去验证,就会提示验证码错误!

image-22.png (57.65 KB, 下载次数: 7)

下载附件

22

2025-8-9 18:14 上传


正确计算的结果

image-23.png (57.44 KB, 下载次数: 6)

下载附件

23

2025-8-9 18:14 上传

最后

补环境一定要仔细,耐心,中间不会的问问AI,再结合实践,你也是可以的!


 

免费评分 参与人数 16吾爱币 +16 热心值 +15 理由

MenH
    + 1   谢谢@Thanks!  

ghdisjdgdijd
  + 1   + 1   我很赞同!  

425529
  + 1   + 1   谢谢@Thanks!  

xioabo
    + 1   热心回复!  

gpchhao
  + 1   + 1   谢谢@Thanks!  

玖卿
  + 2   + 1   我很赞同!  

user23456
  + 1   + 1   谢谢@Thanks!  

zxzx307
  + 1   + 1   谢谢@Thanks!  

fengbolee
  + 2   + 1   用心讨论,共获提升!  

ioyr5995
  + 1   + 1   热心回复!  

Eddie-sjm
  + 1   + 1   我很赞同!  

allspark
  + 1   + 1   用心讨论,共获提升!  

liuxuming3303
  + 1   + 1   谢谢@Thanks!  

mjhwzwg6
  + 1   + 1   谢谢@Thanks!  

fiscivaj
  + 1     谢谢@Thanks!  

helian147
  + 1   + 1   热心回复!  

查看全部评分