[叫我小杨同学] 祝贺dsh破甲插件突破2k!!!发一篇过滑块的理论概念!!!

叫我小杨同学 2026-09-24 11:25 1


叠甲



我感觉肯定会有很多人说,你只会说理论,也不放文件,什么什么的话,遇到这种我直接不回复


个人说实话,已经这个时代了,你都知道理论了,拿去给AI,我不相信出不来,所以那些需要假将饭喂到嘴里的"伸手党"一律略过


进入主题


滑块是怎么样的一个东西


拿阿里云的举例,也就是Zai那个使用的,应该是v3


能看到的那种验证,基本上是一张背景图,左边挂着一块拼图,底下一条能拖的滑条。

这个图怎么出现或者说怎么来的,是页面里会加载阿里云的一段 JavaScript(AliyunCaptcha.js)名称不固定,但是差不多就是一个js代码,。脚本自己去下载背景和拼图,然后你拖动的时候会计算你的差距,对了就给你一串密文。写代码的话其实就是:算出缺口在哪,然后模拟真人拖动滑块把拼图拖过去。


find_gappiece_x这些名称不是固定的,不懂扔给ai


flowchart LR
A[浏览器打开业务页的 https 地址] --> B[加载官方滑块脚本]
B --> C[拿到背景图和拼图]
C --> D[find_gap 算出原图上的 x]
D --> E["target = x * 屏幕宽 / 原图宽"]
E --> F[按住滑条 反复读拼图位置 拖到 target]
F --> G[脚本把原文放进页面变量]
G --> H[原文交给网站]

所以这就说明了,所有的那种http本地解码都是扯淡,除非是打码平台,那种是可以做到


还有一种就是你把所有的图片保存下来, 然后一个一个去对比写清楚


否则必须启动浏览器,你要加载资源,加载一些环境,补环境其实就是不想启动浏览器才操作的

(PS:可能我有说的不对的地方, 在评论区指出给大家解惑)


2. V2 和 V3 的区别


区别在缺口的图,不在函数名叫什么。


flowchart TB
subgraph V2[V2 物品]
A1[背景上能看见一块缺口或阴影] --> A2[拼图常常是整块不透明]
A2 --> A3[提取边缘 或 拿整块去对一次]
A3 --> A4[分数最高的位置就是洞]
end
subgraph V3[V3 洞被涂平]
B1[洞被抹平 边上还留着原来的颜色] --> B2[拼图是带透明通道的异形]
B2 --> B3[拿颜色去对 会贴到旁边那个真物体]
B3 --> B4["三路: 边 xe、糊 xs、边减内部 xh"]
B4 --> B5["最终 x = 选中的峰 - 左边透明宽度"]
end

gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3, 3), 0)
edge = cv2.Canny(blur, 80, 160)
# piece_edge 是拼图外轮廓
score = cv2.matchTemplate(edge, piece_edge, cv2.TM_CCOEFF_NORMED)
_, _, _, peak = cv2.minMaxLoc(score)
x = peak[0]

V3:边的峰和糊的峰通常差不超过 20 像素,投票会取它们的平均。


所以v2就是一个可以看到边缘,区别的拖动进去,但是v3就要注意像素值


还是使用opencv-pythonnumpy,OpenCV这就是py的模块


下面 id 一般都是是官方脚本生成的:
































元素 id
背景 aliyunCaptcha-img
拼图 aliyunCaptcha-puzzle
滑条 aliyunCaptcha-sliding-slider
要点一下才出现的区域 aliyunCaptcha-captcha-body
换一张 aliyunCaptcha-btn-refresh

一般的流程


下面就是一般要注意的和走的流程


flowchart TD
A[打开浏览器] --> B[用业务页地址打开 只替换这一次网页内容]
B --> C[等官方脚本的初始化函数出现]
C --> D[点一次开始验证]
D --> E{滑条和两张图都出现了?}
E -->|没有 连着几次| Z[这条网络不行 换一条]
E -->|出现了| F[等两张图的地址连续两次不变]
F --> G[按完整地址取出图片字节]
G --> H[find_gap]
H --> I{突出程度 >= 0.085?}
I -->|是| J[只拖主峰]
I -->|否| K[主峰 边减内部 边 糊 最多 3 个点]
J --> L[drag_to]
K --> L
L --> M{松开后还差 3 像素以上?}
M -->|是| B
M -->|否| N{页面变量里有原文?}
N -->|没有 且是超时| Z
N -->|没有| B
N -->|有| P{网站接受?}
P -->|接受| Q[告诉脚本通过 结束]
P -->|拒绝 未满 3 张| B
P -->|拒绝 已满 3 张| Z

