亿企赢逆向手记(二):验证码不是墙——ddddocr 自动识别闭环

张 张站长 · 2026-09-08 17:55 · 技术-逆向 · 31 次浏览 · 0 条评论

取图/校验被拆成两个接口、只靠一个可重试的 id 兜底,ddddocr 一行识别+重试就能闭环回登录。

「亿企赢逆向手记」第二篇。承接上篇登录第三步那个「弱密码才触发」的图形验证码,把取图、识别、校验、回注登录的完整闭环拆开。自己账号自动化,技术学习向。

上篇留了尾巴:doLogin 遇到弱密码账号会回 检测到您的密码较为常见或复杂度较低,要求先过一个 4 字符图形验证码。这篇讲它为什么好自动化——因为服务端把它设计得太解耦了。

三个接口,用一个 id 串起来

方法 端点 作用
GET ucapi/auth/authImageServlet 按 id 取一张验证码图(PNG)
POST ucapi/auth/web/checkVerCode 提交识别结果 + 同一个 id
POST ucapi/auth/v1/oauth2.0/doLogin 带上已校验的 id 放行

关键:取图和校验是两个独立接口,中间靠一个前端自造的 id 绑定。checkVerCode 通过后,这个 id 就「盖了章」,doLogin 认它即放行。这种解耦对用户体验友好(能单独刷新验证码),但对自动化极其有利——我可以单独盯着 checkVerCode 反复重试到过为止。

取图:id 是前端自己造的

_timestamp   = str(round(time.time() * 1000))
id_timestamp = _timestamp + str(random.randint(100, 999))   # ← 本次验证码的 id

params = {
    'appId': '19060100010000',
    'scene': 'login',
    'h': '44', 'w': '120',       # 服务端按此尺寸出图
    't': _timestamp,
    'id': id_timestamp,          # 取图时就把 id 报给服务端
}
resp_img = self.session.get(
    "https://www.17dz.com/ucapi/auth/authImageServlet",
    params=params, cookies=self.final_cookies, headers=self.headers_common,
)
# resp_img.content 就是 120x44 的 PNG 字节流

注意 id 不是服务端下发的,是前端用「毫秒时间戳 + 三位随机数」拼的。服务端把「这张图的答案」和这个 id 绑定存起来,等 checkVerCode 拿同一个 id 来核对。所以取图和校验必须用同一个 id——这是整段能闭环的枢纽。

识别:ddddocr 一行

import ddddocr
ocr = ddddocr.DdddOcr(beta=True, show_ad=False)
vercode = ocr.classification(resp_img.content)

这类规整的 4 位数字/字母验证码,ddddocr 的通用模型识别率已经很高,不用自己训模型。beta=True 用它第二套模型,对这种字符往往更准。识别本身就这一行——真正要工程处理的是识别会错,所以要重试。

校验 + 重试:每轮换新图新 id

for attempt in range(1, max_retries + 1):     # 默认 3 轮
    # 1) 取图(每轮都重新取,id 也每轮新生成)
    # 2) OCR 得到 vercode
    data = {
        'verCode':     vercode,
        'appId':       '19060100010000',
        'scene':       'login',
        'id':          id_timestamp,           # ← 和取图同一个 id
        'redirectUri': redirect_uri,
        '_t':          round(time.time() * 1000),
    }
    result = self.session.post(
        "https://www.17dz.com/ucapi/auth/web/checkVerCode",
        cookies=self.final_cookies, data=data, headers=self.headers_common,
    ).json()

    if result.get("data") is True:             # 校验通过
        return {'id': id_timestamp, 'vercode': vercode, ...}
    time.sleep(1.5)                             # 没过,下一轮换新图新 id
raise RuntimeError("验证码连续校验失败")

返回极简:{"data": true} 即通过。为什么每轮要重新取图——一旦 checkVerCode 失败,那个 id 通常就作废了,不能拿同一张图反复猜,必须换新 id 重来。识别错不可怕,max_retries 轮里只要一轮对上,就拿到一个「已校验」的 id。

回注登录

拿到盖了章的 id,回到上篇的登录第二轮,把它塞进 doLogin:

verification_id = cap_result['id']
# ... 第二轮 doLogin ...
if verification_id:
    login_data['id'] = verification_id     # 服务端认这个 id → 放行

于是完整的「带验证码登录」是一个两轮循环:第一轮 doLogin 撞到弱密码提示 → 去过验证码拿 id → 第二轮 doLogin 带 id 成功。

逆向视角的两点

① 验证码的触发条件本身就是情报。 这里是「弱密码账号」才触发,是一种风险补偿而非通用门槛。搞清楚触发条件(账号维度、IP 维度、还是行为维度),比无脑每次都过一遍更省事,也更不容易因为「不该出验证码时你却去取图」而露馅。

② 一个校验环节的强度,取决于它和上下文的耦合度。 取图/校验/登录三段解耦、只靠一个可重试的 id 兜底——这意味着识别率不用高,重试就能弥补;也意味着校验和登录之间没有更强的绑定(比如没有把图片答案的哈希和会话、时间强绑)。真正难绕的验证码,会把「识别」和「使用」紧紧焊死,让你没法把它单拎出来暴力重试。 这道恰恰相反。

下一篇:每个请求头都挂着的 X-Irc-Data——解开是一段 base64 的浏览器指纹,怎么伪造、怎么随机化、为什么它这么好绕。

评论 0

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用