docs: WaHappy 开发文档
孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
# 舞萌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→二维码过期)
|
||||
|
||||
---
|
||||
|
||||
*本文档仅包含技术协议实现细节,不含项目结构、代码规范、文件路径等工程化内容。*
|
||||
Reference in New Issue
Block a user