吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 11631|回复: 22
收起左侧

[游戏安全] 以某款防封为基础对LXL检测的展开分析

  [复制链接]
豆浆你喝不 发表于 2020-6-26 09:16
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函数所在系统模块的钩子扫描是绝对没有问题的.

钩子扫描例子.png


      如上图, 可以看到大量的API函数头部被Hook了, 使用x32dbg附加游戏进程, 随便转到到其中一个被Hook API函数ReadFile的头部

ReadFile.png


      可以看到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系列函数效果图.png


      可以看到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;
	}
}

效果图:
获取原始MAC效果图.png

如上图, 即使你修改了注册表内的Network Address或网络适配器中的Network Address, 利用此函数依旧可以获取出正确的原始MAC.

接下来是该函数的HookFun示例代码:
[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的调用情况图:
1.png
3.png

      可以看到游戏中频繁调用这些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分别设置了断点, 观察它们的调用情况.
Module32NextW下断.png
EnumProcessModules下断.png
ZwQueryVirtualMemory下断.png

      如上图2, 可以看到EnumProcessModules函数断下了, 观察堆栈返回地址 可以发现是来自GameRpcs模块的调用, 第一个参数进程句柄为0xFFFFFFFF 代表游戏进程本身的伪句柄, 显然是GameRpcs正在枚举游戏进程中的所有模块, 拿到这些模块句柄后肯定是去模块头部采取数据比较是否有主流脚本Dll的特征进而作出相应封号处罚, 并且可以通过句柄获取模块路径, 读取文件数据判断是否有脚本Dll特征进而作出相应处罚. 处理方案: 1. Hook修改返回值为失败 2. Hook将获取到的模块列表中擦去你的模块和脚本的模块, 达到欺骗效果.
      如上图1, 可以看到ModuleNextW被TCJ调用了, 检测套路同上, 处理方式同上.
      如上图3, ZwQueryVirtualMemory被TCJ调用, 根据堆栈参数可以看出, 是用于获取MemorySectionName, 也就是模块名, 是TCJ用于配合ModuleNextW得到的模块句柄来获取模块名的, 未发现暴力枚举所有内存页属性的行为, 所以只需处理ModuleNextW即可.

      Step3. 观察其对Call检测的处理方式
      脚本要做到实现自动走A, 躲避, 连招, 必然是要调用到游戏本身的移动Call和技能Call的, 所以防封肯定要处理Call检测.

      首先定位到游戏较内层的移动call:
移动Call头部.png

       如上图, 可以看到游戏移动Call头部被下了钩子, 跳向TenRpcs模块, 显然就是Call检测了, 跟进Call内部看看代码
TenRpcs中转处.png

      可以看到进来后, 堆栈被抬高了4, 为了平衡call进来的堆栈, 随后pushad pushfd保存寄存器状态, 然后push了两个参数最后进入又进入了一个Call 出来后平衡堆栈并 popfd popad还原寄存器状态 执行原移动Call头部原指令, push 要返回的地址, ret返回. 关键还是在于中间调用的Call 于是继续跟进
全局Call上.png
全局Call下.png

      如上图, 可以看到该函数内部关键在于中间的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反作弊引擎可以从这入手对关键对象头部处的虚表指针进行校验, 以此可以推断出玩家是否有作弊行为.
      未发现该防封有相关处理.

      0x3 结束语

      虽然此次粗略分析只能窥出LXL的T*P反作弊引擎的冰山一角, 但也是对其有了最基本的认知, 希望本文能对热爱逆向的读者有所帮助.

      再次声明: 文章内容无任何不正当目的与非法企图, 仅仅是为了公开交流学习, 请读者不要以本文内容去做出任何违法行为!

免费评分

参与人数 14威望 +2 吾爱币 +116 热心值 +12 收起 理由
阿呆哥 + 1 多多分析呀 楼主
DelicateXSJ + 1 + 1 看不懂,不过6月份玩了6.7次小冰雹,当时宣传说能过所有机器码,我的笔记本机.
fc314135188 + 1 + 1 非自愿把我整笑了
1052164666 + 1 + 1 我还买了小冰雹周卡呢,论坛有没大牛有免费的防封啊
antclt + 1 + 1 我很赞同!
djjbxxz + 1 + 1 我很赞同!
gaosld + 1 + 1 用心讨论,共获提升!
959935291 + 1 + 1 用心讨论,共获提升!
yesooook + 1 用心讨论,共获提升!
Time丨Brand + 3 + 1 用心讨论,共获提升!
247605832 + 1 + 1 谢谢@Thanks!
二娃 + 2 + 1 谢谢@Thanks!
evill + 1 + 1 谢谢@Thanks!
Hmily + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

阿呆哥 发表于 2020-7-3 19:45
大佬 开发一个过机器码工具呗  
Rookietp 发表于 2020-7-1 02:40
本帖最后由 Rookietp 于 2020-7-1 02:43 编辑

写的挺好,但是只是冰山一角,期待后续。
netspirit 发表于 2020-7-1 06:49
修改机器码怎么违法了 封机器码才有法律问题吧
庆鼠年 发表于 2020-7-1 15:24
问一下楼主有win10 能用的 PCHunter吗,
刘晓晨 发表于 2020-7-1 16:16
虽然看不懂,但是好高大上的感觉
帝王Burlk 发表于 2020-7-1 23:29
机器码思路确实是这样 防封部分有点问题
247605832 发表于 2020-7-2 00:46
很棒的文章期待后续
pangpangpanghu 发表于 2020-7-3 10:53
感谢分享,学习了!
shuye001 发表于 2020-7-5 03:52
大佬厉害了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-24 04:43

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表