美国司法部上周公开的一份刑事起诉书,把一个持续两年的操作摆到了台面上:佛罗里达 21 岁的 Zyaire Wilkins(线上ID Sibel.eth)被指控在 Steam 上发布带恶意代码的游戏,包括 BlockBlasters、Dashverse、Lampy、Lunara、PirateFi。按起诉书的数字,这批游戏感染了约 8,000 名玩家,攻击者从中攻破了约 80 个加密货币钱包,至少盗走 22 万美元。

资金追踪的过程很有画面感:FBI 顺着一个涉事钱包的资金流,查到用赃款购买的多张礼品卡(包括 Uber Eats),传票拿到配送记录,锁定收件人,然后搜查住宅,缴获 MacBook、手机和数字钱包。受害者的起点,就只是在 Steam 上装了一个免费小游戏。

我想写这篇不是因为案件本身,而是因为它把应用商店这条分发通道的信任模型暴露得很干净:游戏是能玩的,商店审核是过的,杀软的签名策略是空的,而更新通道天然就是一条后门通道。

为什么是游戏平台

选 Steam 而不是钓鱼邮件,攻守两端的账都算得过来:

  • 审核的边界在哪里。商店审核看的是页面、截图、可执行文件的初版行为,它不具备"持续审计"能力。一个先上架干净版本、几个版本之后再投毒的游戏,审核环节根本不会再看第二眼。
  • 可玩性是免死金牌。一个能正常启动、有菜单、有存档的"游戏",玩家没有理由怀疑它;反过来说,如果它一启动就报错,用户会卸载,恶意代码连运行的机会都没有。"能玩"既是伪装也是投递机制。
  • 更新即投递。Steam 的 depot 更新会在客户端静默下载并替换文件,用户不会对"这款游戏又更新了"产生任何警觉。这个机制和正规软件的自动更新没有区别,所以也就没有额外的检测点。

推广渠道也写在起诉书里:Discord、LinkedIn、TG。伪装成独立游戏社群、招测试玩家、找人"试玩反馈"——这是最有效的一类社会工程,因为目标用户自认为是在帮人,不是在点击可疑链接。

技术侧:外壳 + 载荷的混合体

这类样本(PirateFi 是公开报道里最典型的一个,去年被 Valve 下架时已有约 1,500 名下载记录)的结构高度一致,可以拆成四层:

第一层,游戏外壳。 Unity 或 Electron 打包的正常可执行文件,体积、图标、资源文件都符合一款小游戏的特征。它存在的唯一任务是让用户相信这是一款正常软件,并让杀软看不出静态异常。

第二层,载荷释放。 首次运行(或第 N 次运行,规避沙箱超时)后,外壳从内嵌资源里释放真正的载荷,落点通常在 %APPDATA% 或 %LOCALAPPDATA% 下的自建目录,配合 Run 注册表键或计划任务做持久化。也有样本走 DLL sideloading:把恶意 DLL 放到游戏目录,靠系统加载顺序劫持一个合法签名程序的导入。

第三层,收集。 目标是两样东西:浏览器里的凭据和磁盘上的钱包文件。

%LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data
%LOCALAPPDATA%\Google\Chrome\User Data\Local State     # DPAPI 保护的解密密钥
%APPDATA%\Mozilla\Firefox\Profiles\<随机>\logins.json
%APPDATA%\Ethereum\keystore
%APPDATA%\Exodus\exodus.wallet
%APPDATA%\Electrum\wallets
%APPDATA%\Ledger Live          # 注意:这里只有配置与缓存,助记词在硬件设备里
%APPDATA%\Bitcoin\wallet.dat

顺带一个容易被忽略的模块:剪贴板替换(clipper)。它不偷文件,只在用户复制钱包地址的瞬间把目标地址换成攻击者的。对很多用户来说这比偷 keystore 更致命,因为交易是不可逆的,而用户会盯着"我明明粘贴的是这个地址"反复怀疑自己的操作。如果用户把助记词存成截图,样本里的 OCR 模块也能把它读出来。

