Johnjincaaa头像

猿人学第9题「js混淆 - 动态cookie 2」解题记录

猿人学第9题「js混淆 - 动态cookie 2」解题记录

题目链接:https://match.yuanrenxue.cn/match/9

答案(本人账号):26111363

日期:2026-11-07


一、题目目标与规则

  • 请求本页全部 5 页 数据,计算这 50 个值的 加和,提交答案。
  • 第 5 页发送请求时必须把 userAgent 改成 yuanrenxue,否则不返回最后一页数据。
  • 发送请求时必须携带 sessionid;每个已登录用户的数字答案都不同。

二、请求分析

数据接口:

GET https://match.yuanrenxue.cn/api/question/9?page=N&pageSize=10&kw=

请求头里关键的两样东西:

名称说明
sessionid登录态,直接决定返回的是“谁的”数据
m动态生成的 cookie,有时效,验证通过才返回明文数组

行为:

  • 首次请求(不带 m)返回的 data 是一段混淆 JS 字符串;
  • 带有效 m 再请求,data 才是明文数组。

三、逆向思路

1. hook cookie 定位 m 的生成

在浏览器里 hook document.cookie 的 setter,找到种 m 的位置,还原出易读代码:

window = new Array();
for (var m = 0x1; m <= N; m++) {
    res = decrypt(ts) + '\x72';   // '\x72' 就是字符 'r'
}
document['cookie'] = 'm=' + (m - 0x1)['toString']() + res + '; path=/';

即:

m = N + decrypt(ts) + 'r'

其中:

  • N:循环上界,从返回的混淆 JS 里正则提取;
  • ts:响应里内嵌的 10 位时间戳。

2. decrypt = RSA 加密

还原 decrypt:

function decrypt(text) {
    const pub = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5GVku07yXCndaMS1evPIPyWwhbdWMVRqL4qg4OsKbzyTGmV4YkG8H0hwwrFLuPhqC5tL136aaizuL/lN5DRRbePct6syILOLLCBJ5J5rQyGr00l1zQvdNKYp4tT5EFlqw8tlPkibcsd5Ecc8sTYa77HxNeIa6DRuObC5H9t85ALJyDVZC3Y4ES/u61Q7LDnB3kG9MnXJsJiQxm1pLkE7Zfxy29d5JaXbbfwhCDSjE4+dUQoq2MVIt2qVjZSo5Hd/bAFGU1Lmc7GkFeLiLjNTOfECF52ms/dks92Wx/glfRuK4h/fcxtGB4Q2VXu5k68e/2uojs6jnFsMKVe+FVUDkQIDAQAB';
    const e = new JSEncrypt();
    e.setPublicKey(pub);
    return encodeURIComponent(e.encrypt(text));
}

即 decrypt 本质是 2048 位 RSA 公钥加密 + encodeURIComponent,加密对象就是时间戳 ts。

3. 踩坑点

  • m 有时效,生成后必须立即请求,否则过期又返回混淆 JS。
  • m 的前缀是循环上界 N(不用减 1)。
  • udc.js 是 jsjiami.com.v5 混淆:字符串数组 + RC4 解码、大数组顺序混淆、atob / window.crypto 环境检测、无限 debugger。
  • 手写 RSA 容易踩环境坑(crypto 检测、随机填充等),最稳的办法是让浏览器自己算 m。

四、最终代码

思路:不手写 RSA,用 Playwright 打开题目页,让浏览器自己生成 m,再把 sessionid + m 交给 requests 拉 5 页。

# -*- coding: utf-8 -*-
"""
猿人学 第9题「js混淆 - 动态cookie 2」爬虫

用法:
  python solve_crawl.py                                  # 匿名会话
  python solve_crawl.py --sessionid <你的sessionid>      # 你账号的答案
  python solve_crawl.py --cookie "sessionid=xxx; ..."    # 粘贴整段 Cookie
"""
import asyncio
import argparse
import sys
import requests

for _stream in (sys.stdout, sys.stderr):
    try:
        _stream.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass

from playwright.async_api import async_playwright

URL = "https://match.yuanrenxue.cn"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"

BASE_HEADERS = {
    "accept": "*/*",
    "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
    "referer": URL + "/match/9",
    "x-requested-with": "XMLHttpRequest",
    "user-agent": UA,
}


def parse_cookie(cookie_str):
    if not cookie_str:
        return None
    for part in cookie_str.split(";"):
        part = part.strip()
        if part.lower().startswith("sessionid="):
            return part.split("=", 1)[1]
    return None


