菜鸟作品,大牛请无视。如有错误或纰漏,望指出
之前看过很多关于进程创建的拦截,都是勾在NtCreateProcess或者NtCreateSection上,拦截到的往往都是Explorer进程中的线程,此时如果将线程卡在内核中,就会导致桌面卡死。这不是我想要的。
百度GOOGLE了好久,没找到类似的解决办法,逆又逆不出来,就只有自己翻书动脑子了(不知道各大安全卫士是怎么实现的)。
既然不能卡死Explorer的线程,那咱就卡别人去,卡谁?我想你已经知道了被创建者的主线程
相关知识:
每一个线程都是从内核空间的KiThreadStartup开始运行的,线程在被创建的时候就指定了它在系统空间的上下文以及返回用户空间的自陷框架。在KiThreadStartup中,线程将运行级别降低至APC_LEVEL后调用PspUserThreadStartup(如果是系统线程,这里调用的是PspSystemThreadStartup),PspUserThreadStartup将用户空间ntdll.dll中的函数LdrInitializeThunk作为APC函数挂入APC队列。当线程返回用户空间时,就会检测到APC函数的存在,并先加以执行,直到APC队列中不再有请求时才算正式回到用户空间。
回到用户空间的什么地方呢?这要看线程被创建的时候设定的自陷框架了。如果是主线程,则回到BaseProcessStartup,非主线程则是BaseThreadStartup。至于用户空间给定的线程入口(如主线程的OEP),则存放在寄存器EAX中,作为参数调用BaseProcessStartup或者BaseThreadStartup,再由这二者之一将其放在一个SEH保护域中加以调用。
实现:
如果上面看晕了,没关系,只需知道每个线程都是在KiThreadStartup中开始运行的,如果是用户线程,则会调用PspUserThreadStartup。注意此时的IRQL为APC_LEVEL。
而我选择的挂钩地点就是PspUserThreadStartup。E9跳转需要5个字节,我选择在PspUserThreadStartup+2的位置JMP,即
kd> u PspUserThreadStartup nt!PspUserThreadStartup: 805c7050 6a20 push 20h 805c7052 6870ae4d80 push offset nt!ObWatchHandles+0x61c (804dae70) 805c7057 e8c41ef7ff call nt!_SEH_prolog (80538f20) 805c705c 64a124010000 mov eax,dword ptr fs:[00000124h] 805c7062 8bf0 mov esi,eax 805c7064 8975e0 mov dword ptr [ebp-20h],esi 805c7067 8b7e44 mov edi,dword ptr [esi+44h] 805c706a 897ddc mov dword ptr [ebp-24h],edi
相关函数:
程序中的g_uStartSign,是用来判断线程是否是进程的主线程的。它记录的是线程在用户空间的开始地址,如果是主线程,它就是BaseProcessStartup,非主线程就是BaseThreadStartup。ETHREAD结构中有这个值。g_uStartSign的赋值是通过创建一个线程(肯定不是主线程)调用DeviceIoControl,然后通过ETHREAD活得,这个值是BaseThreadStartup的地址。BaseProcessStartup和BaseThreadStartup是kernel32.dll中的函数,kernel32.dll在每个进程中映射的位置都相同,因此这两个函数的地址也在每个进程中也都相同。
kd> dt _ETHREAD ntdll!_ETHREAD ... +0x224 StartAddress : Ptr32 Void ...
注意:BaseProcessStartup或BaseThreadStartup是创建者进程在用户空间通过BasepInitializeContext设置的,如果以R0下安全,R3下不安全来算,这么判断是否主线程是不严谨的。这是学习的时候的一个测试,后面没改回来。
我测试过,如果把主线程的自陷框架中的StartAddress从BaseProcessStartup改成BaseThreadStartup,进程一样能运行起来,而把非主线程改成BaseProcessStartup,这个线程却运行不起来了。。
这里还有一个问题。尽管主线程拦住了,但是该进程中的EXE文件映像和ntdll.dll都已经映射完毕,如果这时候创建一个远程线程,并将线程入口指定为OEP,程序一样能运行起来。因此,我们要拦截的不止是主线程(第一个线程),而是该进程所有的线程,但是不可能每个线程都要给用户选择一遍。。
我的处理方法是维护一张进程PID表,如果有新创建的进程等待用户选择的时候,将其PID添加到表中。之后如果有线程创建,则遍历这张PID表,如果线程所属的进程正在等待用户选择,则让该线程在一个通知事件对象g_eventNotify上等待,否则放行。如果用户允许该进程运行,则在PID表中将该进程PID删除,再通知等待在事件对象g_eventNotify上等待的所有线程,这些线程判断它们所属的进程是已经允许运行还是在等待判断,从而放其运行或者继续等待。注意同步的问题。
具体程序请看附件。我加了注释。
/************************************************************************/ /* 获取信息,并根据用户的选择结束进程或者允许运行 */ /* arg[0] 和 arg[1] 分别为PspUserThreadStartup 的两个参数 */ /* arg[0] = StartRoutine, arg[1] = StartContext */ /************************************************************************/ void DoSomethingIWant(PULONG arg) { ULONG dwEProcess = (ULONG)PsGetCurrentProcess(); ANSI_STRING ansiCurrentProcessName,ansiPerentProcessName; ULONG uRet = 0; ULONG Pid, PerentPid; KSPIN_LOCK SpinLock; KIRQL kirql; PLIST_ENTRY head,curr; ULONG n; if (g_uStartSign == 0) //没准备好? { return; } Pid = (ULONG)PsGetCurrentProcessId(); PerentPid = *(PULONG)(dwEProcess + OFFSET_PERENT_PID_EPROCESS);//通过偏移从EPROCESS中获取父进程ID,偏移为硬编码 KeInitializeSpinLock(&SpinLock); if (g_uStartSign != arg[1]) //是否为进程的第一个线程 { //添加PID到PidTable中 KeAcquireSpinLock(&SpinLock,&kirql); SetPidTable(Pid, 1); KeReleaseSpinLock(&SpinLock,kirql); //取当前进程名 GetProcessPathName(dwEProcess,&ansiCurrentProcessName); //取父进程名 GetProcessPathNameByPID(PerentPid, &ansiPerentProcessName); //打印信息~~ DbgPrint("[ProcessMon]Process Creating, Process Name = %s ,PID = %d ,PerentName = %s PID = %d\r\n", ansiCurrentProcessName.Buffer,Pid, ansiPerentProcessName.Buffer,PerentPid); //让用户选择 uRet = GetUserChoose(&ansiPerentProcessName,&ansiCurrentProcessName, PerentPid, Pid); //从PidTable中删除PID KeAcquireSpinLock(&SpinLock,&kirql); SetPidTable(Pid, 0); KeReleaseSpinLock(&SpinLock,kirql); //释放内存了. if(ansiCurrentProcessName.Length) RtlFreeAnsiString(&ansiCurrentProcessName); if(ansiPerentProcessName.Length) RtlFreeAnsiString(&ansiPerentProcessName); //根据用户的选择允许运行或者结束进程 if(uRet) ZwTerminateProcess(NtCurrentProcess(), 0); KeSetEvent(&g_eventNotify, 0, 0); //通知那些等待的线程--------------------- KeClearEvent(&g_eventNotify); //置为非信号----------------------------| } // | else // | { // | //不是第一个线程 | while (IsPidInTable(Pid)) //如果线程所属进程在等待用户判断,则让其等待 | { // | KeWaitForSingleObject(&g_eventNotify,Executive,KernelMode,0,0); // <----- } //通过EPROCESS->ThreadListHead遍历线程,ThreadListHead的偏移量为硬编码 for (n = 0, curr = head = (PLIST_ENTRY)(dwEProcess + OFFSET_THREAD_LIST_HEAD_EPROCESS); curr->Blink != head ; curr = curr->Blink, n++){} //计算当前进程的线程数目 DbgPrint("[ProcessMon]Pid: %.4d ,Thread count = %d\r\n",Pid, n); } return; } /************************************************************************/ /* 自己假冒的函数 保存现场后CALL DoSomethingIWant */ /************************************************************************/ __declspec(naked) void FakePspUserThreadStartup() { __asm{ pushfd pushad mov ebx,esp add ebx,44 push ebx call DoSomethingIWant popad popfd jmp g_Orig } } /************************************************************************/ /* 挂钩PspUserThreadStartup */ /* 挂钩的地址是PspUserThreadStartup+2 */ /************************************************************************/ void HookPspUserThreadStartup() { PUCHAR pHookAddr = (PUCHAR)FindPspUserThreadStartupAddress(); UCHAR JMPCode[5] = {0xe9,0,0,0,0}; UCHAR JMPBackCode[5] = {0xe9,0,0,0,0}; g_Orig = ExAllocatePool(NonPagedPool,10); if (!g_Orig) { DbgPrint("[ProcessMon]Failed with Allocate Pool\r\n"); return ; } if (!pHookAddr) return; pHookAddr += 2; //挂钩的地址是PspUserThreadStartup+2 *((PULONG)(JMPCode+1)) = (ULONG) FakePspUserThreadStartup - ((ULONG)pHookAddr + 5); *((PULONG)(JMPBackCode+1)) = (ULONG)pHookAddr + 5 - ((ULONG)g_Orig + 10); memcpy( g_Orig, (PVOID)pHookAddr, 5); memcpy( (PVOID)((ULONG)g_Orig+5), (PVOID)JMPBackCode, 5); __asm{ cli mov eax,cr0 and eax,not 10000h mov cr0,eax } memcpy((PVOID)pHookAddr,JMPCode,5); __asm{ mov eax,cr0 or eax,10000h mov cr0,eax sti } }
进程和父进程信息的获取主要来源于EPROCESS结构。
PspUserThreadStartup不是一个导出函数,需要对其定位。这里用的是搜索内存。先从SSDT中找到NtCreateThread,再从NtCreateThread中找到PspCreateThread,最后再通过PspCreateThread找PspUserThreadStartup
函数如下:
/************************************************************************/ /* 取PspUserThreadStartup函数的地址 */ /************************************************************************/ PVOID FindPspUserThreadStartupAddress() { ULONG AddrNtCreateThread = 0; ULONG AddrPspCreateThread = 0; ULONG AddrPspUserThreadStartup = 0; ULONG i; UCHAR code1[29] = "\x52\x52\xff\x75\x24\x8d\x45\xc8\x50\xff\x75\x1c\xff\x75\x18\x52\xff\x75\x14\xff\x75\x10\xff\x75\x0c\xff\x75\x08\xe8"; UCHAR code2[15] = "\xff\x75\xe4\xff\x75\x20\xff\xb6\x24\x02\x00\x00\x6a\x00\x68"; //通过索引从SSDT中获得NtCreateThread的地址,索引为硬编码 AddrNtCreateThread = KeServiceDescriptorTable.ServiceTableBase[INDEX_OF_NTCREATETHREAD]; DbgPrint("[ProcessMon]AddrNtCreateThread = 0x%.8X\r\n",AddrNtCreateThread); //先从NtCreateThread中活得PspCreateThread的地址, for (i=0; i<0x1000; i++){ if (memcmp((PVOID)(AddrNtCreateThread+i),code1,29) == 0){ AddrPspCreateThread = AddrNtCreateThread + i + 29; AddrPspCreateThread = AddrPspCreateThread + 4 + *(PULONG)AddrPspCreateThread; DbgPrint("[ProcessMon]AddrPspCreateThread = 0x%.8X\r\n",AddrPspCreateThread); break; } } //没找到 if (AddrPspCreateThread == 0){ return NULL; } //再从PspCreateThread中活得PspUserThreadStartup的地址, for (i=0; i<0x1000; i++){ if (memcmp((PVOID)(AddrPspCreateThread+i),code2,15) == 0){ AddrPspUserThreadStartup = *(PULONG)(AddrPspCreateThread + i + 15); DbgPrint("[ProcessMon]AddrPspUserThreadStartup = 0x%.8X\r\n",AddrPspUserThreadStartup); break; } } return (PVOID)AddrPspUserThreadStartup; }
注意:
(1)此程序以测试为目的,用到了硬编码,不利于移植。此程序在XP sp3下测试通过,没在别的平台上测试过。主要用到的硬编码:(见sys\ProcessMon.c)
//硬编码 #define INDEX_OF_NTCREATETHREAD 0x35 //NtCreateProcess在SSDT中的偏移 #define OFFSET_PERENT_PID_EPROCESS 0x14c //父进程PID在EPROCESS中的偏移 #define OFFSET_THREAD_LIST_HEAD_EPROCESS 0x190 //ThreadListHead在EPROCESS中的偏移 #define OFFSET_START_ADDRESS_ETHREAD 0x224 //StartAddress在ETHREAD中的偏移
(2)这里对于线程是不是主线程的判断是不严谨的。BaseProcessStartup或BaseThreadStartup是创建者进程在用户空间通过BasepInitializeContext设置的,如果以R0下安全,R3下不安全来算,这么判断是否主线程是不严谨的。这是学习的时候的一个测试,后面没改回来。
此外,大四了,快要去找工作了,今后未必有时间慢慢看书,慢慢些帖子了。特此感谢看雪,感谢那些在论坛上奉献的大小牛们。我们的成长离不开你们的帮助。