[目录]

一. 前言
二. PhantOm 在驱动层的保护方式
三. 挂钩的SSDT函数
四. 挂钩的SSDT Shadow函数
五. 结语

[内容]

一. 前言

相信脱壳和破解界的朋友们大部分都用过PhantOm这个隐藏OD的插件,以前可以说是过TMD这
种加密壳的利器,它可以在应用层和驱动层对OD进行保护。貌似应用层的保护方式已经给某
强人分析过了,这里就不再重复了,现在分析一下PhantOm的驱动保护。拿来开刀的是
PhantOm V1.54版本的。

PhantOm插件的驱动文件在PhantOm.dll的资源中,在OD运行时自动从资源中释放驱动到临时
文件夹中,我们可以通过PE_Stud等PE工具进行提取。

二. PhantOm的驱动层保护方式

当一个程序被调试,驱动将会将OD和被调试程序的相关信息存储在一个结构中,并以双链表
的形式存放。其结构如下:

代码:
typedef struct _DEBUG_INFO
{
  struct _DEBUG_INFO *Prev;
  struct _DEBUG_INFO *Next;
  PLONG  Debugger_Id;  // OD的id号
  PEPROCESS  Debugger_Eprocess;  // OD的EPROCESS结构
  PEPROCESS  Debuggee_Eprocess;  // 被调试程序的EPROCESS结构
} DEBUG_INFO, *PDEBUG_INFO;
此结构非常重要,驱动中主要就是查询此结构来判断是否对OD进程进行保护。

当用OD加载或重新运行一个程序的时候,应用层将会传递OD和被调试程序的id号到驱动层中,
此时驱动程序将会查询双链表看相关的结构是否已经存在,不存在这创建一个节点并加入到
双链表中。
代码:
VOID SetDebugInfo (PVOID DebugIdBuf)
{
  NTSTATUS status;
  PEPROCESS pEprocess;
  PDEBUG_INFO NodeAddr, LastNode;
  ULONG Debugger_id, Debuggee_id;
  Debugger_id = *(PULONG)DebugIdBuf;
  Debuggee_id = *(PULONG)((ULONG)DebugIdBuf+1);
  if (DebugIdBuf == NULL)
    return;
  
  // 判断相关结构是否已经存在
  if ( !GetDebugInfoByPid(*(PULONG)DebugIdBuf)
  {
    // 建立OD与被调试程序的关联
    // 传入被调试程序的id
    status = PsLookupProcessByProcessId(Debuggee_id), 
      EprocessOfDebuggee);
    if (status == STATUS_SUCCESS)
    {
      Debug_Info.Debuggee_Eprocess = EprocessOfDebuggee;
      ObDereferenceObject(EprocessOfDebuggee);
    }
  }
  else   // 相关结构不存在
  {
    // 为节点分配空间
    NodeAddr = (PDEBUG_INFO)ExAllocatePool(PagedPool, sizeof(DEBUG_INFO));
    if (NodeAddr == NULL)
      return;
      
    RtlZeroMemory(NodeAddr, 16);
    // 查找链表最后一个节点
    LastNode = GetLast();
    if (LastNode)
    {
      // 非空链表, 插入到表尾
      LastNode->Next = NodeAddr;
      NodeAddr->Prev = LastNode;
    }
    else
      // 空链表,插入到表头
      DebugInfoHeader = NodeAddr;
    // 填充链表结构
    NodeAddr->Debugger_Id = Debugger_id;
    if (STATUS_SUCCESS == PsLookupProcessByProcessId(Debugger_id, pEprocess))
    {
      NodeAddr->Debugger_Eprocess = pEprocess;
      ObDereferenceObject(pEprocess);
    }
    if (STATUS_SUCCESS == PsLookupProcessByProcessId(Debuggee_id, pEprocess))
    {
      NodeAddr->Debuggee_Eprocess = pEprocess;
      ObDereferenceObject(pEprocess);
    }
  }
}

三. 挂钩的SSDT函数
PhantOm一共挂钩了9个与anti-debug相关的SSDT函数,分别列举如下:

