
如何伪造 AppTicket 解锁 Steam 游戏OpenSteamTool 利用 SteamDRMP off-by-four 漏洞原理深度剖析【免费下载链接】OpenSteamToolOpen Source Steam Unlocker项目地址: https://gitcode.com/gh_mirrors/op/OpenSteamTool想知道 OpenSteamTool 如何让没买过的游戏也能正常启动吗这个开源 Steam 解锁工具的核心武器是一个被称为SteamDRMP off-by-four的本地票据解析漏洞——它允许 OpenSteamTool 在不注入游戏进程的情况下伪造 AppTicket绕过 DRM 授权。本文从零讲起AppTicket 是什么、漏洞为什么叫off-by-four、伪造票据的每一字节如何拼接以及 Denuvo 与 SteamStub 两条路径各自需要什么。读完你会对 Steam 客户端 DRM 体系有一个相当完整的认识。1. AppTicket 是什么为什么它是 DRM 的命门Steam 的 DRM 校验逻辑大致是游戏或其 DRM 模块如 SteamDRMP、Denuvo向 Steam 客户端发起 IPC 请求GetAppOwnershipTicket客户端返回一张AppTicket——一张由 Valve 服务器私钥签名的所有权凭证DRM 层用公开的 Valve 公钥验证签名再从中解析出AppId和SteamID三者都对得上 → 授权通过游戏运行。从票据结构上就能看出关键信息的位置见 AppTicket.h偏移 8 → SteamID 偏移 16 → AppId 票据末尾 128 字节 → 签名kAppTicketSignatureSize 128所以只要你能让 DRM 层读到一个 AppId 是目标游戏的票据且签名校验通过就等价于拥有了该游戏。难点在于签名由 Valve 私钥生成无法凭空捏造。OpenSteamTool 的破局思路是不造新票据只把一张真实票据偷梁换柱。2. 漏洞原理为什么off-by-four能让伪造签名通过漏洞位于 SteamDRMP 解析票据的流程中。正常流程应该是① 读取整张票据 → ② 用 Valve 公钥验证签名签名只覆盖票据前 N 字节→ ③ 签名合法后再按偏移提取 AppId、SteamID → ④ 校验 AppId 是否等于请求方 AppId。而 SteamDRMP 的实际实现是读取票据的签名前正文用公钥验签验签通过认为票据可信直接按固定偏移读取正文里的 AppId与游戏自身的 AppId 比对比对成功 → 授权通过。漏洞在于第 3 步它信任的票据里的 AppId和它校验的请求 AppId其实可以被中间人篡改——只要签名校验时读的范围与提取 AppId 时读的范围不一致且这个不一致恰好是4 字节一个 AppId 的大小。这就是 off-by-four验签时验证器读到签名起点为止的字节提取 AppId 时却从另一个偏移读且多读了 4 字节——这 4 字节原本在签名覆盖范围之外但被当作 AppId 来用。换言之签名只保护到 A 处但 DRM 却从 A4 处读 AppId。这 4 字节的信任盲区正好塞进任意一个我们想伪造的 AppId。3. OpenSteamTool 的伪造流程三步拼接法理解了原理看代码就一目了然。核心实现在 ForgeLocalAppOwnershipTicket第 1 步取一张可信源票据// src/Utils/Tickets/AppTicket.cpp std::vectoruint8_t source Hooks_Decryption::GetCacheAppOwnershipTicket(kLocalAppTicketSourceAppId);这里kLocalAppTicketSourceAppId 7。AppId 7 是一张 Steam 客户端本地必然存在的缓存票据通过 hookConfigStore::GetBinary从apptickets\7读取见 GetCacheAppOwnershipTicket。它签名真实、结构完整是完美的空白画布。第 2 步精确截断——切掉原签名只保留签名覆盖的正文const size_t signedSize source.size() - kAppTicketSignatureSize; ticket.insert(ticket.end(), source.begin(), source.begin() signedSize);第 3 步在签名与正文之间塞4 字节目标 AppIdticket.insert(ticket.end(), appIdBytes, appIdBytes sizeof(AppId_t)); // ← 关键 4 字节 ticket.insert(ticket.end(), source.begin() signedSize, source.end());最终伪造出的票据布局┌──────────────────────────────────┬────────┬──────────────────┐ │ 源票据正文签名真实覆盖 │ 目标 │ 源票据签名 │ │ SteamID / 原 AppId7 / ... │ AppId │ 128 字节 │ └──────────────────────────────────┴────────┴──────────────────┘ ↑ off-by-fourDRM 从签名后 4 字节处读 AppId再看响应侧如何骗过解析器——GetAppOwnershipTicket 中精心计算的四个指针ticket.totalSize ticket.data.size() - sizeof(AppId_t); // 故意少报 4 ticket.appIdOffset ticket.totalSize - kAppTicketSignatureSize; // 指向插入的 AppId ticket.signatureOffset ticket.appIdOffset sizeof(AppId_t); // 跳过 4 字节才是签名 ticket.signatureSize kAppTicketSignatureSize;注意totalSize故意少报 4 字节、signatureOffset故意后移 4 字节——这两个错位正是漏洞的利用点DRM 验签时按totalSize读范围不包含我们插入的 4 字节但提取 AppId 时却按appIdOffset读正好读到我们插入的目标 AppId。签名合法 AppId 匹配授权通过。最后这张票据通过 hook 的 IPC 后处理函数 HandlerPost_IClientUser_GetAppOwnershipTicketExtendedData 注入回 Steam 客户端的响应缓冲游戏完全无感。4. 三种票据来源与 Denuvo 授权窗口并非所有游戏都吃伪造这一套。OpenSteamTool 在 AppTicketSource 中定义了三级策略模式适用场景说明CredentialStoreOnlyDenuvo 游戏授权窗口内只读显式票据/缓存票据不伪造ForgeOnlySteamStub 游戏无显式票据时直接伪造CredentialStoreThenForge通用回退先读缓存读不到再伪造为什么 Denuvo 不能伪造因为 Denuvo 的授权是一次窗口期行为游戏进程通过命名管道与 Steam 客户端握手DenuvoAuth 状态机在检测到 Denuvo 特征后选定一个授权管道只在该管道、且授权未结束时IsAuthorizedPipe才放行 SteamID 与票据欺骗。一旦握手结束第 2 次握手后窗口关闭。而 SteamStub 游戏走的是本地 ConfigStore 票据路径全程离线可伪造这也是为什么 README 说SteamStub-only games do not require configuring AppTicket。Denuvo 用户怎么办用项目自带的 extract_tickets 工具在一台真实拥有该游戏的机器上运行它会从运行中的 Steam 客户端 dump 出appticket.bin/eticket.bin并输出 hex 字符串然后写入 Lua 配置setAppTicket(1361510, 14000000...) -- 写入 AppTicket setETicket(1361510, ...) -- 写入 ETicket注意 Denuvo 票据有30 分钟有效期过期后会报88500005需要刷新。5. 票据从哪里读、写到哪里Windows 上票据存储在注册表HKCU\Software\Valve\Steam\Apps\AppId下由 SteamCredentialStore 统一封装。OpenSteamTool 的读写接口读GetAppOwnershipTicketFromCredentialStore(appId)— 仅当 appId 在addappid白名单内才返回写WriteAppOwnershipTicket/WriteEncryptedTicket/WriteSteamID— 把 Lua 配置里的 hex 票据落盘。还有一个巧妙的细节在 GetSpoofSteamID伪造票据里的 SteamID 必须与 Steam 本地期望的一致否则授权链路会断。所以工具优先读缓存 SteamID读不到就直接从票据偏移 8 处解析——让伪造的票据里写着 Steam 自己相信的那个 ID。6. 快速上手从零到解锁构建项目根目录运行build.bat需 Windows CMake 3.20 VS2022将dwmapi.dll、xinput1_4.dll、OpenSteamTool.dll复制到 Steam 根目录steam.exe同级在Steam\config\lua\下创建 Lua 脚本addappid(你的AppId)即可解锁SteamStub 游戏到此结束Denuvo 游戏则需补setAppTicketsetETicket。配置样例见 opensteamtool.example.toml。7. 技术要点回顾off-by-four 本质验签范围与 AppId 提取范围错位 4 字节形成信任盲区伪造 ≠ 生成全程不伪造签名只移植一张真实票据的签名AppId 7 是钥匙本地必然存在的缓存票据提供结构完整、签名真实的源数据指针操纵是核心totalSize少报 4、signatureOffset后移 4恰好对应漏洞的两个错位点两条 DRM 路径SteamStub 走本地 ConfigStore可伪造Denuvo 走管道授权窗口需显式票据。8. 声明与延伸阅读⚠️ 本项目仅供研究与教育目的。请遵守当地法律、平台服务条款与软件许可。相关源码入口伪造核心src/Utils/Tickets/AppTicket.cppIPC 注入点src/Hook/Hooks_IPC_ISteamUser.cppConfigStore Hooksrc/Hook/Hooks_Decryption.cppDenuvo 授权状态机src/Pipe/Features/DenuvoAuth/DenuvoAuth.cpp票据提取工具tools/extract_tickets/extract_tickets.cpp完整文档README.md / README_ZH.md【免费下载链接】OpenSteamToolOpen Source Steam Unlocker项目地址: https://gitcode.com/gh_mirrors/op/OpenSteamTool创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考