点「开始验证」之前,页面上往往只有一个按钮,没有拼图


代码


const REGION = "cn";       // 换成你的
const PREFIX = "你的前缀";
const SCENE = "你的场景号";

window.AliyunCaptchaConfig = { region: REGION, prefix: PREFIX };
window.__cvp = ""; // 脚本给出的原文,只写第一次
window.__cerr = ""; // 失败原因
window.__cvDecision = ""; // 你告诉脚本:网站认了还是不认

function readParam(p) {
if (typeof p === "string") return p;
return (p && (p.captchaVerifyParam || p.CaptchaVerifyParam)) || JSON.stringify(p || {});
}

function boot() {
initAliyunCaptcha({
SceneId: SCENE,
mode: "embed",
region: REGION,
prefix: PREFIX,
language: "cn",
element: "#captcha-box",
button: "#captcha-btn",
success(p) {
if (!window.__cvp) window.__cvp = readParam(p);
},
captchaVerifyCallback(p) {
window.__cvp = readParam(p);
window.__cvDecision = "";
return new Promise((resolve) => {
const timer = setInterval(() => {
if (window.__cvDecision === "ok") {
clearInterval(timer);
resolve({ captchaResult: true, bizResult: true });
} else if (window.__cvDecision === "bad") {
clearInterval(timer);
resolve({ captchaResult: false, bizResult: false });
}
}, 200);
});
},
fail(e) {
window.__cerr = JSON.stringify(e || {});
},
});
}

脚本用官方地址,加载完再 boot()


https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js


这段代码跑在浏览器页面里,用来把阿里云官方滑块脚本拉起来,并留三个窗口变量,让外面的程序能拿走验证原文。


三个变量



  • __cvp:脚本算出的验证原文。第一次成功回调只写一次,避免被后面的空值盖掉。

  • __cerr:脚本自己报失败时,把原因存下来。

  • __cvDecision:外面的程序写 "ok""bad"。脚本在等这个字,才决定这次验证算过还是算不过。


readParam


官方回调有时直接给字符串,有时给对象。这个函数统一取出里面的 captchaVerifyParam(大小写两种都认)。取不到就整段转成字符串,保证 __cvp 里始终是一段原文。


boot


调用官方的 initAliyunCaptcha,把滑块嵌进页面:



  • SceneIdregionprefix 换成你自己的。

  • element 是拼图容器,button 是「开始验证」按钮。

  • success:脚本认为本地滑过了,若 __cvp 还空着,就把原文写进去。

  • captchaVerifyCallback:用户松手后脚本会进这里。先把原文写入 __cvp,再每 200 毫秒看一次 __cvDecision。外面的程序拿这段原文去问网站,网站返回接受就写 "ok",拒绝就写 "bad"。脚本收到后才结束这次验证。

  • fail:脚本内部失败,原因进 __cerr


替换网页时只拦 document,其它请求放行:


def fulfill(route):
if route.request.resource_type == "document":
route.fulfill(status=200, content_type="text/html; charset=utf-8", body=html)
else:
route.continue_()

page.route("https://你的站点/**", fulfill)
page.goto("https://你的站点/出现滑块的那一页", referer="https://你的来源页/")

背景图,滑块图:都加载完,地址不是空、不是同一张,而且同一对地址连续两次检查都没变。


const key = bgUrl + "|" + pieceUrl;
if (lastKey !== key) {
lastKey = key;
stable = 0;
return false;
}
stable += 1;
return stable >= 2;

从页面读后面要用的数:


const img = document.querySelector("#aliyunCaptcha-img");
const piece = document.querySelector("#aliyunCaptcha-puzzle");
const imgBox = img.getBoundingClientRect();
const pieceBox = piece.getBoundingClientRect();

const pieceX = pieceBox.x - imgBox.x; // 拼图现在离背景左边多远,屏幕像素
const pieceY = pieceBox.y - imgBox.y;
const scale = imgBox.width / img.naturalWidth; // 两个宽度都要大于 1

图片字节用浏览器刚刚下载的那一份,键必须是带问号的完整地址。背景和拼图经常路径相同、只有问号后面不同。砍掉问号会拿到另一张图。


raw = cache[full_url]  # 不要用 full_url.split("?")[0]
img = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_UNCHANGED)

IMREAD_UNCHANGED 用来留下透明通道。拼图变成普通彩色图后,后面的形状是空的。


缺口怎么算