async def get_session_and_m(sessionid):
    async with async_playwright() as p:
        browser = await p.chromium.launch(channel="chrome", headless=True, args=["--no-sandbox"])
        context = await browser.new_context(user_agent=UA)
        if sessionid:
            await context.add_cookies([{
                "name": "sessionid",
                "value": sessionid,
                "domain": ".match.yuanrenxue.cn",
                "path": "/",
            }])
        page = await context.new_page()
        await page.goto(URL + "/match/9", wait_until="networkidle", timeout=60000)
        m = None
        for _ in range(80):
            cookies = await context.cookies()
            m = next((c["value"] for c in cookies if c["name"] == "m"), None)
            if m:
                break
            await page.wait_for_timeout(250)
        cookies = await context.cookies()
        sid = next((c["value"] for c in cookies if c["name"] == "sessionid"), sessionid or "")
        await browser.close()
    if not m:
        raise RuntimeError("未能生成 m cookie")
    return sid, m


def fetch_and_sum(sessionid, m):
    s = requests.Session()
    s.headers.update(BASE_HEADERS)
    s.cookies.set("sessionid", sessionid, domain="match.yuanrenxue.cn")
    s.cookies.set("m", m, domain="match.yuanrenxue.cn")

    total = 0
    per_page = {}
    for page in range(1, 6):
        headers = dict(BASE_HEADERS)
        if page == 5:
            headers["user-agent"] = "yuanrenxue"
        r = s.get(
            URL + "/api/question/9",
            params={"page": page, "pageSize": 10, "kw": ""},
            headers=headers,
            timeout=20,
        )
        data = r.json().get("data")
        if not isinstance(data, list):
            raise RuntimeError(f"第 {page} 页未拿到数组,m 可能已过期")
        nums = [int(x) for x in data]
        per_page[page] = nums
        total += sum(nums)
        print(f"第 {page} 页: {nums}  小计={sum(nums)}")

    print("-" * 56)
    for page in range(1, 6):
        print(f"  第{page}页 {per_page[page]}")
    print("-" * 56)
    print("答案(总和) =", total)
    return total


def main():
    ap = argparse.ArgumentParser(description="猿人学第9题爬虫")
    ap.add_argument("--sessionid", default=None, help="你已登录的 sessionid")
    ap.add_argument("--cookie", default=None, help="浏览器整段 Cookie 字符串")
    args = ap.parse_args()

    sessionid = args.sessionid or parse_cookie(args.cookie)
    if args.cookie and not sessionid:
        print("警告: 未能从 --cookie 中解析出 sessionid,将以匿名会话运行")
    if sessionid:
        chk = requests.Session()
        chk.headers.update({"user-agent": UA})
        chk.cookies.set("sessionid", sessionid, domain="match.yuanrenxue.cn")
        try:
            login = bool(chk.get(URL + "/api/user", timeout=15).json().get("isLogin"))
        except Exception:
            login = False
        if not login:
            print(f"警告: 该 sessionid 已失效或未登录: {sessionid}")
            print("      请重新登录后复制新的 sessionid,本次以匿名会话运行")
            sessionid = None

    sessionid, m = asyncio.run(get_session_and_m(sessionid))
    print("sessionid =", sessionid)
    print("m         =", m[:60] + "...")
    fetch_and_sum(sessionid, m)


if __name__ == "__main__":
    main()

运行:

python solve_crawl.py --sessionid a7tk93g4qzjf2wzjxohbnj75cix2qhyh

五、运行结果

sessionid = a7tk93g4qzjf2wzjxohbnj75cix2qhyh
第 1 页: [845342, 478914, 625140, 357765, 505428, 395262, 434411, 355931, 721870, 223326]  小计=4943389
第 2 页: [952507, 577739, 149953, 276184, 722346, 849033, 550283, 141163, 352705, 347246]  小计=4919159
第 3 页: [283936, 283389, 610606, 500036, 130340, 249192, 795179, 469437, 587619, 597259]  小计=4506993
第 4 页: [897610, 314521, 960760, 219332, 618791, 692427, 920053, 696008, 903900, 513455]  小计=6736857
第 5 页: [111810, 226174, 469122, 634180, 781892, 726554, 402923, 478109, 577089, 597112]  小计=5004965
--------------------------------------------------------
答案(总和) = 26111363

六、总结

  1. 第 9 题的核心是 m cookie:m = N + RSA(ts) + 'r'。
  2. udc.js 混淆重、环境检测多,手写 RSA 容易翻车。
  3. 用 Playwright 让浏览器替我们生成 m,再用 requests 快速抓 5 页,是最稳的组合。
  4. 别忘了两件事:第 5 页 UA 改成 yuanrenxue、用自己登录的 sessionid(每个账号答案不同)。

完结撒花 ✿✿ヽ(°▽°)ノ✿

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_91134400/article/details/170762122

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--