(1) NtQuerySystemInformation
此函数的第一个参数SystemInformationClass是一个类型信息,可由它指定我们所查询的系统
信息。在anti-debug领域主要关注3种类型,分别是SystemProcessesAndThreadsInformation,
可通过此参数枚举所有进程;SystemKernelDebuggerInformation,是否存在windbg等内核调
试器;SystemHandleInformation,可用于枚举进程句柄。
所以我们需要hook这个函数,对以上三种情况进行处理。
代码:
NTSTATUS WINAPI New_NtQuerySystemInformation(
  __in       SYSTEM_INFORMATION_CLASS SystemInformationClass,
  __inout    PVOID SystemInformation,
  __in       ULONG SystemInformationLength,
  __out_opt  PULONG ReturnLength
  )
{
  NTSTATUS status;
  PEPROCESS cur_eprocess;
  PSYSTEM_PROCESSES system_process;
  PSYSTEM_PROCESSES system_process_prev;
  PSYSTEM_HANDLE_INFORMATION_EX handle_info;
  ULONG NextEntryDelta;
  ULONG handle_count, index, total_size;
  status = Org_NtQuerySystemInformation(
    SystemInformationClass,
    SystemInformation,
    SystemInformationLength,
    ReturnLength);
  if (status != STATUS_SUCCESS | IS_HOOK != TRUE)
    return status;

  cur_eprocess = IoGetCurrentProcess();
  // 当前进程是OD
  if (FindDebugInfoByEprocess(cur_eprocess))
    return status;

  switch (SystemInformationClass)
  {
  case SystemProcessesAndThreadsInformation:  // 5
    {
      system_process = (PSYSTEM_PROCESSES)SystemInformation;
      system_process_prev = system_process;
      NextEntryDelta = 0;
      do 
      {
        system_process = (PSYSTEM_PROCESSES)((ULONG)system_process + NextEntryDelta);
        if (GetDebugInfoByPid(system_process->ProcessId)) // 下面断链
        {
          // 非最后一项
          if (system_process->NextEntryDelta)
            system_process_prev->NextEntryDelta += system_process->NextEntryDelta;
          // 最后一项
          else
            system_process_prev->NextEntryDelta = 0;
          // 抹掉进程名字信息
          RtlZeroMemory(system_process->ProcessName.Buffer, 
            system_process->ProcessName.Length*2);
          // 然后将整个SYSTEM_PROCESS抹去
          total_size = system_process->ThreadCount << 6 + 0xB4;
          RtlZeroMemory(&system_process, total_size);
        }
        system_process_prev = system_process;
        NextEntryDelta = system_process->NextEntryDelta;
      } while (NextEntryDelta);
      system_process = (PSYSTEM_PROCESSES)SystemInformation;
      NextEntryDelta = 0;
      do 
      {
        system_process = (PSYSTEM_PROCESSES)((ULONG)system_process + NextEntryDelta);
        if (GetDebugInfoByPid(system_process->InheritedFromProcessId))
          // 如果父进程是OD,则将父进程的ID改为explorer.exe的ID号
          system_process->InheritedFromProcessId = EXPLORER_ID;
        NextEntryDelta = system_process->NextEntryDelta;
      } while (NextEntryDelta);
    }
    break;

  case SystemKernelDebuggerInformation:  // 35
    RtlZeroMemory(SystemInformation, SystemInformationLength);
    break;

  case SystemHandleInformation:  // 16
    {
      handle_info = (PSYSTEM_HANDLE_INFORMATION_EX)SystemInformation;
      handle_count = handle_info->NumberOfHandles;
      index = 0;
      while (handle_count)
      {
        if (GetDebugInfoByPid(handle_info->Information[index].ProcessId))
          handle_info->Information[index].ProcessId = EXPLORER_ID;
        handle_count--;
        index++;
      }
    }
    break;
  }

  return STATUS_SUCCESS;
}
(2) NtOpenProcess
挂钩此函数防止程序打开OD和csrss.exe的句柄
代码:
NTSTATUS  New_NtOpenProcess (    
             __out PHANDLE  ProcessHandle,    
             __in ACCESS_MASK  DesiredAccess,    
             __in POBJECT_ATTRIBUTES  ObjectAttributes,    
             __in_opt PCLIENT_ID  ClientId    
             )
{
  NTSTATUS status;
  if (IS_HOOK == TRUE && !FindDebugInfoByEprocess(IoGetCurrentProcess()))
  {
    status = STATUS_INVALID_PARAMETER;
    if (!MmIsAddressValid(ClientId) ||
      ClientId.UniqueProcess == CSRSS_ID ||
      GetDebugInfoByPid(ClientId->UniqueProcess))
      return status;
  }
  return Org_NtOpenProcess(ProcessHandle,
               DesiredAccess,
               ObjectAttributes,
               ClientId);
}
(3) NtSetInformationThread
在程序保护中,当参数ThreadInformationClass的值为ThreadHideFromDebugger(0x11)时,
此函数可以用来防止调试事件被发往调试器。
代码:
NTSTATUS
NTAPI
New_NtSetInformationThread(
               IN HANDLE               ThreadHandle,
               IN THREAD_INFORMATION_CLASS ThreadInformationClass,
               IN PVOID                ThreadInformation,
               IN ULONG                ThreadInformationLength 
               )
{
  NTSTATUS status;
  PVOID Object;
  if (IS_HOOK == TRUE)
  {
    status = ObReferenceObjectByHandle(ThreadHandle, 0, 0, KernelMode, Object, 0);
    if (status != STATUS_SUCCESS)
      return STATUS_INVALID_HANDLE;
    ObDereferenceObject(Object);
    if (!FindDebugInfoByEprocess(IoGetCurrentProcess()) &&
      ThreadInformationClass == ThreadHideFromDebugger)
      return STATUS_SUCCESS;
  }
  return Org_NtSetInformationThread(ThreadHandle,
                    ThreadInformationClass,
                    ThreadInformation,
                    ThreadInformationLength);
}
(4) NtClose
当进程被调试, 使用一个无效的句柄调用 NtClose 将会产生一个STATUS_INVALID_HANDLE
 (0xC0000008) 异常。
