













一篇流水账式的实战记录:从"多开游戏"开始,一路挖到PASSport机制、平台判定、进程内反作弊,
所有测试在自有环境、自有账号上进行。请勿用于违反服务条款或法律法规的用途。
手头一个老游戏:跑跑卡丁车,靠天成的 TC 平台(TCGame.exe)启动。
想要双开游戏:以前有脸蛋N开跟PP多开,十年了早就失效了。打算自己动手做一个
结果挖到了三层东西:PASSport凭证 → 平台判定 → 进程内反作弊。下面按挖到的顺序写。
直接第二次双击 KartRider.exe:第二个进程会起来,但还没进登录就自己没了(自毁),
或者起来一小会儿就崩在 KartRider.exe+0x??? 的某个地址上(0xC0000005)。
也就是说:客户端有一套"我已经在运行了"的检测,检测到就自杀。
老办法是给它挂个 dinput8.dll 马甲(DLL 劫持,游戏必然会加载它),在客户端进程里做几件事:
| 手段 | 说明 |
|---|---|
FindWindowW/A / FindWindowExW/A 桩化 | 客户端会按窗口类名找"另一个自己",对外来进程的同名窗口返回 NULL |
| 共享对象句柄处理 | 把其它进程里持有的同名对象句柄关掉 |
| 进程列表侧的处理 | 把兄弟进程在 NtQuerySystemInformation(class=5) 的结果里"改名换姓" |
实测:三项一起上,第二个客户端能起来、能进大厅(两个 PopKart Client 窗口同时存在)。
游戏本身的客户端已经被禁止使用了。(这时候我还沉浸在一次性开了16个客户端,其实已经无法登录了。不高兴)

打开进程列表和模块列表,能看到:
BlackCipher.aes 进程 —— 两个客户端也只出现一个;NGClient.aes / BlackCall.aes 是加载在游戏进程里的模块(不是独立进程);CrashReporter.dll、GameToTCG.exe 套件。也就是三层叠着站:进程内模块 + 独立进程 + 内核侧交叉验证。
老套路:钩系统调用 + 顺栈回溯,把调用者的模块+偏移打出来。
static LONG NTAPI Hook_NtQuerySystemInformation(ULONG cls, PVOID buf, ULONG len, PULONG ret)
{
LONG st = orig(cls, buf, len, ret);
if (cls == 5 || cls == 64) // 进程列表 / 句柄表
LogCallerAddr("NtQSI", _ReturnAddress(), _AddressOfReturnAddress());
return st;
}LogCallerAddr :把返回地址映射成 模块+偏移;
再从返回地址往上扫 160 个 DWORD,找出落在"游戏侧模块"里
(自动跳过 KERNEL*/ntdll/USER32/msvcrt 和我们自己的劫持 DLL)。
扫出来极其干净:
[caller] NtQSI class=5 (process list) <- KERNEL32.DLL+0x60B32
stack: [KartRider.exe+0x0] [NGClient.aes+0xCC00E] [NGClient.aes+0x5BB334]
[NGClient.aes+0x4FB7] [NGClient.aes+0x11D00]
[caller] NtQSI class=5 (process list) <- KERNEL32.DLL+0x60B32
stack: [BlackCall.aes+0x166B10A] [BlackCall.aes+0x1A4E22D] [BlackCall.aes+0x5AABA]多开检测的是进程内的反作弊模块。
既然判定走进程枚举,第一直觉当然是"钩掉它、把结果改了"。实测结果:

