对抗GetTickCount的一种方法


很多壳喜欢用检测代码运行时间来判断是否被调试。常用的有rdtsc及GetTickCount。
调试时一一跳过非常麻烦。rdtsc可以通过设置CR4中的Time Stamp Disable位来解
决。下面尝试把GetTickCount一并解决。

简单的办法是直接Hook这个api,返回自己设定的值。但Hook容易被检测。有的壳直
接模仿GetTickCount代码,Hook就不管用了。


下面是WinXP SP1下GetTickCount代码:

77E5A29B >  BA 0000FE7F   mov edx,7FFE0000
77E5A2A0    8B02          mov eax,dword ptr ds:[edx]
77E5A2A2    F762 04       mul dword ptr ds:[edx+4]
77E5A2A5    0FACD0 18     shrd eax,edx,18
77E5A2A9    C3            retn

可以看到,直接用7FFE0000处的2个dword计算出结果。<Windows2000 Native API>对这
里的数据结构说明为:

GetTickCount reads from the KUSER_SHARED_DATA page.This page is mapped read-only 
into the user mode range of the virtual address and read-write in the kernel range.
The system clock tick updates the system tick count, which is stored in this page 
directly.

Reading the tick count from this page is faster than calling ZwGetTickCount.
The KUSER_SHARED_DATA structure is defined in the Windows 2000 versions of ntddk.h.

即这个地址对应的物理页以只读方式映射到用户空间(7FFE0000),以读写方式映射到内核
空间(FFDF0000)。两个线性地址共享同一物理页。

用WinDbg可显示其详细结构:

lkd> dt _KUSER_SHARED_DATA -r
   +0x000 TickCountLow     : Uint4B
   +0x004 TickCountMultiplier : Uint4B
   +0x008 InterruptTime    : _KSYSTEM_TIME
      +0x000 LowPart          : Uint4B
      +0x004 High1Time        : Int4B
      +0x008 High2Time        : Int4B
   +0x014 SystemTime       : _KSYSTEM_TIME
      +0x000 LowPart          : Uint4B
      +0x004 High1Time        : Int4B
      +0x008 High2Time        : Int4B
   +0x020 TimeZoneBias     : _KSYSTEM_TIME
      +0x000 LowPart          : Uint4B
      +0x004 High1Time        : Int4B
      +0x008 High2Time        : Int4B
   +0x02c ImageNumberLow   : Uint2B
   +0x02e ImageNumberHigh  : Uint2B
   +0x030 NtSystemRoot     : [260] Uint2B
   +0x238 MaxStackTraceDepth : Uint4B
   +0x23c CryptoExponent   : Uint4B
   +0x240 TimeZoneId       : Uint4B
   +0x244 Reserved2        : [8] Uint4B
   +0x264 NtProductType    : 
      NtProductWinNt = 1
      NtProductLanManNt = 2
      NtProductServer = 3
   +0x268 ProductTypeIsValid : UChar
   +0x26c NtMajorVersion   : Uint4B
   +0x270 NtMinorVersion   : Uint4B
   +0x274 ProcessorFeatures : [64] UChar
   +0x2b4 Reserved1        : Uint4B
   +0x2b8 Reserved3        : Uint4B
   +0x2bc TimeSlip         : Uint4B
   +0x2c0 AlternativeArchitecture : 
      StandardDesign = 0
      NEC98x86 = 1
      EndAlternatives = 2
   +0x2c8 SystemExpirationDate : _LARGE_INTEGER
      +0x000 LowPart          : Uint4B
      +0x004 HighPart         : Int4B
      +0x000 u                : __unnamed
         +0x000 LowPart          : Uint4B
         +0x004 HighPart         : Int4B
      +0x000 QuadPart         : Int8B
   +0x2d0 SuiteMask        : Uint4B
   +0x2d4 KdDebuggerEnabled : UChar
   +0x2d8 ActiveConsoleId  : Uint4B
   +0x2dc DismountCount    : Uint4B
   +0x2e0 ComPlusPackage   : Uint4B
   +0x2e4 LastSystemRITEventTickCount : Uint4B
   +0x2e8 NumberOfPhysicalPages : Uint4B
   +0x2ec SafeBootMode     : UChar
   +0x2f0 TraceLogging     : Uint4B
   +0x2f8 Fill0            : Uint8B
   +0x300 SystemCall       : [4] Uint8B


GetTickCount使用的是:

+0x000 TickCountLow     : Uint4B
+0x004 TickCountMultiplier : Uint4B

kernel32中不少api都使用了这里的数据。用IDA可以找出不少:

GetSystemTime
GetSystemTimeAsFileTime
GetLocalTime
......

注意不只是与时间或日期相关的api,还有些别的如Beep。


用OD加载notepad,可以看到7FFE0000处数据是变化的(每单击1次)。
是谁在写入数据? 用SoftIce可找到:

ntoskrnl!KeUpdateSystemTime

