检测隐藏进程
最困难的是查找KiDispatcherReadyListHead。主要的问题是KiDispatcherReadyListHead的地址并没有被任何一个导出的函数使用。因此就要
用更加复杂的搜索算法搞定它。就从KiDispatchInterrupt函数开始,我们感兴趣的地方只有这里:
Code:
.text:00404E72 mov byte ptr [edi+50h], 1
.text:00404E76 call sub_404C5A
.text:00404E7B mov cl, 1
.text:00404E7D call sub_404EB9
这段代码中的第一个函数调用指向的就是包含KiDispatcherReadyListHead引用的函数。尽管如此,搜索KiDispatcherReadyListHead的地址却
变的更加复杂,因为这个函数的相关代码在Windows XP SP1和SP2中是不同的。在SP2中它是这个样子:
Code:
.text:00404CCD add eax, 60h
.text:00404CD0 test bl, bl
.text:00404CD2 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:00404CD9 jnz loc_401F12
.text:00404CDF mov esi, [edx+4]
And in SP1:
SP1中是这样的:
Code:
.text:004180FE add eax, 60h
.text:00418101 cmp [ebp+var_1], bl
.text:00418104 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:0041810B jz loc_418760
.text:00418111 mov esi, [edx]
仅仅查找一个“lea”指令是不可靠的,因此我们也应该检查“lea”后面的指令(LDasm中的IsRelativeCmd函数)。搜索
KiDispatcherReadyListHead的全部代码如下:
Code:
void XPGetKiDispatcherReadyListHead()
{
PUCHAR cPtr, pOpcode;
PUCHAR CallAddr = NULL;
ULONG Length;
for (cPtr = (PUCHAR)KiDispatchInterrupt;
cPtr < (PUCHAR)KiDispatchInterrupt + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) return;
if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)
{
CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);
break;
}
}
if (!CallAddr || !MmIsAddressValid(CallAddr)) return;
for (cPtr = CallAddr; cPtr < CallAddr + PAGE_SIZE; cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) return;
if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))
{
KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);
break;
}
}
return;
}
找到线程链表地址之后我们就可以非常简单地枚举出那些进程了,代码如下:
Code:
void ProcessListHead(PLIST_ENTRY ListHead)
{
PLIST_ENTRY Item;
if (ListHead)
{
Item = ListHead->Flink;
while (Item != ListHead)
{
CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
Item = Item->Flink;
}
}
return;
}
CollectProcess是一个非常有用的函数,它可以增加一个进程到进程列表中去。
通过拦截系统调用得到进程列表。
任何一个进程都要通过API来和系统进行交互,而大多数交互都通过系统调用传递给了内核。当然,进程也可以不使用任何API而存在,但是这
样一来它也就不能做任何有用(或有害)的事情。一般而言,我们的思路是使用系统调用管理器拦截系统调用,然后得到管理器中当前进程的
EPROCESS指针。应该在某段时间收集指针列表,这个表不会包含信息收集时没有使用任何系统调用的进程(比如,进程的线程都处于等待状态
)。
Windows 2000平台使用2Eh中断进行系统调用,因此我们需要修改IDT中的相应的中断描述符来拦截系统调用,这就要用sidt指令得到IDT在内存
中的位置。该指令返回这样一个结构:
Code:
typedef struct _Idt
{
USHORT Size;
ULONG Base;
} TIdt;
修改2Eh中断向量的代码如下:
Code:
void Set2kSyscallHook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, NewSyscall
mov ebx, Idt.Base
xchg [ebx + 0x170], si
rol esi, 0x10
xchg [ebx + 0x176], si
ror esi, 0x10
mov OldSyscall, esi
sti
popad
}
}
当然在卸载驱动之前还要保存原始状态的信息:
Code:
void Win2kSyscallUnhook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, OldSyscall
mov ebx, Idt.Base
mov [ebx + 0x170], si
rol esi, 0x10
mov [ebx + 0x176], si
sti
xor eax, eax
mov OldSyscall, eax
popad
}
}
Windows XP使用sysenter/sysexit指令(出现在Pentium 2处理器中)实现系统调用。这些指令的功能由model-specific registers(MSR)控制
。系统调用管理器的地址保存在MSR寄存器,SYSENTER_EIP_MSR(0x176)中。用rdmsr指令读取MSR寄存器,同时设置ECX = 要读取的寄存器的号
码,结果保存在两个积存器EDX:EAX中。在我们这里,SYSENTER_EIP_MSR积存器是32位积存器,所以EDX为0,EAX内是系统调用管理器的地址。
同样地,我们也可以用wrmsr指令写MSR积存器。有一个地方需要注意:当写32位MSR积存器的时候,EDX应该被清空,否则将引起异常并且导致
系统立即崩溃。
考虑到所有的事情之后,替代系统调用管理器的代码如下:
Code:
void SetXpSyscallHook()
{
__asm
{
pushad
mov ecx, 0x176
rdmsr
mov OldSyscall, eax
mov eax, NewSyscall
xor edx, edx
wrmsr
popad
}
}
恢复原始的系统调用管理器代码:
Code:
void XpSyscallUnhook()
{
__asm
{
pushad
mov ecx, 0x176
mov eax, OldSyscall
xor edx, edx
wrmsr
xor eax, eax
mov OldSyscall, eax
popad
}
}
Windows XP的另外一个特性是它既可以使用sysenter也可以使用int 2Eh来进行系统调用,所以我们要替换这两种情况下的系统调用管理器。
用更加复杂的搜索算法搞定它。就从KiDispatchInterrupt函数开始,我们感兴趣的地方只有这里:
Code:
.text:00404E72 mov byte ptr [edi+50h], 1
.text:00404E76 call sub_404C5A
.text:00404E7B mov cl, 1
.text:00404E7D call sub_404EB9
这段代码中的第一个函数调用指向的就是包含KiDispatcherReadyListHead引用的函数。尽管如此,搜索KiDispatcherReadyListHead的地址却
变的更加复杂,因为这个函数的相关代码在Windows XP SP1和SP2中是不同的。在SP2中它是这个样子:
Code:
.text:00404CCD add eax, 60h
.text:00404CD0 test bl, bl
.text:00404CD2 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:00404CD9 jnz loc_401F12
.text:00404CDF mov esi, [edx+4]
And in SP1:
SP1中是这样的:
Code:
.text:004180FE add eax, 60h
.text:00418101 cmp [ebp+var_1], bl
.text:00418104 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:0041810B jz loc_418760
.text:00418111 mov esi, [edx]
仅仅查找一个“lea”指令是不可靠的,因此我们也应该检查“lea”后面的指令(LDasm中的IsRelativeCmd函数)。搜索
KiDispatcherReadyListHead的全部代码如下:
Code:
void XPGetKiDispatcherReadyListHead()
{
PUCHAR cPtr, pOpcode;
PUCHAR CallAddr = NULL;
ULONG Length;
for (cPtr = (PUCHAR)KiDispatchInterrupt;
cPtr < (PUCHAR)KiDispatchInterrupt + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) return;
if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)
{
CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);
break;
}
}
if (!CallAddr || !MmIsAddressValid(CallAddr)) return;
for (cPtr = CallAddr; cPtr < CallAddr + PAGE_SIZE; cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) return;
if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))
{
KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);
break;
}
}
return;
}
找到线程链表地址之后我们就可以非常简单地枚举出那些进程了,代码如下:
Code:
void ProcessListHead(PLIST_ENTRY ListHead)
{
PLIST_ENTRY Item;
if (ListHead)
{
Item = ListHead->Flink;
while (Item != ListHead)
{
CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
Item = Item->Flink;
}
}
return;
}
CollectProcess是一个非常有用的函数,它可以增加一个进程到进程列表中去。
通过拦截系统调用得到进程列表。
任何一个进程都要通过API来和系统进行交互,而大多数交互都通过系统调用传递给了内核。当然,进程也可以不使用任何API而存在,但是这
样一来它也就不能做任何有用(或有害)的事情。一般而言,我们的思路是使用系统调用管理器拦截系统调用,然后得到管理器中当前进程的
EPROCESS指针。应该在某段时间收集指针列表,这个表不会包含信息收集时没有使用任何系统调用的进程(比如,进程的线程都处于等待状态
)。
Windows 2000平台使用2Eh中断进行系统调用,因此我们需要修改IDT中的相应的中断描述符来拦截系统调用,这就要用sidt指令得到IDT在内存
中的位置。该指令返回这样一个结构:
Code:
typedef struct _Idt
{
USHORT Size;
ULONG Base;
} TIdt;
修改2Eh中断向量的代码如下:
Code:
void Set2kSyscallHook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, NewSyscall
mov ebx, Idt.Base
xchg [ebx + 0x170], si
rol esi, 0x10
xchg [ebx + 0x176], si
ror esi, 0x10
mov OldSyscall, esi
sti
popad
}
}
当然在卸载驱动之前还要保存原始状态的信息:
Code:
void Win2kSyscallUnhook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, OldSyscall
mov ebx, Idt.Base
mov [ebx + 0x170], si
rol esi, 0x10
mov [ebx + 0x176], si
sti
xor eax, eax
mov OldSyscall, eax
popad
}
}
Windows XP使用sysenter/sysexit指令(出现在Pentium 2处理器中)实现系统调用。这些指令的功能由model-specific registers(MSR)控制
。系统调用管理器的地址保存在MSR寄存器,SYSENTER_EIP_MSR(0x176)中。用rdmsr指令读取MSR寄存器,同时设置ECX = 要读取的寄存器的号
码,结果保存在两个积存器EDX:EAX中。在我们这里,SYSENTER_EIP_MSR积存器是32位积存器,所以EDX为0,EAX内是系统调用管理器的地址。
同样地,我们也可以用wrmsr指令写MSR积存器。有一个地方需要注意:当写32位MSR积存器的时候,EDX应该被清空,否则将引起异常并且导致
系统立即崩溃。
考虑到所有的事情之后,替代系统调用管理器的代码如下:
Code:
void SetXpSyscallHook()
{
__asm
{
pushad
mov ecx, 0x176
rdmsr
mov OldSyscall, eax
mov eax, NewSyscall
xor edx, edx
wrmsr
popad
}
}
恢复原始的系统调用管理器代码:
Code:
void XpSyscallUnhook()
{
__asm
{
pushad
mov ecx, 0x176
mov eax, OldSyscall
xor edx, edx
wrmsr
xor eax, eax
mov OldSyscall, eax
popad
}
}
Windows XP的另外一个特性是它既可以使用sysenter也可以使用int 2Eh来进行系统调用,所以我们要替换这两种情况下的系统调用管理器。
0
相关文章