孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
20 KiB
20 KiB
舞萌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())
def is_valid(qr: str) -> bool:
return len(qr) == 84 and qr[0:4] == "SGWC" and qr[4:8] == "MAID"
2.3 传给原生层的数据(EraseHashIdentifier())
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 字符 ────────────┘
提取规则:
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)
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 算法规范
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 最小可用代码
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):{"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 探活请求
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 系列
// 创建探活会话
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 系列
// 创建登录会话
// 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 状态机)
// 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);
十、常量汇总表
# ===== 签名与密钥 =====
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 强制走代理
// Hook WinHttpOpen
// dwAccessType = 3 (WINHTTP_ACCESS_TYPE_NAMED_PROXY)
// pwszProxy = "127.0.0.1:8890"
11.2 mitmproxy CA 证书安装
# 必须用 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 落盘与读取
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 捕获签名输入
// 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→二维码过期)
本文档仅包含技术协议实现细节,不含项目结构、代码规范、文件路径等工程化内容。