flowchart TD
IN[背景彩色图 + 拼图四通道图] --> BOX[透明值大于 20 的像素 框出外接矩形]
BOX --> X0[记下矩形左边的空白宽度 x0]
BOX --> YQ{实心高度 >= 拼图高度的 75%?}
YQ -->|是| Y1["y = round(pieceY / scale)"]
YQ -->|否| Y2["y = round(实心顶部 * 背景高 / 拼图高)"]
Y1 --> BAND[只在这条横带里比较]
Y2 --> BAND
BAND --> XE[xe 轮廓对边缘 取最高]
BAND --> XS[xs 拼图内部对细节图 取最低]
BAND --> XH["xh = 边有多像 - 中间有多像 取最高"]
XE --> VOTE[投票]
XS --> VOTE
XH --> VOTE
VOTE --> OUT["x = 选中的峰 - x0"]

框出拼图形状


alpha = piece[:, :, 3]
ys, xs = np.where(alpha > 20)
y0, y1 = int(ys.min()), int(ys.max()) + 1
x0, x1 = int(xs.min()), int(xs.max()) + 1 # x0 是左边透明有多宽
crop = piece[y0:y1, x0:x1]
mask = (crop[:, :, 3] > 20).astype(np.uint8) * 255
h, w = mask.shape

拖的是整张拼图元素的左边缘,真正的形状在 x0 的右边。最后每个横坐标都要减一次 x0,不能减两次。


横带的上下位置:


if (y1 - y0) >= piece.shape[0] * 0.75:
y = int(round(piece_y / scale))
else:
y = int(round(y0 * bg.shape[0] / piece.shape[0]))
if y < 0:
y = 0
if y + h > bg.shape[0]:
y = bg.shape[0] - h

pad = max(16, int(bg.shape[1] * 0.06))
right = bg.shape[1] - w - 6

宽度 300 的图,0.06 * 300 = 18,所以从第 18 列才开始找。


三路分算


横带切好以后,拼图从左往右扫。每一列打一个分。


gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY)

# xe:背景先模糊再抽边,阈值 80 和 160。拼图外圈画成 1 像素的线。
edge = cv2.Canny(cv2.GaussianBlur(gray, (3, 3), 0), 80, 160)
sil = np.zeros((h, w), np.uint8)
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(sil, cnts, -1, 255, 1)
xe_map = cv2.matchTemplate(edge[y:y + h], sil, cv2.TM_CCOEFF_NORMED)

# xs:用拼图内部去对这张细节图,最糊的地方分最低,取最低的那一列。
detail = cv2.absdiff(gray, cv2.GaussianBlur(gray, (0, 0), 2.2))
inner = cv2.erode(mask, np.ones((3, 3), np.uint8))
xs_map = cv2.matchTemplate(detail[y:y + h], np.zeros((h, w), np.uint8), cv2.TM_SQDIFF, mask=inner)

# xh:边有多像,减去中间有多像。洞在的那一列,这个差最大。
border = cv2.subtract(mask, inner)
color = bg[y:y + h]
like_border = cv2.matchTemplate(color, crop[:, :, :3], cv2.TM_CCORR_NORMED, mask=border)
like_inner = cv2.matchTemplate(color, crop[:, :, :3], cv2.TM_CCORR_NORMED, mask=inner)
xh_map = like_border - like_inner

xexh 要这一排里最高的那一列,xs 要最低的那一列。最高分如果贴在搜索范围的最左边或最右边,多半是边界,把那一头抹掉再找,最多再找两次。


还要看这个峰比旁边高出多少。把峰左右各挖掉一段,剩下的最高(xs 是剩下的最低)拿来比。挖掉的宽度是拼图短边的一半,最少 6 像素。差越大,这一列越站得住。