结束提示
发现正在使用异常程序,与服务器断开连接。
TCGame.exe 判定的规则想搞明白这个,最直接的办法是注入一个记录 API 调用序列的 spy DLL。抓到的判定流程(简化):
EnumProcesses / NtQuerySystemInformation(class=5) ← 枚举进程
→ 匹配进程名 KartRider.exe
→ NtQueryInformationProcess(h, 43 /* ProcessImageFileNameWin32 */)
→ NtQueryInformationProcess(h, 27 /* ProcessImageFileName */)
→ CreateFile("\??\G:\TCGameApps\kart\KartRider.exe") ← 拿映像路径去核对NtQueryInformationProcess 返回的缓冲里 pid 在偏移 0。
我一开始按 ci+8 读,hook 打出来全是垃圾 pid,排查了很久。
本地这条判定线是能骗的(把进程在列表里改名,平台就检测不到了),但实测发现还有第二条线:
TCGame.exe、切换平台账号、隐藏窗口 —— 全都清不掉;本地进程扫描解释不了"换账号也不恢复",所以这条是服务端侧的。
这一点直接决定了:"靠清本地痕迹来实现多开"是死路。
TCGame.exe 是 CEF 界面:主窗口类名 TCMainFrame(标题 TCGames),登录窗口是 tcgLogin(标题 tcLauncher)。
CEF 内容不吃 PrintWindow(截出来是空白),只能屏幕截图;
置顶要 AttachThreadInput + SetWindowPos(HWND_TOP);按钮状态判定用区域裁剪截图(比找控件可靠)。
前面三节都在讲"多开被检测",这里讲登录——因为绕到后来你会发现:
客户端能不能进游戏,取决于它启动时有没有被塞一张 passport。
现象:直接双击 KartRider.exe → 出 Login 窗口 → 输账号密码点登录 → 毫无反应,随后弹"服务器错误"。

