这是棒子的一款游戏保护,知名度好像不是很高,网上搜了一圈得到的信息很少,于是只能自己动手了。中途因为难度太大(对我而言)弃坑了好几次,现在基本上放弃,遂分享一下成果。环境是win 10 64位系统下的某32位游戏。
驱动层
通过PCHunter
观察得知,该保护的驱动文件在路径C:\Windows\SysWOW64\drivers
下。拖进DIE
中显示驱动是没有壳的,于是可以很高兴的直接拖进IDA中。
整个驱动文件其实不大,只有几十kb,IDA中分析出来的函数也只有二三十个,允许我们一个个看过去。在这个驱动中有个贯穿全文的全局变量,我把它命名为XtrapGlobalData
,在这里我先把我分析得到该变量有限的结构体贴出。
1 | struct XTRAP_GLOBAL_DATA |
DriverEntry
首先初始化了设备名和符号链接名,从而得知了应用层和驱动层是通过设备通信的。
1 | memmove(&SourceString, L"\\Device\\X6va066", 0x20u);// DeviceName |
使用PsGetVersion
获取版本号,确认系统版本。
1 | PsGetVersion(&MajorVersion, &MinorVersion, &BuildNumber, 0i64); |
根据系统版本,填写一些硬编码,比如各种结构的偏移。
1 | if ( result == WINDOWS_10_10586 ) |
设置了设备的派遣函数。
1 | Device->MajorFunction[0xE] = DispatchGeneral;// IRP_MJ_DEVICE_CONTROL |
具体是在DispatchGeneral
中判断功能号再调用对应的Dispatcher。
派遣函数
总共有3个函数,DispatchCreate
DispatchClose
和 DispatchIoControl
分别对应IRP_MJ_CREATE
IRP_MJ_CLOSE
和 IRP_MJ_DEVICE_CONTROL
。
DispatchCreate
该函数做了几件事:一是使用PsCreateSystemThread
创建了两个线程,这两个线程分别用来检测线程和查句柄;二是使用ObRegisterCallbacks
注册了一个进程回调。
先讲两个线程,其中一个线程用来检测被保护的游戏进程的线程是否被暂停,具体操作如下:
1 | do |
通过遍历游戏的ThreadList来检测游戏某个地址范围内的线程,我猜测这个范围是该保护本身的模块XTrapVa.dll(下面会讲),如果暂停的线程大于等于3个就结束进程,目的应该是为了防止调试器附加或者人为暂停该保护的检测线程。
第二个线程是用来枚举拥有被保护游戏进程句柄的进程,操作如下:
1 | if ( XtrapGlobalData.Start ) |
通过调用ZwQuerySystemInformation
的16号功能SystemHandleInformation
遍历系统中所有的句柄,从中找出指向被保护的游戏进程的句柄,将拥有该句柄的进程的PID
以及权限GrantedAccess
记录到XtrapGlobalData
当中。记录的格式是UniqueProcessId[i]
对应GrantedAccess[i]
。
然后是进程回调,做的事情和上面那个线程大同小异,代码如下:
1 | __int64 __fastcall PreProcessCallback(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation) |
记录以PROCESS_VM_WRITE
权限打开游戏的进程的PID
和EPROCESS
以及打开的权限GrantedAccess
。
DispatchClose
主要做了一些清理操作,比如取消进程回调,结束两个检测的线程等等。
1 | void *DispatchClose() |
DispatchIoControl
这个就是负责通信的函数了,很常规的通过自己定义的控制码来进行通信。总共定义了5个控制码0x86000880
0x8600089C
0x860008C0
0x86000900
0x86001804
,这里挑几个通过已知条件能整得明白的讲。
控制码0x86000880
主要工作就是把之前通过进程回调和检测线程记录下来的游戏认为的可疑进程的相关信息稍做加工后传回应用层。
1 | case 0x86000880: |
sub_11CC0
做的工作好像和之前有点重复,这里不再讲。sub_11BA0
则是获取之前记录下来的可疑进程的父进程ID和进程名,然后将它们传回应用层。
控制码0x86001804
主要工作是遍历游戏的线程,然后把相关信息传回应用层。
1 | PEPROCESS __fastcall sub_12160(XTRAP_GLOBAL_DATA *XtrapGlobalData, _DWORD *a2, _DWORD *a3) |
通过该函数和一些其它信息可以整理出一个结构体。
1 | typedef struct _XTRAP_THREAD_ENUM_INFO |
这个结构体就是该控制码传回应用层的内容,也就是说记录了游戏每个线程在这个结构体里有的成员。和XTRAP_HANDLE_INFORMATION
类似,每个结构体成员同一个索引对应的就是同一个线程。不过这里还有一个有趣的地方就是这个结构体不是明文传回应用层的,还做了一个很简单的加密。
1 | do |
大概就是把这个结构体的每个字节取反,加解密都是同样的操作。
其他控制码
其他的控制码做的工作主要是让应用层传了一些信息过来设置了XtrapGlobalData
中的Unknown
部分,以及一些蜜汁操作(反调试?),比如修改ETHREAD
中的FreezeCount
和SuspendCount
。通过已知条件尚不能解释得很清楚,所以这里就不讲了。
驱动层总结
驱动部分其实没做多少事情,主要是收集信息然后传回应用层接着分析。
应用层
其实应用层才是让人崩溃的地方。
看了一下XTrap
的文件夹,确定了两个比较主要的文件XTrap.xt
和XTrapVa.dll
。使用DIE
查壳显示Themida/Winlicense(2.X)[-]
,WDNMD直接就来了个下马威。不过通过大量的百度我还是稀里糊涂的把壳脱掉了(?)。把这两个程序拖进IDA后,导入表看不到太多函数,一开始我还以为是我脱壳失败,后来才发现是通过GetProcAddress
动态获取需要用的函数。比如像这样:
1 | *EnumProcesses = GetProcAddress_0(v4, aEnumprocesses); |
于是我只能每个变量慢慢的重命名过去。
大致浏览过后,猜测XTrap.xt
应该主要负责UI和其他工作,重头戏还是在XTrapVa.dll
中(当然并不是说XTrap.xt
没什么用,我感觉里面应该也有一部分操作,只是我没仔细研究过)。当我开始浏览XTrapVa.dll
后,我崩溃了,到处都是神奇的函数调用,放一些代码让大家感受一下。
比如虚函数
1 | int __thiscall sub_40796B60(int (__thiscall ***this)(_DWORD)) |
或者加密调用
1 | void __thiscall sub_404225B0(_DWORD *this, int a2) |
当然恶心的东西还不止是这些。这么一看,只靠静态分析这条路确实是会让人崩溃的。于是我努力地靠着有限的动态调试和有限的静态分析以及自杀式的尝试得出了一些结果,当然得出的这些结果并不一定准确。
窗口检测
使用了EnumWindow
,并且可能调用GetWindowLong
获取了以下属性:
- GWL_HINSTANCE
- GWL_HWNDPARENT
- GWL_WNDPROC
- GWL_STYLE
还调用了GetClassName
获取了类名。
创建快照
调用了CreateToolhelp32Snapshot
,参数为TH32CS_SNAPMODULE
和TH32CS_SNAPPROCESS
,即遍历了自身模块和进程。至于干了什么,我觉得需要靠完整的动态调试才能略窥一二。
x64代码调用
在有限的动态调试过程中,我发现了一个很有趣的函数。
1 | ___:40D224D0 55 push ebp |
很经典的一段32位调用64位代码,通过
1 | ___:40D22537 6A 33 push 33h |
这段代码,改变了段寄存器cs
,进入了x64代码模式。因为ida32没办法看x64代码,于是我用注释将x64代码补了上去。从代码中可以看到调用了syscall
,所以整个函数其实就是一个利用syscall
调用Nt函数的封装。该函数的格式如下:
1 | NTSTATUS |
第一个参数为要调用的Nt函数的Index,其余参数就为该Nt函数的参数。通过搜索特征码,我得知在XTrapVa.dll
中一共有两个函数实现了类似的效果,即利用syscall
调用Nt函数。并通过测试发现,XTrap
使用该函数调用过以下几个Nt函数(可能不全):
- NtQueryInformationProcess
- NtTerminateProcess
- NtReadVirtualMemory
至于干了什么,任凭各位想象。
应用层总结
虽然应用层没有vm也没有混淆之类的东西,但它还是用了一些神奇的操作阻挡了我的静态分析(毕竟我太菜了)。当然肯定不只干了我讲的这些,还有内存扫描、线程检测等等,然而精力和技术的双重限制让我只能讲到这了。
Bypass
虽然没有能够完全透烂这个保护,但是Bypass的话我还是能提供一点思路的。首先需要准备一个dll,该dll需要实现的功能如下:
- 结束
XTrapVa.dll
的所有线程 - 结束
XTrap.xt
进程 - Hook
NtTerminateProcess
和x64syscall
结束进程和线程的部分:
1 | VOID KillThread() |
Hook部分:
1 | NTSTATUS |
准备好dll之后用任意注入方式注入游戏进程即可,只需要注意一点就是注入一定要快。
当我注入后发现我可以使用CE正常附加和调试了(VEH),于是我兴冲冲的准备使用动态调试进行深入分析,然后我发现。。。
有关检测的线程都被我干掉了,我还调试个鸡儿。