0x0 须知
文章内容无任何不正当目的与非法企图, 仅仅是为了公开交流学习!
0x1 准备工作
我的调试环境: Windows 10-1809
技术支持(非自愿): 小冰雹防封软件
我用到的工具: 1. x32dbg 2. PCHunter(一款强大的ARK工具, 缺点: 作者长时间未更新, 现已不支持1809往后的新系统).
0x2 进入正题
小冰雹防封
出处: *群下载的
开发语言: 易语言
主要功能:
1. 针对对两款主流脚本(HanBOT, GE)的过检测, 使用者可以配合脚本在LXL游戏中实现自动走A, 连招, 躲避而不被封号.
2. 躲避机器码封禁, 使用者可以在已经被T*P封禁的电脑设备上正常游戏.
未发现小冰雹加载驱动, 所有功能均在应用层完成.
注入姿势: 将其核心功能Dll释放到游戏目录, 以hid.dll命名
补充: hid是一个系统Dll的名字, 位于系统盘Windows\\System32\\目录下. LXL游戏进程在启动初期会尝试载入一次同目录下的hid.dll文件, 并且载入时机十分早, 在T*P保护未覆盖游戏时就已将hid.dll载入内存.
Step1. 观察其对机器码封禁检测的处理方式
在开始之前, 我们要对游戏的机器码封禁检测有个基本构思, 首先IP地址显然不可能作为判断机器封禁的依据, 那么只能是玩家进行游戏的本机设备信息了, 例如硬盘, CPU, 网卡, 键盘鼠标这一系列设备的信息, LXL玩家众多, 考虑到要防止误封的情况, 必然是要采集多个设备的信息综合判断, 而若是在R3层面完成这些信息的获取, 无疑就是使用Win32 API获取. 而防封软件无非就是使用Hook, 阻止游戏调用相关API或者将获取后的缓冲区中的设备信息随机化再作返回. 而Hook位置则可以选为相关API函数处, 或是在返回的T*P检测模块调用处. 而显然是在API函数处Hook更省力, 因为API函数所在地址可以通过GetProcAddress获取到, 可视为固定地址, 而若是在游戏相关模块内存地址作处理, 在这些模块更新后我们都要重新找到对应偏移, 且还会触发其CRC, 是吃力不讨好的方式.
如上, 我们应先观察防封对API的Hook情况, 但是如果逐个跳转至API函数头部查看是否被Hook显然十分低效且可能会遗漏, 这时我们可以借助PCHunter工具中的进程应用层钩子扫描功能, 可以快速查看指定进程中模块内存的Hook情况, 某些加壳的模块和非可执行代码的区域(例如数据段)这些位置的数据改变是无法侦测的, 但对API函数所在系统模块的钩子扫描是绝对没有问题的.
如上图, 可以看到大量的API函数头部被Hook了, 使用x32dbg附加游戏进程, 随便转到到其中一个被Hook API函数ReadFile的头部
可以看到ReadFile函数头部被改为jmp了, 跳转至到防封的hid.dll内了, 在他的函数内处理完后在返回出去, 标准的Hook流程.
这些API函数都是十分常用的并且网上资料丰富, 不作过多叙述, 下面我会演示其中两个比较复杂的API函数用法和Hook后的处理方式
1. SetupDiGetDeviceInstanceIDA 功能: 检索指定设备实例Id
此函数原型:
[C++] 纯文本查看 复制代码 BOOL WINAPI SetupDiGetDeviceInstanceIdA(
HDEVINFO DeviceInfoSet, // 设备信息集合的句柄
PSP_DEVINFO_DATA DeviceInfoData, // SP_DEVINFO_DATA结构体的指针
PSTR DeviceInstanceId, // 用于接收返回的设备Id字符串, 此参数为我选取的处理点
DWORD DeviceInstanceIdSize, // 缓冲区大小
PDWORD RequiredSize // 接收返回Id的实际大小
)
以下是我写的该函数调用例子:
[C++] 纯文本查看 复制代码 void Test()
{
HDEVINFO hDevInfo = SetupDiGetClassDevsA(&GUID_DEVCLASS_NET, nullptr, nullptr, DIGCF_PRESENT);
cout << "hDevInfo: " << hDevInfo << endl;
if (hDevInfo == INVALID_HANDLE_VALUE)
{
cout << "hDevInfo Error!" << endl;
return;
}
SP_DEVINFO_DATA DevInfoData{ sizeof(SP_DEVINFO_DATA) };
char szBuf[MAX_PATH]{ 0 };
for (int i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &DevInfoData); i++)
{
cout << i << "、" << endl;
if (SetupDiGetClassDescriptionA(&DevInfoData.ClassGuid, szBuf, MAX_PATH, nullptr))
{
cout << "SetupDiGetClassDescriptionA - > "
<< "ClassDescription: " << szBuf << endl;
}
if (SetupDiGetDeviceInstanceIdA(hDevInfo, &DevInfoData, szBuf, MAX_PATH, nullptr))
{
cout << "SetupDiGetDeviceInstanceIdA - > "
<< "DeviceInstanceId: " << szBuf << endl;
}
if (SetupDiGetDeviceRegistryPropertyA(hDevInfo, &DevInfoData, SPDRP_DEVICEDESC, nullptr, reinterpret_cast<PBYTE>(szBuf), MAX_PATH, nullptr))
{
cout << "SetupDiGetDeviceRegistryPropertyA - > "
<< "DeviceRegistryProperty: " << szBuf << endl;
}
}
SetupDiDestroyDeviceInfoList(hDevInfo);
}
控制台窗口程序运行后的效果图:
可以看到SetupDi系列函数配合使用可以将电脑中所有适配器(包括你在设备管理器禁用的设备)相关的信息枚举出来.
以下是该函数的HookFun编写示例:
[C++] 纯文本查看 复制代码 BOOL WINAPI HookFun_SetupDiGetDeviceInstanceIdA(
HDEVINFO DeviceInfoSet,
PSP_DEVINFO_DATA DeviceInfoData,
PSTR DeviceInstanceId,
DWORD DeviceInstanceIdSize,
PDWORD RequiredSize
)
{
DbgPrintEx("TP - > SetupDiGetDeviceInstanceIdA");
BOOL bResult = g_oSetupDiGetDeviceInstanceIdA(DeviceInfoSet, DeviceInfoData, DeviceInstanceId, DeviceInstanceIdSize, RequiredSize);
if (DeviceInstanceId != nullptr)
{
for (int i = 0; i < 20; i++)
DeviceInstanceId[i] = FakeSN[i]; // 将DeviceInstanceId改为预先设好的随机字符
}
return bResult;
}
2.DeviceIoControl (功能只针对机器码检测方面进行描述)功能: 可以与从指定驱动设备通信, 使其返回相关设备信息.此函数原型: [C++] 纯文本查看 复制代码 BOOL WINAPI DeviceIoControl(
HANDLE hDevice, // 设备句柄
DWORD dwIoControlCode, // 控制码
LPVOID lpInBuffer, // 指向所执行操作所需的数据缓冲区的指针
DWORD nInBufferSize, // 输入缓冲区的大小
LPVOID lpOutBuffer, // 输出缓冲区, 接收返回来的设备数据的, 此处为我的处理点
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 输出数据的大小
LPOVERLAPPED lpOverlapped // OVERLAPPED结构体指针
)
此函数可以做的事情太多了, 这里我只演示获取网络适配器原始MAC的写法: [C++] 纯文本查看 复制代码 void GetRealMac()
{
LPVOID pBuf = nullptr;
PIP_ADAPTER_INFO pAdapterInfo = nullptr;
char szFileName[MAX_PATH]{ 0 };
ULONG ulOutLen = 0;
GetAdaptersInfo(nullptr, &ulOutLen);
pBuf = new char[ulOutLen];
pAdapterInfo = reinterpret_cast<PIP_ADAPTER_INFO>(pBuf);
if (GetAdaptersInfo(pAdapterInfo, &ulOutLen) == NO_ERROR)
{
while (pAdapterInfo)
{
strcpy_s(szFileName, "\\\\.\\");
strcat_s(szFileName, pAdapterInfo->AdapterName);
HANDLE hFile = CreateFileA(szFileName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
DWORD dwInBuf = OID_802_3_PERMANENT_ADDRESS;
BYTE outBuf[6];
DWORD dwRetLen;
DeviceIoControl(hFile, IOCTL_NDIS_QUERY_GLOBAL_STATS, &dwInBuf, sizeof(dwInBuf), outBuf, sizeof(outBuf), &dwRetLen, nullptr);
CloseHandle(hFile);
// 输出MAC地址
cout << setw(2) << setfill('0') << setiosflags(ios::uppercase) << hex << outBuf[0] + 0 << "-"
<< outBuf[1] + 0 << "-"
<< outBuf[2] + 0 << "-"
<< outBuf[3] + 0 << "-"
<< outBuf[4] + 0 << "-"
<< outBuf[5] + 0 << endl;
pAdapterInfo = pAdapterInfo->Next;
}
delete[] pBuf;
}
}
效果图:
如上图, 即使你修改了注册表内的Network Address或网络适配器中的Network Address, 利用此函数依旧可以获取出正确的原始MAC. [C++] 纯文本查看 复制代码 BOOL WINAPI HookFun_DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
)
{
BOOL bResult = g_oDeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped);
if (dwIoControlCode == IOCTL_STORAGE_QUERY_PROPERTY || dwIoControlCode == SMART_RCV_DRIVE_DATA)
{
//DbgPrintEx("DeviceIoControl - > 获取序列号");
memcpy((((PIDINFO)lpOutBuffer)->sSerialNumber) + (*lpBytesReturned) - IDENTIFY_BUFFER_SIZE, FakeSN, 20);
}
if (dwIoControlCode == IOCTL_NDIS_QUERY_GLOBAL_STATS)
{
//DbgPrintEx("DeviceIoControl - > 获取原生MAC");
((PBYTE)lpOutBuffer)[0] = FakeMAC[0]; // 改为伪造的MAC
((PBYTE)lpOutBuffer)[1] = FakeMAC[1];
((PBYTE)lpOutBuffer)[2] = FakeMAC[2];
((PBYTE)lpOutBuffer)[3] = FakeMAC[3];
((PBYTE)lpOutBuffer)[4] = FakeMAC[4];
((PBYTE)lpOutBuffer)[5] = FakeMAC[5];
}
return bResult;
}
我自己也写了个Dll针对这些获取设备信息的API逐个进行了Hook, 注入方式同样采用更名hid方式, 因为这个注入时机很好, 可以避免出现你还未对游戏进行Hook, 游戏检测就已经获取完你电脑设备信息的情况, 下面是Hook后的各个API的调用情况图:
可以看到游戏中频繁调用这些API获取设备信息作为机器封禁判断依据的检测模块是PolicyProbe, 说明T*P对玩家游戏设备信息采集并上报是由该模块完成的. 处理方式: Hook相关API, 使其得到我们伪造后的设备信息. Step2. 观察其对第三方模块检测的处理
防封软件本身也是通过载入游戏的hid.dll实现防封, 而脚本也是通过注入的Dll实现脚本功能的, 显然这就要处理对这些第三方非法模块的检测了.
防封Dll也Hook了一些枚举进程模块的API, 如Module32First, Module32FirstW, Module32Next, Module32NextW. 这一类通过快照枚举进程模块的API(详细资料自行百度, 本文不作过多叙述) 可以看到该防封做法十分多此一举, Module32First和Module32Next明明只需要Hook其中一个即可达到目的. 另外还有EnumProcessModules和ZwQueryVirtualMemory也是枚举进程模块的, x32dbg附加游戏后, 我对Module32Next, Module32NextW, EnumProcessModules, ZwQueryVirtualMemory这四个API分别设置了断点, 观察它们的调用情况.
如上图2, 可以看到EnumProcessModules函数断下了, 观察堆栈返回地址 可以发现是来自GameRpcs模块的调用, 第一个参数进程句柄为0xFFFFFFFF 代表游戏进程本身的伪句柄, 显然是GameRpcs正在枚举游戏进程中的所有模块, 拿到这些模块句柄后肯定是去模块头部采取数据比较是否有主流脚本Dll的特征进而作出相应封号处罚, 并且可以通过句柄获取模块路径, 读取文件数据判断是否有脚本Dll特征进而作出相应处罚. 处理方案: 1. Hook修改返回值为失败 2. Hook将获取到的模块列表中擦去你的模块和脚本的模块, 达到欺骗效果. 如上图1, 可以看到ModuleNextW被TCJ调用了, 检测套路同上, 处理方式同上. 如上图3, ZwQueryVirtualMemory被TCJ调用, 根据堆栈参数可以看出, 是用于获取MemorySectionName, 也就是模块名, 是TCJ用于配合ModuleNextW得到的模块句柄来获取模块名的, 未发现暴力枚举所有内存页属性的行为, 所以只需处理ModuleNextW即可. 脚本要做到实现自动走A, 躲避, 连招, 必然是要调用到游戏本身的移动Call和技能Call的, 所以防封肯定要处理Call检测.
如上图, 可以看到游戏移动Call头部被下了钩子, 跳向TenRpcs模块, 显然就是Call检测了, 跟进Call内部看看代码
可以看到进来后, 堆栈被抬高了4, 为了平衡call进来的堆栈, 随后pushad pushfd保存寄存器状态, 然后push了两个参数最后进入又进入了一个Call 出来后平衡堆栈并 popfd popad还原寄存器状态 执行原移动Call头部原指令, push 要返回的地址, ret返回. 关键还是在于中间调用的Call 于是继续跟进
如上图, 可以看到该函数内部关键在于中间的Call edx, 会进入对应的GameRpcs模块中的检测函数, 继续跟进马上就会碰到人见人爱的T*P版虚拟机了. [C++] 纯文本查看 复制代码 __asm
{
lea esp, ss: [esp + 0x4] // 恢复Call进来减小的4字节的堆栈
pushad // 保存8个通用寄存器
pushfd // 保存eflags寄存器
push structPtr //压入的是应是一个结构体指针, 其中有代表此处的Id
push 0 // 应是TenRpcs保留的一个参数, 方便以后需要用到
call funx // 进入一个全局钩子的分配区, 根据传入的结构体中的成员判断去往对应的检测函数
lea esp, ss: [esp + 0x8] // 上面的call是外平栈, 恢复上面两个参数的压栈
popfd // 还原eflags寄存器
popad // 还原8个通用寄存器
// 以下执行挂钩处覆盖的原指令, 并跳回
mov esp, dword ptr ss : [esp - 0x14]
sub esp, 0xE0
push 0x586316
ret
}
如上代码注释, 该结构基本解析完毕, TenRpcs模块在游戏很多功能函数处都下了这种钩子, 内部结构都是这样(以此作为特征码, 可以搜出游戏中所有此类结构的地址), 实际分析中, 此类结构都是相邻的, 间距为0x190个字节, 压入的结构体指针为各个TenRpcs钩子中转结构的"身份证", 最后由上图中的Call edx跳向各个"身份证"所对应的函数或检测函数. 该防封并未对这些TenRpcs钩子相关处下Hook, 更别说伪造堆栈, 伪造调用链一类的操作了, 首先来到移动Call钩子的中转处, 目光聚焦到上述结构中的push structPtr处, 跳转至该指针指向, 直觉告诉我: 此处的结构体成员数据被他改变了 从而影响了原来要跳向的对应检测函数. 此处正好为数据段, PCHunter的钩子扫描是会跳过这些位置的, 所以要靠自己发现, 但由于其hid.dll的注入处理时机太早, 不方便观察, 所以我预先把该防封所产的hid.dll从游戏目录抠了出来, 等我进入游戏记录完TenRpcs钩子处的结构体成员数据, 再用其他工具将其hid.dll注入游戏进程, 最终发现该处数据的确被他改变了, 导致游戏中脚本需调功能函数在经过内部的Call edx时不会执行至原先对应的检测函数, 其实就是绕开了检测, 所以该防封处理Call检测的方式是使脚本所调用的几个功能函数绕开原先TenRpcs钩子中必经的检测函数继续往下执行. 如上解释, 该防封直接跳过Call检测并不是一种明智的的做法, 想办法欺骗检测应该是较合理的方式, 要知道T*P的本质就是发包检测, 必然涉及到各种检测与其服务器的通讯, 强撸只会触发其它校验导致的异常. 如果这样能过掉Call检测, 只能说T*P的大佬们在故意放水. Step4. 观察其对虚表数据校验和代码CRC校验的处理 这里我简单解释一下什么是虚表检测, LXL主流脚本Hook了游戏中大量对象头部的虚表指针, 替换为指向自己表的指针, 以此来实时获取所需数据与执行脚本功能的最佳时机. 显然T*P反作弊引擎可以从这入手对关键对象头部处的虚表指针进行校验, 以此可以推断出玩家是否有作弊行为. 虽然此次粗略分析只能窥出LXL的T*P反作弊引擎的冰山一角, 但也是对其有了最基本的认知, 希望本文能对热爱逆向的读者有所帮助. 再次声明: 文章内容无任何不正当目的与非法企图, 仅仅是为了公开交流学习, 请读者不要以本文内容去做出任何违法行为! |