.text:0043EFFE     public __stdcall KeUpdateSystemTime()
.text:0043EFFE __stdcall KeUpdateSystemTime() proc near
.text:0043EFFE     mov     ecx, 0FFDF0000h
.text:0043F003     mov     edi, [ecx+8]
.text:0043F006     mov     esi, [ecx+0Ch]
.text:0043F009     add     edi, eax
.text:0043F00B     adc     esi, 0
.text:0043F00E     mov     [ecx+10h], esi
.text:0043F011     mov     [ecx+8], edi
.text:0043F014     mov     [ecx+0Ch], esi
.text:0043F017     sub     _KiTickOffset, eax
.text:0043F01D     mov     eax, _KeTickCount.LowPart
.text:0043F022     mov     ebx, eax
.text:0043F024     jg      short loc_43F08B
.text:0043F026     mov     ebx, 0FFDF0000h
.text:0043F02B     mov     ecx, [ebx+14h]
.text:0043F02E     mov     edx, [ebx+18h]
.text:0043F031     add     ecx, _KeTimeAdjustment
.text:0043F037     adc     edx, 0
.text:0043F03A     mov     [ebx+1Ch], edx
.text:0043F03D     mov     [ebx+14h], ecx
.text:0043F040     mov     [ebx+18h], edx
.text:0043F043     mov     ebx, eax
.text:0043F045     mov     ecx, eax
.text:0043F047     mov     edx, _KeTickCount.High1Time
.text:0043F04D     add     ecx, 1
.text:0043F050     adc     edx, 0
.text:0043F053     mov     _KeTickCount.High2Time, edx
.text:0043F059     mov     _KeTickCount.LowPart, ecx
.text:0043F05F     mov     _KeTickCount.High1Time, edx
.text:0043F065     mov     ds:0FFDF0000h, ecx
.text:0043F06B     and     eax, 0FFh
.text:0043F070     lea     ecx, _KiTimerTableListHead[eax*8]
……

可以用驱动直接patch这里的代码,使其不再更新数据,或者插入一些别的代码,
减慢更新速度等。但这里的修改是全局性的,简单地NOP掉更新数据的opcode,
会导致一些程序不能正常运行。

由于KUSER_SHARED_DATA的特殊性,可以换个办法。windows操作系统使用分页
机制管理内存,每个进程拥有自己的页目录和页表。可以替换掉被调试进程内
地址7FFE0000对应页的pte,使其指向我们分配的buffer,这样可以向buffer内
写入数据影响被调试进程,而不会产生全局性的后果。


大致的实现如下:

写1个dll,修改OD的引入表将这个dll映射到OD进程空间。Hook OD的WaitForDebugEvent:


