调查日期: 2026-09-23 调查起因: RSA 逆向定论(见
rsa_investigation.md)后, 追问"密钥是否由盒子传入游戏" 方法: env.json 落盘文件实测 + Lua 解密源码双向验证 (launcher + fgcq 两模块) 结论: 传递机制与 RSA stub 无关, 但完整还原了盒→游的两条数据通道
996盒子启动传奇游戏时, 通过 env.json 文件注入(选服进游戏时一次性落盘)和
GN/NG 运行时桥(回调式)两条通道, 把账号凭证/签名密钥/渠道信息传递给游戏模块;
游戏侧凭 token+sdata 构造 CS20 登录握手串进服。全程无 RSA 参与,
敏感字段靠"沙盒隔离 + HMAC-SHA1 请求签名"保护(信任壳层架构)。
用户在启动器选服点"进入游戏"后:
壳层(ObjC) 发 GameList 选择
→ Lua: NativeBridgeManager:NG_SelectGameAndRestart(sender, envString) (NativeBridgeManager_launcher.lua:370)
→ ModuleChooseLayer:WriteEnvAndRestart(envStr) (ModuleChooseLayer_launcher.lua:3157)
→ WriteEnv(envStr) (:3119)
→ 落盘 <writablePath>/mod_launcher/env.json
→ GameEnvManager:RestartGame() ★ 重启 Lua 虚拟机, 切换到 mod_fgcq
envPath = writePath .. getNewFoldername("mod_launcher") .. "/env.json"
if fileutil:isFileExist(envPath) then
fileutil:removeFile(envPath) -- 先删旧
-- 然后写入新 envString
跨模块传递的介质就是这份明文 JSON 文件; fgcq 游戏模块启动后从同路径读回。
| 字段 | 实测值(脱敏) | 用途 |
|---|---|---|
uid |
2001849239 | 盒子账号 ID |
token |
166a68a8...(42 位 hex) |
游戏登录票据(SDK 登录凭证) |
new_boxtoken |
eyJ0eXAiOiJKV1...(JWT, HS256) | 盒子新版令牌 |
gameAccount |
z1098210167 | 游戏账号名 |
signkey |
47f67f30...(32 位 hex) |
SDK 请求 HMAC-SHA1 签名密钥 |
u_key |
QuM2001849239itC | 用户密钥(账号绑定) |
channel |
110585 | 渠道号(握手串用) |
gameid / gameId |
206923 | 游戏 ID |
modlist |
https://list-box.dhsf.xqhuyu.com/modlist/modlist_110585.txt?time=...&sign=... | 模块清单 URL(带 sign 防篡改) |
gameListUrl |
https://h5qfdt.996box.com/ | 游戏列表 H5 |
sdkBaseUrl |
https://api.tj.996sdk.com | SDK 后端 |
cacheName |
cache_206923 | 缓存目录名 |
boxVersion |
5.1.1.9007 | 盒子版本 |
sdkChannel |
996box_mac | SDK 渠道标识 |
platform |
mac | 平台 |
isBoxLogin |
1 | 盒子登录态 |
isPortrait / resolution |
0 / (空) | 屏幕方向/分辨率 |
loadingAuthor |
推广商:玖扬网络 | 加载页署名 |
scene |
0 | 场景标记 |
boxConfig |
1 | 盒子配置开关 |
macAddress |
FCE92589-...(UUID) | 设备指纹 |
gm |
0 | GM 标志 |
signToken / serverId / resolution |
(空) | 预留 |
对照: .app 包内自带的
env.json(首次启动模板)还含sdkAppid/sdkAppkey、is_adult/is_realname、JWT 格式 token —— 结构同源, 字段随渠道配置增减。
| .app 包内(模板) | Documents 落盘(真机) | |
|---|---|---|
| token | JWT (eyJ0eXAi...) | 42 位 hex 票据 |
| 渠道 | 37504 | 110585 |
| 特有字段 | sdkAppid/sdkAppkey, is_adult, is_realname | u_key, gameAccount, signkey, sdkBaseUrl |
| 生成方 | 打包时固化 | 壳层登录+选服后动态写入 |
Lua → 壳: NativeBridgeCtl:Inst():sendMessage2Native("GN_方法名", jsonString)
壳 → Lua: nativeBridgeCtl:addNativeSelector("组名", "NG_方法", handler)
| 组名 | NG_ 回调 | 传递的数据 |
|---|---|---|
| recvDataForInit | NG_recvData | 壳层数据下发(onRecvDataCB) |
| SDKLoginResponse | NG_SDKLoginCB | 登录凭证(见 2.3) |
| AccountOperator | NG_Logout | 登出通知 |
| OverlayInstallation | NG_OverlayInstallation | 覆盖安装 |
| WifiMacRecord | NG_WifiMacRecv | MAC 地址 |
| NativeStatus | NG_NativeStatusChanged | 壳层状态 |
| DeviceRotation | NG_DeviceRotationChanged | 旋转 |
| AllowLaunchGame | NG_AllowLaunchGame | {isAllowLaunch} 启动许可 |
| RealNameInfo | NG_RealNameInfo | {RealNameStatus} 实名状态 |
| SuspectInfo | NG_SuspectInfo | 可疑行为(防沉迷) |
| HeartInfo | NG_HeartInfo / NG_AppInfo | 心跳 / App 信息 |
| LastServerInfo | NG_LastServerInfo | 上次选服 |
| SDKHasBuff | NG_HasBuff | SDK buff |
| GameList | NG_SelectGameAndRestart | envString → env.json 落盘(即通道①的触发源) |
{
"state": 1, // 1 成功 / 0 失败
"userName": "...", // 账号名
"sessionId": "...", // 会话票(赋给 _password)
"sdata": "...", // ★ 游戏服务器票据 → GameEnvManager:SetSData()
...其余字段逐个 SetLoginDataByKey(k, v)
}
fgcq 的 LoginProxy.lua:1056-1087 把两通道数据拼成 Mir 风格握手串:
-- // *#账号/角色名字/角色ID/parma4/920080512/channelid/whitelist
sendStr = string.format("*#%s/%s/%s/%d/%s/0/%s/%s/%s/%s/%s/%s",
account, -- ← env.gameAccount
self:GetSelectedRoleName(),
self:GetSelectedRoleID(),
netClient:GetNetMessageTypeSData(), -- ← NG_SDKLoginCB 的 sdata 票据序号
platform, -- android:0 / ios:1 / 其他:2 (mac=2)
envProxy:GetChannelID(), -- ← env.channel (110585)
whitelist, cert, adult, -- ← NG_RealNameInfo / NG_SuspectInfo
isboxlogin, -- ← env.isBoxLogin
isBatchMsg) -- ← LuaBridgeCtl:GetModulesSwitch
与《996传奇客户端框架结构与调用链.md》§13.1 的三段连接模型吻合: 此握手串是第三段(游戏服)连接的首包。
AuthProxy.lua (fgcq) 优先用 env 注入的 signkey, 无则回退硬编码:
-- AuthProxy.lua:14 (默认值, 仅兜底)
local signkey = "634eff98723b31da43ed35f0dd4edf36"
-- AuthProxy.lua:269-274 (优先 env.json 注入值)
local tmp = global.L_GameEnvManager:GetSignkey()
if tmp and string.len(tmp) > 0 then
signkey = tmp
else
releasePrint("can't find signkey by env.json, use default")
end
-- AuthProxy.lua:142 (消费: HMAC-SHA1 请求签名)
local sign = sha1.hmac(signkey, origin)
其他消费点: GetCustomDataByKey("sdkPay"/"XZPay") 决定支付通道;
GetEnvDataByKey("juhe_*"/"original_user_id") 注入聚合支付参数。
顺带发现的相关硬编码(同一文件, 支付/聚合渠道):
juhe_secret_key = "ptONCD6UxNbQRxYrvdVpFACgL1Ir53rjfUO0lOM2Did0ZxGEg2WB0WfhtsqTT2ee"pay-sdkv2.dhsf.xqhuyu.com / pcjuhe.dhsf.xqhuyu.com / xdwqq.dhsf.xqhuyu.com| 问题 | 答案 |
|---|---|
| RSA 密钥是不是盒子传进来的? | 不是。RsaEncryptPub/RsaDecryptPri 是引擎通用桥接接口, 客户端实现为空壳(返回空串), 证据链见 rsa_investigation.md |
| 那 token/signkey 怎么保护? | 信任壳层架构: 盒子 App 与游戏主二进制同进程同沙盒, env.json 落盘沙盒内; 出沙盒的 HTTP 请求靠 HMAC-SHA1(signkey) 签名防重放/防篡改 (AuthProxy.lua:142) |
| 端侧为何无加密? | 传奇类"信任客户端壳、防服务器外敌"的经典设计; 破解者拿走 env.json 也只能顶替该账号(有设备指纹 macAddress + JWT 时效约束), 无需端侧加密 |
本机制与 protocol_bot/login_996pc.py 的纯 HTTP 登录互为印证:
| 环节 | 真机客户端 | protocol_bot |
|---|---|---|
| 账号登录 | 壳层 SDK (极验滑块) | 极验滑块自动破解 ✓ |
| 拿 token | env.json 注入 | HTTP 响应直取 ✓ |
| 选服 | NG_LastServerInfo/GameList | serverlist txt 直读 ✓ |
| 进游戏 | env.json 重启 + CS20 握手串 | CS20 握手串直发 ✓ |
| 请求签名 | HMAC-SHA1(signkey) | 同算法(如需调 SDK 接口) |
即: bot 已复刻通道①②的全部有效字段, 未依赖任何未还原的壳层能力。
文件(源码相对 lua脚本解密源码/mod_launcher/stab/scripts/) |
作用 |
|---|---|
logic/NativeBridgeManager_launcher.lua:370 |
NG_SelectGameAndRestart 入口 |
logic/NativeBridgeManager_launcher.lua:393-432 |
NG_ 桥注册表(13 组) |
logic/NativeBridgeManager_launcher.lua:218-243 |
NG_SDKLoginCB 字段解析 |
logic/ModuleChooseLayer_launcher.lua:3119 |
WriteEnv 落盘 env.json |
logic/ModuleChooseLayer_launcher.lua:3157 |
WriteEnvAndRestart |
../mod_fgcq/stab/scripts/game/proxy/remote/LoginProxy.lua:1056 |
CS20 握手串构造 |
../mod_fgcq/stab/scripts/game/proxy/remote/AuthProxy.lua:142,269 |
signkey 签名消费 |