代码:
NTSTATUS 
NTAPI
New_NtClose(HANDLE ObjectHandle)
{
  PVOID Object;
  NTSTATUS status;
  if (IS_HOOK == FALSE)
    return Org_NtClose(ObjectHandle);
  // 防止使用无效句柄来检测调试器
  InterlockedIncrement(Lock_Number);
  status = ObReferenceObjectByHandle(ObjectHandle, 0, 0, 0, Object, 0);
  if (status == STATUS_SUCCESS)
  {
    ObDereferenceObject(Object);
    status = Org_NtClose(ObjectHandle);
  }
  else
    status = STATUS_SUCCESS;
  InterlockedDecrement(Lock_Number);
  return status;
}
(5) NtYieldExecution
此函数hook来做什么用的?不明真相中,那位能人异士知道的话恳求相告,感激不尽。
直接翻译代码
代码:
NTSTATUS New_NtYieldExecution()
{
  Org_NtYieldExecution();
  return STATUS_NO_YIELD_PERFORMED;
}
(6) NtQueryInformationProcess
此函数用于返回目标进程的各类信息。在软件保护中,参数ProcessInformationClass
有几个值必须特殊对待:
ProcessBasicInformation(0x0)--可检测目标进程的父进程,如果程序被调试,则
父进程是调试器,而对应一般窗口程序,其父进程是explorer.exe。
ProcessDebugPort(0x07) -- 如果目标进程正在被调试,系统会为进程分配一个调试
端口。通过此参数调用NtQueryInformationProcess则返回调试端口号,返回0表示当前
无调试器附在进程上。
ProcessDebugFlags(0x1f) -- 此时函数返回EPROCESS->NoDebugInherit域的值。为0
表示进程正处于调试状态。
代码:
NTSTATUS WINAPI New_NtQueryInformationProcess(
  __in       HANDLE ProcessHandle,
  __in       PROCESSINFOCLASS ProcessInformationClass,
  __out      PVOID ProcessInformation,
  __in       ULONG ProcessInformationLength,
  __out_opt  PULONG ReturnLength
)
{
  NTSTATUS status;
  PPROCESS_BASIC_INFORMATION ProcessBasicInfo;
  PEPROCESS pEprocess;
  status = Org_NtQueryInformationProcess(ProcessHandle,
    ProcessInformationClass,
    ProcessInformation,
    ProcessInformationLength,
    ReturnLength)
  if (STATUS_SUCCESS != status)
    return status;
  if (!FindDebugInfoByEprocess(IoGetCurrentProcess()))
  {
    // 调用此函数的进程不是OD
    if (ProcessInformationClass == ProcessBasicInformation)
    {
      // BrocessBasicInformation(0)
      ProcessBasicInfo = (PROCESS_BASIC_INFORMATION)ProcessInformation;
      // 检查此句柄的父进程是否为OD
      if (GetDebugInfoByPid(ProcessBasicInfo->InheritedFromUniqueProcessId))
        // 对于带窗口类进程,其父进程一般为explorer.exe,如果程序
        // 被调试,则其父进程为调试器。
        ProcessBasicInfo->InheritedFromUniqueProcessId = EXPLORER_ID;
      // 此句柄所属的进程是否为OD
      else if (GetDebugInfoByPid(ProcessBasicInfo->UniqueProcessId))
      {
        // 返回的信息清0
        RtlZeroMemory(ProcessInformation, sizeof(PROCESS_BASIC_INFORMATION));
        if (ReturnLength != NULL)
          // 返回长度置0
          *ReturnLength = 0;
      }
    }
    else
    {
      // 根据句柄得到进程对象
      if (ObReferenceObjectByHandle(ProcessHandle,
        0, 0, KernelMode, pEprocess, NULL) == STATUS_SUCCESS)
      {
        // 判断此进程是否是被调试
        if (GetDebuggeeByEprocess(pEprocess))
        {
          if (ProcessInformationClass == ProcessDebugPort 
            || ProcessInformationClass == ProcessDebugObjectHandle)
            //  调试端口信息或调试句柄清0
            *ProcessInformation = 0;
          // 它将返回EPROCESS->NoDebugInherit的值,
          // 当调试器存在的时候,其值为FALSE,表示
          // 进程正在被调试
          else if (ProcessInformationClass == ProcessDebugFlags)
          {
            if (ProcessInformation != NULL)
              *(PDWORD)(ProcessInformation) = TRUE;
          }
        }
        ObDereferenceObject(pEprocess);
      }
    }
  }
  return status;
}
(7) NtQueryObject
每当一个应用程序被调试的时候,将会为调试对话在内核中创建一个DebugObject类型的对象。
程序可以检查DebugObject10类型内核对象的数量来确定是否有调试器的存在。DebugObject的
数量可以通过NtQueryObject函数来检索所有对象类型的信息获得,此时需要将
ObjectInformationClass的参数设为ObjectAllTypeInformation(0x03)。PhantOm在内核中的
解决方法是hook此函数,如果发现检索的是ObjectAllTypeInfomation类型则抹掉名称为
"DebugObject"的调试对象的相关信息。
代码:
NTSTATUS New_NtQueryObject(
               __in_opt   HANDLE Handle,
               __in       OBJECT_INFORMATION_CLASS ObjectInformationClass,
               __out_opt  PVOID ObjectInformation,
               __in       ULONG ObjectInformationLength,
               __out_opt  PULONG ReturnLength
               )
{
  NTSTATUS status;
  POBJECT_ALL_INFORMATION pObjectAllInfo;
  ULONG NumObjects;
  status = Org_NtQueryObject(Handle,
                 ObjectInformationClass,
                 ObjectInformation,
                 ObjectInformationLength,
                 ReturnLength);
  if (IS_HOOK == FALSE || ObjectInformation == NULL)
    return status;

  //判断当前进程是否为OD
  if (!FindDebugInfoByEprocess(IoGetCurrentProcess()))
    return status;

  switch (ObjectInformationClass)
  {
  case ObjectTypeInformation:
    // 把返回信息全部清0,一了百了
    RtlZeroMemory(ObjectInformation, ObjectInformationLength);
    break;

    // 如果是检索所以对象类型信息
  case ObjectAllInformation:
    {
      pObjectAllInfo = (POBJECT_ALL_INFORMATION)ObjectInformation;
      PUCHAR pObjInfo = (PUCHAR)pObjectAllInfo->ObjectTypeInformation;
      NumObjects = pObjectAllInfo->NumberOfObjectsTypes;
      for (UINT i = 0; i < NumObjects; i++)
      {
        POBJECT_TYPE_INFORMATION pObjectTypeInfo = 
          (POBJECT_TYPE_INFORMATION)pObjInfo;
        ULONG TypeNameLength = pObjectTypeInfo->TypeName.Length;
        PUCHAR TypeNameBuffer = pObjectTypeInfo->TypeName.Buffer;

        // 检查debug object是否存在
        if (TypeNameLength == 0x16)
        {
          if (wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0)
            *pObjectTypeInfo = NULL;
        }

        pObjInfo = TypeNameBuffer;
        // 加上字符串的长度
        pObjInfo += TypeNameLength;
        // 双字节对齐
        ULONG tmp = (ULONG)pObjInfo & 0xFFFFFFFC;
        pObjInfo = ((PUCHAR)tmp + sizeof(ULONG));
      }
    }
    break;
  }
}
(8) NtSetContextThread
反调试程序可能会通过此函数获得并修改CPU中调试寄存器的内容,所以如果程序下了
硬件断点的话就会失效,并且可以检测到调试器的存在。我们可以检查ContextFlags
中的调试寄存器组的标志位,看程序是否在查询调试寄存器。
代码:
NTSTATUS
NTAPI
New_NtSetContextThread(IN HANDLE ThreadHandle,
             IN PCONTEXT Context)
{
  if (IS_HOOK == TRUE)
  {
    if (!FindDebugInfoByEprocess(IoGetCurrentProcess()) && 
      MmIsAddressValid(Context))
      // 清除ContextFlags中查询调试寄存器组的标志位
      Context->ContextFlags & ~CONTEXT_DEBUG_REGISTERS;
  }
  return Org_NtSetContextThread(ThreadHandle, Context);
}
(9) NtQueryInformationThread
查询线程信息,可以由此获得线程所对应的进程

