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

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 上传
返回值分析:
验证码提交接口
手动提交时,会请求一个验证接口,参数如下:

image-2.png (222.47 KB, 下载次数: 7)
下载附件
2
2025-8-9 18:14 上传
请求参数分析:
补环境
这里仅给出部分补环境代码与思路,按照思路来是没有问题的。
堆栈分析
通过验证请求接口分析,查看请求堆栈:

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, 下载次数: 5)
下载附件
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,再结合实践,你也是可以的!