掏出抓包工具看它连了哪:
CONNECT -> 109.244.39.49:39311 ret=-1 (不通)
CONNECT -> 109.244.39.50:39311 ret=-1
CONNECT -> 162.14.144.162:39311 ret=0 (通)网络是通的,问题不在连接。答案在平台启动它时的命令行:
X:\游戏路径\KartRider.exe TGC -region:3 -passport:<108 字符 base64>客户端不是自己登录的,它是用了"passport"才登录的。 没有 -passport:,那个 Login 窗口就是个摆设。
TCGame.exe 向服务端申领,点击「启动游戏」按钮的是出来的;TCGame.exe 当"passport 发放机"既然 passport 是点「启动」时出的,那我们不需要平台去启动游戏,只需要它生成的一张 passport。
观察:平台启动游戏必然要调 CreateProcessW(或 A)。在平台进程里注入一个小 DLL 钩住它:
typedef BOOL (WINAPI *PFN_CPW)(LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES,
BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION);
static PFN_CPW o_cpw;
static BOOL WINAPI H_cpw(LPCWSTR app, LPWSTR cmd, ...)
{
if (cmd && wcsstr(cmd, L"KartRider")) {
WCHAR* p = wcsstr(cmd, L"-passport:");
if (p) SavePassport(p + 10); // 1) 把 passport 抄下来
if (FlagOn("X:\\_exp\\disp\\block.flag")) { // 2) 关键:拒绝这次启动
SetLastError(ERROR_ACCESS_DENIED);
return FALSE; // 不真的起客户端
}
}
return o_cpw(app, cmd, ...);
}抓到的日志:
[cpw#1] cmd=X:\TCGameApps\kart\KartRider.exe TGC -region:3 -passport:**bZuu…Ng7tg=**
[PASSPORT] caught from CreateProcessW
[BLOCK] refusing this launch 拒绝启动这一步是精髓,因为它带来一个很妙的状态:
KartRider clients: (空) ← 没有任何游戏会话
平台按钮 = 「启动游戏」 ← 可以反复点、随便切账号取 passport平台的按钮状态是跟着"有没有游戏在跑"走的:不真起客户端 → 平台认为没人玩游戏 → 按钮一直可点
→ 一台机器上可以给多个账号各取一张 passport。
于是流程变成:
起 TCGame.exe → 注入发放机 → 开 block → 点一次「启动按钮」→ passport 入池
→ 平台切到另一个账号 → 再点一次 → 第二张 passport 入池
passport 到手,客户端就是靠命令行登录的,跟平台进程毫无关系:
Start-Process -FilePath 'X:\TCGameApps\kart\KartRider.exe' `
-ArgumentList 'TGC','-region:3',("-passport:" + $pp) `
-WorkingDirectory 'X:\TCGameApps\kart'实测(把 TCGame.exe 完全关掉):

TCGame=0 ← 平台已退出
用抄到的 passport 直接启动 KartRider.exe
→ pid=47480 title='PopKart Client' 646MB
→ 连接 162.14.144.168:39311 / 162.14.144.174:39322 ← 真的上线了结论:KartRider.exe 自己就能当登录器,TCGame.exe 只在"取 passport 那 30 秒"需要开。
注意配合关系:passport 是按账号发的、且同账号只能一个在线会话。
所以"多账号 + 各自一张 passport + 各自一个客户端" = 合法的双开上限;
想要同账号多开,撞的是服务端会话规则,不是客户端。
TCGame.exe 和 KartRider.exe 都是 x86,且跑在提权上下文里 —— 中权限工具会直接 OpenProcess err=5。
要用 32 位的 PowerShell:C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe。
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID mem = VirtualAllocEx(h, NULL, len, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(h, mem, dllPath, len, NULL);
CreateRemoteThread(h, NULL, 0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA"),
mem, 0, NULL);| 序言 | 处理方式 |
|---|---|
8B FF 55 8B EC / 55 8B EC 83 EC 等标准函数头 | 复制 N 字节进 trampoline,回填 E9 rel32 |
FF 25 imm32(jmp dword ptr [addr] 转发桩) | 不改代码,直接改那个指针槽 |
A1 imm32 C3(mov eax,[global]; ret) | 直接改全局槽里的指针,最干净 |
第三条是意外收获:Win11 上 GetCommandLineW 就是 A1 xx xx xx xx C3 ——
它只是"返回一个全局指针",想替换返回值,改槽就够了。
顺带纠正一个错误:只改 PEB 里的 CommandLine 没用 ——
kernelbase 在进程启动时就把命令行缓存进自己的全局了,GetCommandLine 读的是缓存。
_ReturnAddress()(直接调用是系统 DLL 包装层)→
从返回地址槽顺栈扫 + 模块归属过滤(找到真正的业务代码)→
映射成 模块+偏移(跨重启可用)。
module=(无) 的区域,代码引用扫描结果 0 处,静态定位直接失效。SetWindowTextW、MessageBoxW、DrawTextW、ExtTextOutW…),不想碰网卡,就在客户端进程里钩 ws2_32 的 7 个函数:send / recv / sendto / recvfrom / connect / WSASend / WSARecv,
按 时间|方向|socket|长度|hex+ascii 落盘。只读、不改一个字节,
比网卡抓包多一个天然好处:带进程上下文,还能顺手带上调用点。
用 5.5 的记录器跑一轮"登录 → 大厅 → 进小屋 → 回房间":
07:48:03.793 CONNECT -> 162.14.144.162:39311 ← 主通道
07:48:03.896 上行 132B ← 认证握手
07:48:05.368 上行 655B ← 登录请求(带 passport)
07:48:12.2xx 上行 199/200/346/622B ← 大厅进入的一串请求
07:48:13.9~14.4 下行 16384/15088/15184B ← 服务端一次性推下来的状态
07:48:41.433 下行 16384B ← 状态切换:服务端先推
07:48:41.5xx 上行 87/64/67/496B ← 客户端随后应答
07:48:44.755 CONNECT -> 162.14.144.174:39322 ← 小屋/房间走【另一条通道】
07:49:05.902 CONNECT -> 162.14.144.167:39311 ← 主通道被服务端【重定向】到实例服三点观察:
另外载荷全是高熵数据(4A EB E3 2F DA 4F 46 EE…)→ 协议加密或压缩。
想在协议上做文章,第一件事就是先把加解密还原出来。
NGClient.aes / BlackCall.aes)+ 机器级进程(BlackCipher.aes)+ 内核交叉验证。-passport:,KartRider.exe 就能脱离 TCGame.exe 自己登录 ——本文为个人技术研究记录,所有测试均在自有环境、自有账号上进行,未对任何线上服务造成影响。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。