代码:
NTSTATUS NTAPI New_NtQueryInformationThread(
  IN HANDLE               ThreadHandle,
  IN THREAD_INFORMATION_CLASS ThreadInformationClass,
  OUT PVOID               ThreadInformation,
  IN ULONG                ThreadInformationLength,
  OUT PULONG              ReturnLength OPTIONAL 
  )
{
  NTSTATUS status;
  PTHREAD_BASIC_INFORMATION ThreadBasicInfo;
  status = Org_NtQueryInformationThread(ThreadHandle,
    ThreadInformationClass,
    ThreadInformation,
    ThreadInformationLength,
    ReturnLength);

  if (status == STATUS_SUCCESS && IS_HOOK == 1)
  {
    ThreadBasicInfo = (PTHREAD_BASIC_INFORMATION)ThreadInformation;
    // 获得线程对应的进程ID,并判断是否在查询OD进程
    if (GetDebugInfoByPid(ThreadBasicInfo->ClientId.UniqueProcess))
    {
      RtlZeroMemory(ThreadInformation, sizeof(THREAD_BASIC_INFORMATION));
      if (ReturnLength != NULL)
        *ReturnLength = 0;
      status = STATUS_ACCESS_DENIED;
    }
  }

  return status;
}

四. 挂钩的SSDT Shadow函数
PhantOm挂钩了4个SSDT Shadow函数,主要用于防止程序检测到OD的窗口

