[目录]
一. 前言
二. 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和被调试程序的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; }
挂钩此函数防止程序打开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); }
在程序保护中,当参数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); }
当进程被调试, 使用一个无效的句柄调用 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; }
此函数hook来做什么用的?不明真相中,那位能人异士知道的话恳求相告,感激不尽。
直接翻译代码
代码:
NTSTATUS New_NtYieldExecution() { Org_NtYieldExecution(); return STATUS_NO_YIELD_PERFORMED; }
此函数用于返回目标进程的各类信息。在软件保护中,参数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; }
每当一个应用程序被调试的时候,将会为调试对话在内核中创建一个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; } }
反调试程序可能会通过此函数获得并修改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); }
查询线程信息,可以由此获得线程所对应的进程
代码:
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; }
查询给定窗口句柄所属的进程
代码:
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; }
此函数用于枚举桌面上所有窗口的句柄,所以在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; }
此函数返回由窗口名或窗口类标识的窗口句柄。如果查询的是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文件参考一下。
欢迎拍砖!!!