BOOL __stdcall COllyDbg::NewWaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,
  DWORD dwMilliseconds)
{
  
  HANDLE  hDebuggedProcess = 0;
  HANDLE  hDebuggedThread = 0;
  BOOL  bRet = false;
  
  
  bRet = m_pfnWaitForDebugEvent(lpDebugEvent,dwMilliseconds);  //call原api
  
  switch(lpDebugEvent->dwDebugEventCode) 
  {
  case EXCEPTION_DEBUG_EVENT:
    
    // 第1个int 3调试事件,
    
    if((EXCEPTION_BREAKPOINT == 
      lpDebugEvent->u.Exception.ExceptionRecord.ExceptionCode) 
    {
    
      // m_pDriver用来与驱动通信

      m_pDriver->SetTSD();        // for rdtsc ;-)
      m_pDriver->ReplacePteOfKuserSharedData();  // 替换pte
    }
  
    break;
    
  case EXIT_PROCESS_DEBUG_EVENT:  
    
    m_pDriver->ClearTSD();
    m_pDriver->RecoverPteOfKuserSharedData();  // 恢复

    break;
    
  default:
    break;
  }
  
  
  return bRet;
}


这是在dll内定义的4KB的buffer,用来保存从被调试进程的7FFE0000读出的1页数据(因为这里的
数据不少api要使用,必须保留)。使用单独的section,可以保证其虚拟地址按页对齐。

#pragma data_seg("shared")  
DWORD KuserSharedData[1024] = {0};
#pragma data_seg()


ReplacePteOfKuserSharedData向驱动发送命令,读出7FFE0000处原来的1页数据到KuserSharedData,
替换pte。

BOOL CDriver::ReplacePteOfKuserSharedData()
{
  DWORD  dwBytesReturned = 0;
  DWORD  dwAddr = (DWORD)&KuserSharedData;

    
  if(INVALID_HANDLE_VALUE == m_hDevice)  // CreateFile驱动的返回
  {
    return FALSE;
  }
  
  // 不知道有无必要? 是想确保真正提交到物理页
    
  for(int i = 0; i <1024; i++)  
  {
    KuserSharedData[i] = i;
  }
      
  VirtualLock(KuserSharedData,1024 * 4);  // 锁定

  
  m_data[0] = (DWORD)(&KuserSharedData);  // 用来传递参数的array,这里只用1个dword

  // IOCTL_821

  return DeviceIoControl(
    m_hDevice,
    IOCTL_821,
    &dwAddr,    // 传递buffer在OllyDbg进程空间内的Virtual Address
    sizeof(DWORD) * 1,
    (PVOID)dwAddr,    // 返回debuggee原来KUSER_SHARED_DATA处的4KB数据
    sizeof(DWORD) * 1024,
    &dwBytesReturned,
    NULL);
  
}

RecoverPteOfKuserSharedData在被调试进程结束前调用,恢复被调试进程地址7FFE0000对应的原pte。
如果不执行,会导致蓝屏,错误为PFN_LIST_CORRUPTED(页帧号链表损坏,如果不用恢复原pte的做法,
应该怎样解决这个问题?我不知道:-(

BOOL CDriver::RecoverPteOfKuserSharedData()
{
  DWORD  dwBytesReturned = 0;

  VirtualUnlock(KuserSharedData,1024 * 4);

  return DeviceIoControl(
    m_hDevice,
    IOCTL_822,
    NULL,
    0,
    NULL,
    0,
    &dwBytesReturned,
    NULL);
}



下面是对应的驱动代码:

替换被调试进程7FFE0000对应的pte:

void ReplacePteOfKuserSharedData(ULONG FakedDataVA,PVOID OutputBuff)
{
  // 参数(来自ring3的DeviceIoControl):
  // FakedDataVA: 即OD内KuserSharedData的地址
  // OutputBuff:  指针,把被调试进程的页数据读到这里
  

  ULONG        addrOfPte = 0;
  ULONG        pte = 0;
  ULONG        addrOfObjPte = 0;

  PVOID        pEProcess = 0;
  NTSTATUS      ret = 0;

  PSLOOKUPPROCESSBYPROCESSID  PsLookupProcessByProcessId = 0;
  KEATTACHPROCESS      KeAttachProcess = 0;
  KEDETACHPROCESS      KeDetachProcess = 0;

  
  // _asm int 3

  // 下面的3个函数,ntoskrnl.exe输出了,但ddk不支持,要自己取

  PsLookupProcessByProcessId = (PSLOOKUPPROCESSBYPROCESSID)
      Ring0_GetProcAddress("PsLookupProcessByProcessId");

  KeAttachProcess = (KEATTACHPROCESS)
      Ring0_GetProcAddress("KeAttachProcess");

  KeDetachProcess = (KEDETACHPROCESS)
      Ring0_GetProcAddress("KeDetachProcess");

  
  // 此时运行在OD的context内

  addrOfPte = (FakedDataVA >> 12) * 4 + PAGE_TABLE_BASE;  // 计算OD提供的buffer对应的pte;
  pte = *((PULONG)addrOfPte);

  
  ret = PsLookupProcessByProcessId(objPid,&pEProcess);  // 获取被调试程序的EPROCESS
  KeAttachProcess(pEProcess);        // 等价于SoftIce的addr命令 ;-)

  RtlCopyMemory(OutputBuff,(PVOID)0x7FFE0000,1024 * 4);  // copy原来的数据

  addrOfObjPte = (0x7FFE0000 >> 12) * 4 + PAGE_TABLE_BASE; // 被调试程序空间内对应7FFE0000的pte虚拟地址

  savedPte = *((PULONG)addrOfObjPte);      // 保存这个值,恢复时使用

  *((PULONG)addrOfObjPte) &= 0x00000FFF;      // 保留标记
  *((PULONG)addrOfObjPte) |= (pte & 0xFFFFF000);    // 修改物理页地址

  KeDetachProcess();
}


恢复pte:

void RecoverPteOfKuserSharedData()
{
  PVOID        pEProcess = 0;
  NTSTATUS      ret = 0;

  PSLOOKUPPROCESSBYPROCESSID  PsLookupProcessByProcessId = 0;
  KEATTACHPROCESS      KeAttachProcess = 0;
  KEDETACHPROCESS      KeDetachProcess = 0;

  //

  PsLookupProcessByProcessId = (PSLOOKUPPROCESSBYPROCESSID)
      Ring0_GetProcAddress("PsLookupProcessByProcessId");

  KeAttachProcess = (KEATTACHPROCESS)
      Ring0_GetProcAddress("KeAttachProcess");

  KeDetachProcess = (KEDETACHPROCESS)
      Ring0_GetProcAddress("KeDetachProcess");

  
  ret = PsLookupProcessByProcessId(objPid,&pEProcess);  
  KeAttachProcess(pEProcess);    

  *(PULONG)((0x7FFE0000 >> 12) * 4 + PAGE_TABLE_BASE) = savedPte;
  
  KeDetachProcess();
}


写个程序调用GetTickCount测试一下,看起来不错。GetTickCount现在总返回同一个值(进程
开始运行时读出的值)。

到这里告一段落。有的壳(如SDP)在判断GetTickCount的差值时,结果为0也算错,所以现在的结果还不够。
可以用timer或thrad向KuserSharedData写入数据,但这样做不好掌握,什么值才能恰好骗过壳代码?

也可以考虑把被调试进程7FFE0000的pte的valid位改为无效,这样代码访问这个页时会产生
page fault。这样就不必用thread或timer,可以每次在处理页故障时赋值。问题是处理完后,
又该在什么时候重新将其设置为无效以等待下一次页故障? 也没想清楚。

对付GetTickCount似乎有点小题大作了,也是想学点东西。不过,在页表上做手脚绝对是威力
强大的。