(1) NtUserGetForegroundWindow
此函数返回用户正在工作的窗口的句柄,如果程序被调试,则顶层窗口是OD的窗口。
代码:
ULONG New_NtUserGetForegroundWindow(VOID)
{
  NTSTATUS status;
  status = Org_NtUserGetForegroundWindow();

  if (IS_HOOK == TRUE)
  {
    // 判断所查询的窗口是否为OD
    if (GetDebugInfoByPid(status))
    {
      // 判断当前进程是否为OD
      if (!FindDebugInfoByEprocess(IoGetCurrentProcess()))
        status = LastForegroundWindow;
      else
        LastForegroundWindow = status;
    }
  }

  return status;
}
(2) NtUserQueryWindow
查询给定窗口句柄所属的进程
代码:
INT_PTR New_NtUserQueryWindow(
                IN ULONG WindowHandle,
                IN ULONG TypeInformation)
{
  ULONG ProcessID;

  // 查询窗口句柄所属的进程
  ProcessID = Org_NtUserQueryWindow(WindowHandle, 0);

  if (ProcessID && IS_HOOK == TRUE)
  {
    // 查询窗口句柄是否属于OD
    if(!GetDebugInfoByPid(ProcessID))
    {
      if (!FindDebugInfoByEprocess(IoGetCurrentProcess()))
        return 0;
    }
  }

  return ProcessID;
}
(3) NtUserBuildHwndList
此函数用于枚举桌面上所有窗口的句柄,所以在hook程序中,我们必须在返回值结构中把
OD所属的句柄抹掉。
代码:
NTSTATUS New_NtUserBuildHwndList(
                   IN HDESK hdesk, 
                   IN HWND hwndNext, 
                   IN ULONG fEnumChildren, 
                   IN DWORD idThread, 
                   IN UINT cHwndMax, 
                   OUT HWND *phwndFirst, 
                   OUT ULONG* pcHwndNeeded)
{
  NTSTATUS status;
  ULONG i, ProcessId;
  if (fEnumChildren == 1 && IS_HOOK == TRUE)
  {
    ProcessId = Org_NtUserQueryWindow((ULONG)hwndNext, 0);
    if (GetDebugInfoByPid(ProcessId))
    {
      if (!FindDebugInfoByEprocess(IoGetCurrentProcess())
        return STATUS_UNSUCCESSFUL;
    }
  }

  status = Org_NtUserBuildHwndList(hdesk, 
                   hwndNext, 
                   fEnumChildren, 
                   idThread, 
                   cHwndMax, 
                   phwndFirst, 
                   pcHwndNeeded);

  if (status == STATUS_SUCCESS)
  {
    if (IS_HOOK == 1 && !FindDebugInfoByEprocess(IoGetCurrentProcess()))
    {
      i = 0;
      while (i < *pcHwndNeeded)
      {
        // 获取句柄所属的进程ID
        ProcessId = Org_NtUserQueryWindow((ULONG)phwndFirst[i], 0);
        // 判断句柄是否属于OD
        if (GetDebugInfoByPid(ProcessId))
        {
          // 将此后的句柄信息前移,覆盖掉OD的句柄信息
          RtlCopyMemory(&phwndFirst[i+1], &phwndFirst[i], *pcHwndNeeded-i);
          // 最后一项清0
          phwndFirst[*pcHwndNeeded-1] = 0;
          // 总数减1
          (*pcHwndNeeded)--;
          continue; 
        }
        i++;
      }
    }
  }

  return status;
}
(4) NtUserFindWindowEx
此函数返回由窗口名或窗口类标识的窗口句柄。如果查询的是OD窗口,则返回0
代码:
NTSTATUS New_NtUserFindWindowEx(
                  IN HWND hwndParent, 
                  IN HWND hwndChild, 
                  IN PUNICODE_STRING pstrClassName OPTIONAL, 
                  IN PUNICODE_STRING pstrWindowName OPTIONAL, 
                  IN DWORD dwType)
{
  NTSTATUS status;

  status = Org_NtUserFindWindowEx(hwndParent, hwndChild, 
    pstrClassName, pstrWindowName, dwType);

  if (status == 0 || IS_HOOK == FALSE)
    return status;

  // 查询此窗口所属的进程的ID
  ProcessId = Org_NtUserQueryWindow(status, 0);
  // 判断窗口是否属于OD
  if (GetDebugInfoByPid(ProcessId))
  {
    // 当前进程是否为OD
    if (!FindDebugInfoByEprocess(IoGetCurrentProcess()))
      return 0;
  }

  return status;
}
五. 结语
很多朋友问PhantOm可不可以调NP或TexSafe保护的游戏,看看上面的程序就一目了然了,
很明显不能。PhantOm在驱动层只是做了很基本的服务函数表的hook,用很简单的ssdt表
还原它的功能就消失了。而NP这些猥琐东西不但inline hook了n个函数,还会采用各种
手段检测游戏运行中有无异常情况发生,如果有它觉得不爽的情况就给你一个BSOD。
但是用它来对付运行在ring3层的程序也绰绰有余了。

上面的代码均未经过测试,直接看汇编代码翻译过来的,有错误是必然的。偶很懒,
而且BSOD的话会很心疼偶滴电脑,想想还是不调代码了。如果同志们计划打造一个山寨版
的PhantOm驱动,最好经过严格修改并加上适当的错误处理,不然你的电脑会蓝得很难看。


附件是IDA文件,加了详细的注解,这个逆得比较彻底,100%全逆。
还有很多函数未列出来,有兴趣的朋友可以根据IDA文件参考一下。

欢迎拍砖!!!
上传的附件 PhantOm1.54.rar