XTrap驱动分析
NetRoc/cc682
最近拿到一个使用XTrap的游戏,据说此物乃NP和HS之外的第三大反外挂系统,so拿来瞧了瞧。
Ring3层包括几个dll和一个进程。看里面貌似使用了pipe相关的函数,运行时也起了一个进程。所以XTrap的架构应该和NP很类似,但是实现上就要弱很多了。
1、 现在还没发现有ring3全局注入的dll。
2、 大量工作放到了作为和游戏接口的dll里面。通过dll方式提供游戏使用这点不同于NP的lib库,而更类似HS。这种方式一大弱点就是那个dll容易被模拟,并且比较难发现。
3、 驱动相对来说应该是三个系统里面最弱的了,原因下面会讲到。花了5天时间逆出了整个驱动的源码,好像没有那么多硬编码的东西,呵呵。不过倒是发现了一些编程的BUG什么的。基本的功能点只有三个:HOOK SSDT实现的跨进程访问控制、通过对IoAccessMap的设置关闭对鼠标键盘端口访问权限、通过挂接Int 1中断获得调试信息。
大概的流程如下:
1、 DriverEntry:通过PsGetVersion判断系统版本,并根据不同的版本保存要Hook的在SSDT Shadow表中服务的ID。而SSDT表中的则是由后面IoControl里面Ring3传下来的。目前来看已经支持Vista了。通过KeQuerySystemTime拿了一下系统时间并保存下来,不过后面就没有再使用了,估计以后为了反调试会做时间检查什么的东西吧。申请了0x2000长度的内存,这是用于后面设置IoAccessMap的。然后就是例行的IoCreateDevice和IoCreateSymbolicLink,设置Dispatch例程。XTrap的IRP_MJ_DEVICE_CONTROL、IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_CLEANUP是在同一个例程中处理的。最后有一个莫名其妙的调用KfLowerIrql( KeRaiseIrqlToDpcLevel());偶的水平实在是还难以理解高丽棒子为啥要这样做,嘿嘿。
2、 剩下的就是通过DeviceIoControl来控制的了,我这个版本的XTrap一共有17个ControlCode。Dispatch例程的代码如下
NTSTATUS
XDvaDispatchAll( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
PIO_STACK_LOCATION pIrpStack;
PVOID pSystemBuffer;
PVOID pOutBuffer;
ULONG ulMajorFunction;
NTSTATUS ntStatus;
pIrpStack = IoGetCurrentIrpStackLocation( Irp);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
pSystemBuffer = Irp->AssociatedIrp.SystemBuffer;
ulMajorFunction = pIrpStack->MajorFunction;
switch( ulMajorFunction)
{
case IRP_MJ_DEVICE_CONTROL:
if( (pIrpStack->Parameters.DeviceIoControl.IoControlCode & 0x3) == 0x3)
{
pOutBuffer = Irp->UserBuffer;
}
else
{
pOutBuffer = pSystemBuffer;
}
return DoDeviceIoControl( Irp, pIrpStack->FileObject, 1, pSystemBuffer,
pIrpStack->Parameters.DeviceIoControl.InputBufferLength,
pOutBuffer, pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,
pIrpStack->Parameters.DeviceIoControl.IoControlCode,
&Irp->IoStatus, DeviceObject);
case IRP_MJ_CLOSE:
HookSSDT( g_HookInfo.NtOpenProcessInfo.Id, (ULONG)g_pNtOpenProcess, (ULONG)NewNtOpenProcess);
HookSSDT( g_HookInfo.NtDeviceIoControlFileInfo.Id, (ULONG)g_pNtDeviceIoControlFile, (ULONG)NewNtDeviceIoControlFile);
HookSSDT( g_HookInfo.NtWriteVirtualMemoryInfo.Id, (ULONG)g_pNtWriteVirtualMemory, (ULONG)NewNtWriteVirtualMemory);
HookSSDT( g_HookInfo.NtOpenSectionInfo.Id, (ULONG)g_pNtOpenSection, (ULONG)NewNtOpenSection);
HookSSDT( g_HookInfo.NtProtectVirtualMemoryInfo.Id, (ULONG)g_pNtProtectVirtualMemory, (ULONG)NewNtProtectVirtualMemory);
HookSSDT( g_HookInfo.NtTerminateProcessInfo.Id, (ULONG)g_pNtTerminateProcess, (ULONG)NewNtTerminateProcess);
HookSSDT2( g_dwNtGdiGetPixelId, (ULONG)g_pNtGdiGetPixel, (ULONG)NewNtGdiGetPixel);
HookSSDT2( g_dwNtUserSendInputId, (ULONG)g_pNtUserSendInput, (ULONG)NewNtUserSendInput);
HookSSDT2( g_dwNtUserCallNextHookExId, (ULONG)g_pNtUserCallNextHookEx, (ULONG)NewNtUserCallNextHookEx);
HookSSDT2( g_dwNtUserPostMessageId, (ULONG)g_pNtUserPostMessage, (ULONG)NewNtUserPostMessage);
HookSSDT2( g_dwNtUserTranslateMessageId, (ULONG)g_pNtUserTranslateMessage, (ULONG)NewNtUserTranslateMessage);
if( g_byIsSuccess)
{
g_byIsSuccess = FALSE;
}
if( g_byIsReboot)
{
_asm cli;
WRITE_PORT_UCHAR( (PUCHAR)0x64, (UCHAR)0xFE);
_asm hlt;
}
else
{
ntStatus = STATUS_SUCCESS;
}
break;
case IRP_MJ_CREATE:
if( g_arrSomeCode[0] == 0)
{
memcpy( g_arrSomeCode, MyInt1, 5*sizeof( ULONG));
}
ntStatus = STATUS_SUCCESS;
break;
case IRP_MJ_CLEANUP:
ntStatus = STATUS_SUCCESS;
break;
default:
Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
ntStatus = STATUS_INVALID_DEVICE_REQUEST;
}
IofCompleteRequest( Irp, IO_NO_INCREMENT);
return ntStatus;
}
IRP_MJ_DEVICE_CONTROL的处理是在另外一个函数里面,由于太复杂,影响blog的美观,就不写出来了,哈哈。这里面可以看到,XTrap采用了和NP一样的办法重起电脑,就是往64端口写0xFE。挺白痴的是居然通过WRITE_PORT_UCHAR,难道以为只有他会Hook~
IRP_MJ_CREATE里面把自己的Int 1 函数的代码复制了一段出来,这个会用于后面再覆盖回去,是用于对付Inline hook的伎俩。
其他就没什么好说的了。
3、 关于SSDT的Hook
SSDT中Hook的函数有以下几个:NtOpenProcess、NtDeviceIoControlFile、NtWriteVirtualMemory、NtOpenSection、NtProtectVirtualMemory、NtTerminateProcess
Shadow Table中Hook的函数有下面几个:NtGdiGetPixel、NtUserSendInput、NtUserCallNextHookEx、NtUserPostMessage、NtUserTranslateMessage。
所有函数Hook的目的都很清楚,没有什么古怪的地方,呵呵。不过相当部分的钩子都只是简单的pass过去,并没有任何实质性的处理。可以看出来XTrap仍然是一个非常不完善的系统,这些部分应该都是留到以后进行功能扩充的。
关于Shadow Table的处理有一些特别的地方。Shadow Table的地址获取采用了硬编码+验证的方式。这一点偶个人觉得还是在KeAddSystemServiceTable中去取比较好,至少说在出现新的系统的时候很大可能并不用修改代码。另外,取到Shadow Table地址之后,除了将KSERVICE_TABLE_DESCRIPTOR地址保存之外,还将Shadow Table的Base保存到了KeServiceDescriptorTable第二项的Base中,以后在Hook或者其他操作的时候就直接到KeAddSystemServiceTable地址+0x10去取了。这一点我也觉得有些奇怪,保存到全局变量什么的不就好了,为什么要去修改系统本身的东西,虽然目前那个位置并没有什么用。大约是为了反调试。
4、 关于Int 1的处理
这里貌似也没什么好说的,记录了一下断点被触发的次数、dr0到dr4的内容什么的,然后IoControl里面Ring3会取走这些信息。不过有个很搞笑的BUG,Hook中断的函数里面的cli没有对应的sti。
5、 关于IoAccessMap的处理
这里没什么好说的,是由Ring3触发,Ring0实现。贴一段DeviceIoContrl里面的代码就明白了。
case 0x85000044:
ntStatus = STATUS_INVALID_PARAMETER;
if( !pSystemBuffer || ulInputBufferLength != 4)
{
break;
}
PsLookupProcessByProcessId( *((ULONG*)pSystemBuffer), pSystemBuffer);
((PUCHAR)g_pIoAccessMap)[0x0C] |= 0xFF;
((PUCHAR)g_pIoAccessMap)[0x0D] |= 0xFF;
Ke386IoSetAccessProcess( pSystemBuffer, 1);
Ke386SetIoAccessMap( 1, g_pIoAccessMap);
ntStatus = STATUS_SUCCESS;
break;
现在模拟键盘的所谓硬件模式,大部分人都是使用了网上一些开源工具,例如WinIo,基本原理就是通过IoAccessMap打开ring0的端口读写权限(啰嗦一句,上次看到某人拿来的一个sys,貌似将整个机器的io都打开了,实在是无比暴力……。寒一个)。所以对应办法就是也通过改写IoAccessMap关闭掉权限。
这也是我现在比较推荐使用的方法,对使用WinIo的按键精灵什么的外挂,都有药到病除的疗效。而且,影响范围比较小,只关闭了有限的端口。对于某些特殊情况下的程序,也可以发现之后再单独处理。不过对于自己写驱动读写端口的一类外挂来说,任何办法都没用了。In~~~out~~~~in~~~~out~~~~~in~~~~~~out~~~~~~
6、 下面选一些函数贴出来吧
ULONG __stdcall
NewNtGdiGetPixel( PVOID hDC, LONG XPos, LONG YPos)
{
BOOLEAN blIsBlock = TRUE;
if ( g_dwCurrentProcessId == (ULONG)PsGetCurrentProcessId())
{
blIsBlock = FALSE;
}
//这里奇怪,不知道为什么这么搞
if ( XPos == 0)
{
if( YPos != 0)
{
if( YPos == 0x5A)
{
blIsBlock = FALSE;
}
}
else
{
blIsBlock = FALSE;
}
}
if( g_byIsSuccess == TRUE && blIsBlock == TRUE)
return 0;
return g_pNtGdiGetPixel( hDC, XPos, YPos);
}
ULONG
__stdcall NewNtUserSendInput(
ULONG nInputs,
LPINPUT pInput,
ULONG cbSize)
{
if( (g_byIsSuccess != TRUE) || (g_byAllowUserSendInput == TRUE))
{
return g_pNtUserSendInput( nInputs, pInput, cbSize);
}
else
{
return 1;
}
}
NTSTATUS
__stdcall NewNtOpenProcess (
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
)
{
if( g_dwCurrentProcessId != 0)
{
if( (ULONG)ClientId->UniqueProcess == g_dwCurrentProcessId)
{
if( DesiredAccess != 0x478)
{
DesiredAccess &= 0xFFFFFFCF;//清掉PROCESS_VM_READ和PROCESS_VM_WRITE
}
}
}
if( g_dwProtectPid2 != 0)
{
if( g_dwProtectPid2 == (ULONG)ClientId->UniqueProcess)
{
DesiredAccess &= 0x0FFFFFFFE; //清掉PROCESS_TERMINATE
}
}
if( g_dwCurrentProcessId == (ULONG)ClientId->UniqueProcess)
{
g_dwIsSomeoneOpenMe = 1;
}
return g_pNtOpenProcess( ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}
NTSTATUS
__stdcall NewNtWriteVirtualMemory(
HANDLE ProcessHandle,
PVOID BaseAddress,
CONST VOID *Buffer,
SIZE_T BufferSize,
PSIZE_T NumberOfBytesWritten
)
{
BOOLEAN blIsNeedSkip = FALSE;
PROCESS_BASIC_INFORMATION stProcessInfo;
HANDLE Handle;
HANDLE hCurrentPid;
RtlZeroMemory( &stProcessInfo, sizeof(stProcessInfo));
if( STATUS_SUCCESS ==
ZwDuplicateObject( (HANDLE)0xFFFFFFFF,
ProcessHandle,
(HANDLE)0xFFFFFFFF,
&Handle,
0x400,
0,
0)
)
{
ZwQueryInformationProcess( Handle, 0, &stProcessInfo, 0x18, 0);
ZwClose( Handle);
}
if( g_dwCurrentProcessId == (ULONG)stProcessInfo.UniqueProcessId)
{
blIsNeedSkip = TRUE;
}
hCurrentPid = PsGetCurrentProcessId();
if( (ULONG)hCurrentPid == g_dwSafePid1 ||
(ULONG)hCurrentPid == g_dwSafePid2 ||
(ULONG)hCurrentPid == g_dwSafePid3)
{
blIsNeedSkip = FALSE;
}
if( (ULONG)hCurrentPid == g_dwCurrentProcessId)
{
if( (g_dwFromUser2 | 0xFFFFF0F) == 0xFFFFF1F)
{
blIsNeedSkip = FALSE;
}
}
if ( !g_byIsSuccess || !blIsNeedSkip)
{
return g_pNtWriteVirtualMemory( ProcessHandle, BaseAddress, Buffer, BufferSize, NumberOfBytesWritten);
}
return 0;
}
ULONG __stdcall NewNtUserTranslateMessage(PMSG lpMsg, ULONG dwhkl)
{
CHAR ucScanCode, ucScanCode2;
UCHAR blNeedSkip = FALSE;
if( lpMsg->message == 0x100 || lpMsg->message == 0x101)
{//WM_KEYDOWN,WM_KEYUP
ucScanCode2 = IsNeedSkipKeyMsg( lpMsg->wParam);
if( ucScanCode2)
{
ucScanCode = (lpMsg->lParam & 0x00FF0000) >> 16;
if( ucScanCode == ucScanCode2)
{
blNeedSkip = TRUE;
}
}
}
if( !g_byIsSuccess || !blNeedSkip)
{
return g_pNtUserTranslateMessage( lpMsg, dwhkl);
}
return 1;
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!