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;
}