# 舞萌DX(maimai DX)扫码登录协议技术规范 > **来源文档**: > - `docs-old/舞萌DX_aimedb调用链分析.md` —— 完整调用链、二维码格式、ChimeLib/aimedb 关系 > - `docs-old/chimelib协议逆向.md` —— 纯网络实现细节、签名算法、抓包验证、关键坑点 > > **适用场景**:纯网络(纯 HTTP)复刻舞萌DX国服微信扫码登录,获取 userId 与 token > **协议路径**:路径 A —— 微信扫码(HTTPS + JSON,`chimelib_dll.dll` + WinHTTP) > **非适用**:路径 B —— 传统 Aime 物理卡(TCP 二进制,`amdaemon_api.dll` + IPC) --- ## 一、架构总览 ### 1.1 两条 aimedb 通信路径 | 路径 | 名称 | 协议 | 核心组件 | 你实际用的 | |------|------|------|----------|------------| | **A** | 微信扫码 | HTTPS + JSON | `chimelib_dll.dll` (WinHTTP) | ✅ **是** | | B | 传统 Aime 物理卡 | TCP 二进制 | `amdaemon_api.dll` (IPC) | ❌ 否 | ### 1.2 调用链(路径 A) ``` 游戏客户端 Assembly-CSharp │ ChimeReaderManager (驱动扫码、取结果) ▼ ChimeLib.NET.dll (托管封装) │ Chime / ChimeUnit / ChimeResult / CCommGetUserData / CCommServAliveCheck ▼ chimelib_dll.dll (x64 原生,62KB,静态链接 WinHTTP) │ CCommGetUserData_Create/Execute/IsEnd/GetUserID/GetToken │ CCommServAliveCheck_Create/StartCheck/Execute/GetIndex/IsAlive ▼ aimedb 服务器集群 │ https://ai.sys-all.cn/ (主) │ https://ai.sys-allnet.cn/ (备) ▼ POST /wc_aime/api/get_data → 返回 userID + token POST /wc_aime/api/alive_check → 返回 "alive" ``` ### 1.3 关键数据流向 ``` 二维码串(SGWCMAID...) → ChimeLib.NET.CCommQRCode.IsValid() 校验 → EraseHashIdentifier() 取 [20:] 64位载荷 → CCommGetUserData_Create(strGameID="MAID", strChipID, strCommonKey="", strQRData=载荷, strTitleKey="SDGB", serverURLIndex) → WinHTTP HTTPS POST /wc_aime/api/get_data → 响应 {errorID, userID, token, key, timestamp} → ChimeResult.GetChimeId() → userId (uint) → ChimeResult.GetToken() → token (string) → OperationManager.SetToken(userId, token) 缓存 → 后续游戏服务器 API 携带 token ``` --- ## 二、二维码格式规范 ### 2.1 完整二维码字符串(84 字符) ``` 结构:SGWC(4) + MAID(4) + timestamp(12) + payload(64 hex) = 84 字符 偏移 长度 字段 说明 示例 0–3 4 微信标识 常量 "SGWC" SGWC 4–7 4 GameID "MAID"=舞萌DX MAID 8–19 12 时间戳 yymmddHHMMss (UTC+9) 260613133715 20–83 64 载荷/哈希 64位十六进制(疑似SHA-256) 77503FD2...427759BCA5 ``` ### 2.2 校验规则(`CCommQRCode.IsValid()`) ```python def is_valid(qr: str) -> bool: return len(qr) == 84 and qr[0:4] == "SGWC" and qr[4:8] == "MAID" ``` ### 2.3 传给原生层的数据(`EraseHashIdentifier()`) ```python def erase_hash_identifier(qr: str) -> str: """去掉前 20 字符,返回 64 位载荷""" return qr[20:] # 即 payload 部分 ``` ### 2.4 二维码 URL ⇄ 字符串互转 **URL 格式**: ``` https://wq.wahlap.net/qrcode/req/{GameID+timestamp+payload}.html?l=...&t=...&d=... └──────────── 80 字符 ────────────┘ ``` **提取规则**: ```python from urllib.parse import urlparse import re def url_to_qr_string(url: str) -> str: """URL → 完整 84 字符二维码串""" path = urlparse(url).path match = re.search(r"/qrcode/req/([^/]+)\.html", path) if not match: raise ValueError("URL 路径格式不符") return "SGWC" + match.group(1) # 前加 SGWC 前缀 def qr_string_to_url_body(qr: str) -> str: """完整二维码串 → URL 路径段(80字符,无 SGWC)""" return qr[4:] # 去掉 SGWC ``` **查询参数**(`l`/`t`/`d`):**不参与**机台协议,仅为微信侧生成二维码时的元数据。 --- ## 三、服务器端点与网络协议 ### 3.1 服务器地址 | 环境 | 域名 | IP | 端口 | TLS | |------|------|-----|------|-----| | 主 | `ai.sys-all.cn` | 81.71.193.236 | 443 | TLSv1.2 | | 备 | `ai.sys-allnet.cn` | (同解析) | 443 | TLSv1.2 | ### 3.2 API 端点 | 端点 | Method | Content-Type | User-Agent | 用途 | |------|--------|--------------|------------|------| | `/wc_aime/api/alive_check` | POST | `application/json` (body `{}`) | 无 | 探活,返回 `200 alive` | | `/qrcode/api/alive_check` | POST | 无(空body) | `WC_AIME_LIB` | 探活变体,返回 `200 alive` | | `/wc_aime/api/get_data` | POST | **无** | `WC_AIME_LIB` | **核心登录接口** | ### 3.3 请求头(关键:无 Content-Type) ```http POST /wc_aime/api/get_data HTTP/1.1 Host: ai.sys-allnet.cn User-Agent: WC_AIME_LIB Connection: Keep-Alive Content-Length: 243 {"chipID":"...","openGameID":"MAID","key":"...","qrCode":"...","timestamp":"...","titlekey":"SDGB"} ``` > ⚠️ **严禁添加 `Content-Type: application/json`**,否则服务端返回 `errorID=57`(HTTP 200 + 合法 JSON,极易误判为签名错误)。 ### 3.4 请求体 JSON 字段 | 字段 | 类型 | 来源 | 说明 | |------|------|------|------| | `chipID` | string | Keychip ID | 机台唯一标识,如 `XXXX-XXXXXXXXXXX` | | `openGameID` | string | 固定 | `"MAID"` | | `key` | string | 签名计算 | SHA-256 大写 hex,**核心签名字段** | | `qrCode` | string | 二维码[20:] | **64 位载荷,非完整 84 字符串** | | `timestamp` | string | 本机本地时间 | yymmddHHMMss 格式 | | `titlekey` | string | 固定 | `"SDGB"` | ### 3.5 响应体 JSON 字段 | 字段 | 类型 | 说明 | |------|------|------| | `errorID` | int | 0=成功,非0=失败 | | `userID` | int | 成功时为正整数(如 12345678),失败时 -1 | | `token` | string | 成功时 20 位临时凭据,失败时空串 | | `key` | string | 服务端返回的签名(可忽略) | | `timestamp` | string | 服务端时间戳 | --- ## 四、签名算法(核心) ### 4.1 算法规范 ```python import hashlib SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" # 32字符,含前导 X def calculate_key(chip_id: str, timestamp: str) -> str: """ key = SHA-256(chipID + timestamp + SIGN_KEY).hexdigest().upper() 参数: chip_id: 如 "XXXX-XXXXXXXXXXX" timestamp: 如 "261001004115" (yymmddHHMMss) 返回:64字符大写十六进制 """ sign_input = chip_id + timestamp + SIGN_KEY return hashlib.sha256(sign_input.encode()).hexdigest().upper() ``` ### 4.2 技术细节 | 项目 | 值 | 验证来源 | |------|-----|----------| | 哈希算法 | SHA-256 | Hook `CryptCreateHash` ALGID=0x800C (CALG_SHA_256) | | 密钥串 | `XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW` | .rdata UTF-16 @0x00B040,**含前导 X** | | 拼接顺序 | `chipID + timestamp + KEY` | **不含 qrCode、不含 titlekey** | | 输出编码 | 大写十六进制 | hex 表 `0123456789ABCDEF` @0x00A440 | ### 4.3 抓包验证向量 ``` 输入: chipID = "XXXX-XXXXXXXXXXX" timestamp = "261001004115" SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" SHA256("XXXX-XXXXXXXXXXX" + "261001004115" + "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW") = 67015F28CFC3C7FA5E629BECE54893B581A6DBD153426AE5E6BA1681C6D8E327 ``` 与 mitmproxy 抓包完全一致 ✓ --- ## 五、完整请求流程(纯 Python 实现) ### 5.1 最小可用代码 ```python import hashlib, http.client, json, ssl from datetime import datetime SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" CHIP_ID = "XXXX-XXXXXXXXXXX" # 可配置 HOST = "ai.sys-allnet.cn" PATH = "/wc_aime/api/get_data" def parse_qr_login(qr_input: str, chip_id: str = CHIP_ID) -> tuple[int, str]: """ 输入:二维码串(84字符)或 URL 返回:(user_id: int, token: str) """ # 1. 统一输入 → 标准 84 字符串 qr_string = normalize_qr_input(qr_input) # 2. 校验格式 validate_qr_string(qr_string) # 3. 构造请求体 ts = datetime.now().strftime("%y%m%d%H%M%S") key = calculate_key(chip_id, ts) qr_code = qr_string[20:] # 仅 64 位载荷 body_dict = { "chipID": chip_id, "openGameID": "MAID", "key": key, "qrCode": qr_code, "timestamp": ts, "titlekey": "SDGB", } body_bytes = json.dumps(body_dict, separators=(",", ":")).encode() # 4. 发送请求(关键:无 Content-Type) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE conn = http.client.HTTPSConnection(HOST, 443, context=ctx, timeout=20) try: conn.request("POST", PATH, body=body_bytes, headers={ "Connection": "Keep-Alive", "User-Agent": "WC_AIME_LIB", "Content-Length": str(len(body_bytes)), # ★ 绝不加 Content-Type }) resp = conn.getresponse() data = json.loads(resp.read().decode()) finally: conn.close() # 5. 解析响应 if data.get("errorID") != 0 or data.get("userID") == -1: raise RuntimeError(f"登录失败: errorID={data.get('errorID')}, userID={data.get('userID')}") return data["userID"], data["token"] def normalize_qr_input(qr_input: str) -> str: """URL 或字符串 → 标准 84 字符二维码串""" qr = qr_input.strip() if qr.startswith("http://") or qr.startswith("https://"): qr = url_to_qr_string(qr) return qr def validate_qr_string(qr: str) -> None: """校验二维码格式,不通过抛 ValueError""" if len(qr) != 84: raise ValueError(f"长度应为84,实际{len(qr)}") if not qr.startswith("SGWC"): raise ValueError("必须以 SGWC 开头") if qr[4:8] != "MAID": raise ValueError(f"GameID 必须为 MAID,实际 {qr[4:8]}") # 可选:校验 payload 为 64 位 hex if not all(c in "0123456789ABCDEF" for c in qr[20:]): raise ValueError("载荷必须为大写 hex") ``` ### 5.2 关键实现要点清单 | 要点 | 正确做法 | 错误做法后果 | |------|----------|--------------| | qrCode 字段 | `qr_string[20:]` (64位) | 传完整 84 字符 → 签名不匹配 | | 签名拼接 | `chipID + timestamp + KEY` | 加入 qrCode/titlekey → 签名错误 | | 密钥串 | 含前导 `X` (32字符) | 漏掉 X → 签名完全不同 | | 请求头 | 无 `Content-Type` | 加上 → `errorID=57` | | timestamp | 本机本地时间即可 | 纠结时区 → 其实服务端不强校验 | | SSL 验证 | 关闭验证 | 开启 → 证书链可能不通过 | --- ## 六、错误码对照 | errorID | 含义 | 处理建议 | |---------|------|----------| | 0 | 成功 | 正常返回 userID + token | | 1-8 | 原生 ChimeErrorId | 读卡器/服务器/二维码无效等通用错误 | | **57** | **二维码已失效/已被使用/请求头不符** | 检查:二维码是否过期(~10分钟)、是否重复提交、**请求头是否含 Content-Type** | | `userID:-1` | 失败标志 | 配合 errorID 判断具体原因 | > ### ★ 实测补充(2026-10-06) > > 本工程用同一套参数实测到的失败响应是 **`errorID=2`**(不是 57): > > ```json > {"errorID": 2, "key": "B744ADD5...", "timestamp": "261006125706", > "userID": -1, "token": ""} > ``` > > **触发条件是「二维码过期」,不是「用过一次」。** > > | 二维码生成时刻(timestamp 内嵌) | 使用时刻 | 结果 | > |---|---|---| > | `261006012047`(01:20:47) | 01:21:55(+68s) | ✅ 成功 | > | `261006124637`(12:46:37) | 12:52:22(+5m45s) | ✅ 成功 | > | `261006130621`(13:06:21) | 13:10:42(+4m21s) | ✅ 成功 | > | 上面那个 12:46:37 的二维码 | 13:0x(+约 15min) | ❌ `errorID=2` | > > 即:**二维码是「有时效」的,不是「按次数计量」的**。 > > **有效期 = 10 分钟**(实测至少 5 分 45 秒内均可用,超时后返回 `errorID=2`)。 > 所以报错时**不要怀疑「用过了」**,重新生成一个即可。 --- ## 七、时间戳处理细节 | 时间戳位置 | 时区 | 格式 | 说明 | |------------|------|------|------| | 二维码内 (偏移8-19) | **UTC+9 (日本时间)** | yymmddHHMMss | 微信侧生成,约 10 分钟有效期 | | 请求体 `timestamp` | **本机本地时间** | yymmddHHMMss | 实测 UTC+8 与 DLL 行为一致,服务端不强校验时区 | | 响应 `timestamp` | 服务端时间 | yymmddHHMMss | 仅作参考 | > **注意**:本机时区非 UTC+8 时,请求体 timestamp 仍用本地时间,但需确保系统时间准确(NTP 同步)。 --- ## 八、探活接口(可选,纯 Python 可独立实现) ### 8.1 探活请求 ```python def check_alive(host: str = "ai.sys-allnet.cn") -> bool: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE conn = http.client.HTTPSConnection(host, 443, context=ctx, timeout=10) try: conn.request("POST", "/wc_aime/api/alive_check", body=b"{}", headers={ "Content-Type": "application/json", # 探活需加 Content-Type "Connection": "Keep-Alive", }) resp = conn.getresponse() return resp.status == 200 and resp.read().decode().strip() == "alive" finally: conn.close() ``` ### 8.2 行为差异 | 端点 | Content-Type | User-Agent | 响应 | |------|--------------|------------|------| | `/wc_aime/api/alive_check` | `application/json` | 无 | `200 alive` | | `/qrcode/api/alive_check` | 无 | `WC_AIME_LIB` | `200 alive` | | `/wc_aime/api/get_data` | **无** | `WC_AIME_LIB` | 业务响应 | > DLL 实测:`CCommServAliveCheck` 探活阶段**不发真实网络请求**(WinHTTP hook 不触发),纯 Python 可直接调用 alive_check 验证服务器存活。 --- ## 九、原生 DLL 接口签名(供 P/Invoke 参考) ### 9.1 CCommServAliveCheck 系列 ```c // 创建探活会话 HANDLE CCommServAliveCheck_Create(); // 设置连接权限 void CCommServAliveCheck_setConnectPermission(HANDLE h, BOOL permit); // 开始探活 void CCommServAliveCheck_startCheck(HANDLE h); // 执行轮询 void CCommServAliveCheck_execute(HANDLE h); // 是否忙碌 BOOL CCommServAliveCheck_isBusy(HANDLE h); // 获取存活服务器索引 int CCommServAliveCheck_getIndex(HANDLE h); // 是否存活 BOOL CCommServAliveCheck_isAlive(HANDLE h); // 销毁 void CCommServAliveCheck_Destroy(HANDLE h); ``` ### 9.2 CCommGetUserData 系列 ```c // 创建登录会话 // strGameID="MAID", strChipID=KeychipID, strCommonKey="", strQRData=二维码[20:], strTitleKey="SDGB", serverURLIndex=探活返回索引 HANDLE CCommGetUserData_Create( LPCSTR strGameID, LPCSTR strChipID, LPCSTR strCommonKey, LPCSTR strQRData, LPCSTR strTitleKey, int serverURLIndex ); // 执行请求 void CCommGetUserData_execute(HANDLE h); // 是否结束 BOOL CCommGetUserData_isEnd(HANDLE h); // 获取错误码 (0=成功) int CCommGetUserData_getErrorID(HANDLE h); // 获取 userID (uint) unsigned int CCommGetUserData_getUserID(HANDLE h); // 获取 token (ANSI 字符串) LPCSTR CCommGetUserData_getToken(HANDLE h); // 销毁 void CCommGetUserData_Destroy(HANDLE h); ``` ### 9.3 托管层调用序列(ChimeUnit.Execute 状态机) ```csharp // 1. 探活 var alive = CCommServAliveCheck_Create(); CCommServAliveCheck_setConnectPermission(alive, true); CCommServAliveCheck_startCheck(alive); while (CCommServAliveCheck_isBusy(alive)) CCommServAliveCheck_execute(alive); int urlIndex = CCommServAliveCheck_getIndex(alive); bool isAlive = CCommServAliveCheck_isAlive(alive); CCommServAliveCheck_Destroy(alive); // 2. 登录 var login = CCommGetUserData_Create( "MAID", // gameID keychipId, // chipID (如 XXXX-XXXXXXXXXXX) "", // commonKey (托管层传空,原生层内部处理) qrString.Substring(20), // qrData (64位载荷) "SDGB", // titleKey urlIndex // 探活返回的服务器索引 ); while (!CCommGetUserData_isEnd(login)) CCommGetUserData_execute(login); if (CCommGetUserData_getErrorID(login) == 0) { uint userId = CCommGetUserData_getUserID(login); string token = CCommGetUserData_getToken(login); // PtrToStringAnsi } CCommGetUserData_Destroy(login); ``` --- ## 十、常量汇总表 ```python # ===== 签名与密钥 ===== SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" # 含前导 X,32字符 # ===== 默认机台标识 ===== DEFAULT_CHIP_ID = "XXXX-XXXXXXXXXXX" # ===== 服务器 ===== PRIMARY_HOST = "ai.sys-all.cn" BACKUP_HOST = "ai.sys-allnet.cn" PORT = 443 LOGIN_PATH = "/wc_aime/api/get_data" ALIVE_PATH = "/wc_aime/api/alive_check" # ===== 固定请求字段 ===== OPEN_GAME_ID = "MAID" TITLE_KEY = "SDGB" USER_AGENT = "WC_AIME_LIB" # ===== 二维码常量 ===== QR_PREFIX = "SGWC" QR_GAME_ID = "MAID" QR_PREFIX_LEN = 20 # SGWC(4) + MAID(4) + timestamp(12) QR_PAYLOAD_LEN = 64 QR_TOTAL_LEN = 84 # ===== HTTP 头(登录接口) ===== LOGIN_HEADERS = { "Connection": "Keep-Alive", "User-Agent": USER_AGENT, # "Content-Type": "application/json", # ★ 绝不加这一行 } ``` --- ## 十一、抓包调试方法论 ### 11.1 强制走代理 ```csharp // Hook WinHttpOpen // dwAccessType = 3 (WINHTTP_ACCESS_TYPE_NAMED_PROXY) // pwszProxy = "127.0.0.1:8890" ``` ### 11.2 mitmproxy CA 证书安装 ```powershell # 必须用 X509Store 静默导入,Import-Certificate 会因 UI 限制失败 $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser") $store.Open("ReadWrite") $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("mitmproxy-ca-cert.pem") $store.Add($cert) $store.Close() ``` ### 11.3 落盘与读取 ```bash mitmdump -w flow.mitm # Python 读取 from mitmproxy.io import FlowReader with open("flow.mitm", "rb") as f: for flow in FlowReader(f).stream(): print(flow.request.pretty_url) print(flow.request.content) print(flow.response.content) ``` ### 11.4 捕获签名输入 ```csharp // Hook CryptHashData 导出入口 // 捕获 pbData 指针指向的字节数组 → 即签名原文 // 对比:chipID + timestamp + KEY ``` ### 11.5 常见 Hook 坑点 | 坑点 | 原因 | 解决 | |------|------|------| | IAT hook 全失效 | pefile `imp.address` 已是完整 VA,再加 ImageBase 导致地址错误 | 直接用 `imp.address` + ASLR_delta | | IAT 写入后还原 | CrackProof 驱动写保护页面 | 改用全局 inline hook:改写 advapi32 导出函数入口 + capstone 精确 trampoline | | x64 trampoline 崩 | 指令切碎不完整 | capstone 逐条解析,累计 >=14 字节再跳转 | | 误判加壳 | Hook 全失效以为加壳 | 实测:.text 熵 6.2、465 个 FF 15、入口点可正常反汇编 → **未加壳** | --- ## 十二、实测验证记录 | # | 二维码时间戳 | userID | token (20位) | 备注 | |---|--------------|--------|--------------|------| | 1 | 260925205225 | 12345678 | TESTTOKEN0000000000 | DLL 验证成功 | | 2 | 260930235204 | 12345678 | e1Tsql1aP1SHLUthRdYI | | | 3 | 261001001304 | 12345678 | rI48n34Ttne1qR9FDFnY | 同一时间戳多次请求 token 各异 | | 4 | 261001001304 | 12345678 | znoWwYW1bMpwlNUzcTpX | | | 5 | 261001001304 | 12345678 | 8Cp6IztEGvLlYt5SRjxw | | **结论**: - userID 固定(同一账号) - token 每次请求生成新的 20 位临时凭据(一次性) - 探活+登录总耗时 ~800ms(网络延迟主导) --- ## 十三、最小复刻检查清单 实现纯网络扫码登录,必须覆盖: - [ ] 二维码输入归一化(URL → 84字符串) - [ ] 格式校验(长度84、前缀SGWC、GameID=MAID、载荷64位hex) - [ ] 载荷提取(`qr_string[20:]`) - [ ] 时间戳生成(本地时间 `%y%m%d%H%M%S`) - [ ] 签名计算(SHA256(chipID+timestamp+KEY) 大写hex) - [ ] 请求体构造(紧凑JSON,字段顺序固定) - [ ] HTTPS POST(无Content-Type,UA=WC_AIME_LIB,Keep-Alive) - [ ] SSL 不验证证书 - [ ] 响应解析(errorID=0、userID>0、token非空) - [ ] 错误处理(errorID=57→请求头问题,errorID=2→二维码过期) --- *本文档仅包含技术协议实现细节,不含项目结构、代码规范、文件路径等工程化内容。*