第四层,外带。 这一点值得记下来:很多样本不用自建 C2,而是走 TG Bot API、Discord webhook 或者干脆一个普通的 HTTP POST。从网络设备的角度看,这些流量指向的都是被大量正常业务使用的合法 SaaS 域名,域名信誉库给不了任何信号。

按起诉书的数据做个粗算:8,000 台感染里只有 80 个钱包被掏(约 1%),平均每个约 2,750 美元。命中率低是因为大部分玩家手里没有加密资产,但攻击者要的从来不是转化率,而是规模。只要分母足够大,1% 就够回本。

检测:从钱包目录倒推

对个人和终端管理员来说,最有效的排查角度不是"找恶意进程",而是看谁动过钱包目录。正常程序不会去写这些路径:

# 1) 钱包/凭据目录近期被谁改写
$paths = @("$env:APPDATA\Ethereum","$env:APPDATA\Exodus","$env:APPDATA\Electrum",
           "$env:APPDATA\Bitcoin","$env:LOCALAPPDATA\Google\Chrome\User Data\Default")
Get-ChildItem $paths -Recurse -File -ErrorAction SilentlyContinue |
  Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-14) } |
  Sort-Object LastWriteTime -Descending | Select-Object LastWriteTime, FullName -First 30

# 2) 游戏目录里有没有不该出现的可执行文件/脚本
Get-ChildItem "$env:ProgramFiles(x86)\Steam\steamapps\common\*\*" -Include *.exe,*.dll,*.ps1,*.vbs `
  -Recurse -ErrorAction SilentlyContinue |
  Get-AuthenticodeSignature | Where-Object { $_.Status -ne 'Valid' } |
  Select-Object Status, Path

# 3) 从游戏目录拉起的子进程
Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like "*steamapps*" -and
  $_.Name -match 'powershell|cmd|wscript|mshta|rundll32' } | Select-Object Name, ExecutablePath, CommandLine

有 Sysmon 的话再加三个事件就基本闭环:EventID 11(文件创建,盯钱包路径)、EventID 22(DNS 查询,盯 api.TG.org、discord.com/api/webhooks 这类非游戏流量)、EventID 1(进程创建,父进程是游戏可执行文件)。这三条规则的价值在于误报极低——正常的游戏不会去碰 keystore。

另一个便宜的动作:Steam 客户端里对每个可疑游戏做"验证文件完整性",本地有额外文件不会报错,但和商店当前 depots 比对能看出端倪。只是别忘了,验证通过也不代表干净——如果游戏还在库里,下次更新可能又把载荷送回来。

我的做法

游戏机和持有资产的环境,我从一开始就物理隔离,三条规矩:

  1. 热钱包不上游戏机。日常打游戏的那台机器上不装任何钱包客户端,不看助记词,不必要的浏览器扩展全删。持有资产用一个独立的系统用户或干脆另一台机器,浏览器 profile 也不共用。
  2. 地址一律走硬件钱包。收款地址在设备屏幕上核对,剪贴板被替换的套路就断在这里;助记词只存在于纸质备份,不截图、不存网盘。
  3. Steam 库做定期体检。装过的游戏里凡是短期下架的、社群反馈奇怪的、更新后杀软报过一次的都清掉,不留"也许还能玩"的侥幸。

这件事没有技术上的优雅解法。应用商店的审核、代码签名、杀软信誉,在这条链上全都不是控制点;真正能兜底的只有两件事:资产和运行环境分离,以及转账前在另一块屏幕上确认地址。前者防偷,后者防换。

⚠️ 网络安全免责声明

本文内容仅供技术研究与安全学习之用。文中描述的漏洞分析、攻击链拆解和代码示例均基于公开披露的安全研究,旨在帮助开发者理解攻击原理、提升安全意识和防御能力。

请勿将本文所述技术用于任何未经授权的安全测试、渗透攻击或非法用途。任何因滥用本文信息而造成的法律后果,由使用者自行承担,与本文作者无关。

如发现系统安全漏洞,请通过合法渠道向相关厂商或平台报告,共同维护网络安全生态。