rad = max(6, min(w, h) // 2)
margin = abs(peak - rival) / (abs(peak) + 1e-6)

margin 接近 0,旁边还有差不多高的峰。


6.3 三个峰合成一个 x


三路经常不指同一列。先看边和糊差多少。


if abs(xe - xs) <= 20:
agreed = round((xe + xs) / 2)
if abs(agreed - xh) <= 16:
best = round((agreed + xh) / 2) # 三路都指到附近,取平均
else:
best = agreed # 边和糊已经对上,第三路先不用
margin = max(edge_margin, smooth_margin)
elif w * h >= 4500:
best = xs # 拼图很大,洞是一整片被涂平的,信糊
margin = smooth_margin
else:
best = xh # 对不上,就信边减内部
margin = hole_margin

spread = max(xe, xs, xh) - min(xe, xs, xh)
if spread > 48:
# 三列散开超过 48 像素,上面的投票可能投错
# 第一名至少 0.055,并且比第二名高出 0.012,改用第一名
if top_margin >= 0.055 and top_margin - second_margin >= 0.012:
best = top_x
margin = top_margin

边和糊相差不超过 20 像素,取它们的中点。这个中点和第三路再相差不超过 16,三路一起平均。拼图宽乘高到了 4500,大块的洞是被涂平的,颜色会认成旁边的真东西,所以改信糊的那一列。面积没到,就用边减内部。


x = best - x0
xe, xs, xh = xe - x0, xs - x0, xh - x0
margin = min(0.99, max(0.0, margin))

举例:峰在第 143 列,左边透明 8 像素,缺口是 143 - 8 = 135。这是原图上的列。页面上背景显示成 260 宽,原图是 300,比例 scale = 260 / 300 = 0.867。鼠标要停的位置是 135 * 0.867 = 117。拼图现在停在 4,还要再走 117 - 4 = 113。135 是原图上的列,117 是屏幕上要停的位置。


突出不到 0.085,这一列不太稳,同一张图多备几个点。按主峰、边减内部、边、糊的顺序收。和已经收下的点相差 10 像素以内,当成同一个。小于 12 或大于 290 的丢掉,滑条右边到不了那么远,最多留 3 个。290 按常见滑轨写的,你的滑条更短就把 290 改小。


换下一个点之前,先把拼图拖回这一轮刚开始时的 pieceX,再从头拖。接着上次停的地方走,位置会对不上。


模拟鼠标拖动


flowchart TD
T["target = x * scale"] --> D[鼠标移到滑条中心并按下]
D --> K{"相差至少 14 像素?"}
K -->|是| K2[先朝目标挪 12 像素]
K -->|否| LOOP
K2 --> LOOP[最多 48 步]
LOOP --> R[读 拼图左边缘减背景左边缘]
R --> S{"还差不超过 1.2?"}
S -->|是| U[松开]
S -->|否| P["这一步等于还差 限制在 -18 到 18 绝对值至少 0.8"]
P --> LOOP
U --> E{"松开后还差超过 2?"}
E -->|是| A[再拖一次]
E -->|否| W[等原文]
A --> ST{仍大于 3?}
ST -->|是| RE[换一张图]
ST -->|否| W

def piece_x(page) -> float:
# 拼图左边缘 - 背景左边缘,每次拖之前重新读
...

dest = gap_x * scale
start = piece_x(page)
if abs(dest - start) >= 14:
mouse_dx = 12 if dest >= start else -12 # 滑条一开始常常拖不动

for _ in range(48):
remain = dest - piece_x(page)
if abs(remain) <= 1.2:
break
step = max(-18.0, min(18.0, remain))
if abs(step) < 0.8:
step = 0.8 if remain > 0 else -0.8
mouse.move(step)
mouse.up()

最后的一些焚决


上述基本上都是一些流程和逻辑


始终记得人机验证现在一点都不难了,难的是风控机制,因为目前所有的大厂都在增强风控机制

以前没有ai,机器的访问并不多,现在越来越多了,所以会发现各大厂商都在接入人机验证


关于风控机制,注意这些


浏览器环境,IP,速率


以上就是一些简单的教程,自己去尝试吧


代码不是唯一的

最新回复 (11)
  • hlool 09-24 11:26
    1

    好文章 ^-^

    感觉1级好点

  • neoooooo0o 09-24 11:28
    2

    好技术文章,点赞点赞,爱来自三月七 ^-^ ^-^

  • 丸子 09-24 11:28
    3

    果然在l站再次看到了逆向大脑的风采,看完收藏懂了点毛

  • 叫我小杨同学 楼主 09-24 11:29
    4

    我要去批判三月七了

  • 還記得你說家是唯一的城堡 隨著稻香河流繼續奔跑 微微笑 小時候的夢我知道 09-24 11:30
    5

    佬在持续输出干货啊,太强了,支持一下~

  • 煎饺 09-24 11:30
    6

    ds4.1flash能破甲使用吗

  • 叫我小杨同学 楼主 09-24 11:31
    7

    翻别的帖子,这不回复这个问题

  • fx318 09-24 11:32
    8

    小杨同学,牛

  • Wolverine 09-24 11:34
    9

    佬的技术干货,认真认真中。。

  • sanden 09-24 11:37
    10

    这个太基础了,讲讲轨迹,傅里叶已经烂了

  • 叫我小杨同学 楼主 09-24 12:08
    11

    大佬就是大佬,请大佬指出,问题是你没认真看嘛,下面不是有鼠标拖动的流程图,扔个ai自己就知道了,什么叫做模拟人类

* 帖子来源Linux.do
返回