能力值:
( LV12,RANK:400 )
25 楼
以下言论只当作技术探讨,请勿应用在非法领域
昨天与大家一起学习了关于遍历 PspCreateProcessNotifyRoutine 的一些知识,
今天我们来看一下关于线程方面
还是那句话我们这里讨论的目的不是如何杀死tp的两条线程,而是对于Windows系统的理解,从而能更加熟练地驾驭系统 驱动程序创建线程一般使用 PsCreateSystemThread 函数
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,
IN PVOID StartContext
);
这个函数很简单,由它创建的线程将运行在system进程中,
那么既然我们要杀死一条由驱动创建的线程,我们遍历一下system进程中的线程,
然后判断一下该线程函数是否位于目标模块的地址空间中就可以找到它了
那么我们接下来的工作就是首先要遍历system进程中的线程得到所有的线程函数地址,
遍历系统空间所有的模块得到目标模块的加载基址和模块的大小,
剩下的就是进行匹配了,
杀死线程我用的插APC的方法,所以必须设法得到线程的ETHREAD域,
因为插APC需要用到 KeInitializeApc 函数,这里面需要使用线程的KTHREAD结构地址,而KTHREAD就是ETHREAD的起始
NTKERNELAPI VOID
KeInitializeApc (PRKAPC Apc,
PRKTHREAD Thread, //目标线程对象
KAPC_ENVIRONMENT Environment,
PKKERNEL_ROUTINE KernelRoutine,
PKRUNDOWN_ROUTINE RundownRoutine,
PKNORMAL_ROUTINE NormalRoutine,
KPROCESSOR_MODE ProcessorMode,
PVOID NormalContext);
我们这里使用 ZwQuerySystemInformation 进行进线程信息和模块信息的查询,
这个函数想必大家都再熟悉不过了,声明一下函数原型就能直接使用
不过相关的结构需要注意一下:
typedef struct _SYSTEM_PROCESSES_INFORMATION { // Information Class 5
ULONG NextEntryDelta; //由此结构开始距离下一个结构的偏移,如果为0表示没有下一个结构了
ULONG ThreadCount; //进程中的线程数
ULONG Reserved1[6]; //保留
LARGE_INTEGER CreateTime; //进程的创建时间(从1601 1 1)
LARGE_INTEGER UserTime; //用户模式下运行的时间
LARGE_INTEGER KernelTime; //内核模式下运行的时间
UNICODE_STRING ProcessName; //进程名
KPRIORITY BasePriority;
ULONG ProcessId; //进程ID
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters;
SYSTEM_THREADS_INFORMATION Threads[1]; //进程的线程结构首个数组地址
} SYSTEM_PROCESSES_INFORMATION, *PSYSTEM_PROCESSES_INFORMATION;
typedef struct _SYSTEM_THREADS_INFORMATION { // Information Class 5
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress; //线程起始地址
CLIENT_ID ClientId; //线程标识符,包含线程标识符和进程标识符
KPRIORITY Priority; //线程优先级
KPRIORITY BasePriority;
ULONG ContextSwitchCount; //线程的切换次数
THREAD_STATE State; //线程的运行状态
KWAIT_REASON WaitReason;
ULONG Unknow;
} SYSTEM_THREADS_INFORMATION, *PSYSTEM_THREADS_INFORMATION;
我们一般都是从 Windows Nt 2000 Nttive Api Reference这本书中获取这些结构信息,
但是实际使用中我发现 SYSTEM_THREADS_INFORMATION 这个结构的长度是不准确的,少了四个字节,
所以结构最后我追加了一个双字域,我只测试了xp,win7两个系统,在这两个系统下该结构能正确匹配 封装了一下对于 ZwQuerySystemInformation 查询进线程信息的操作
BOOLEAN GetSystemProcessInformationByZwQuerySystemInformation(PVOID* pvBuf)
{
ULONG ulBufLen = 0x2000;
NTSTATUS status;
do{
*pvBuf = ExAllocatePoolWithTag(PagedPool,ulBufLen,1542);
if (*pvBuf == NULL)
return FALSE;
status = ZwQuerySystemInformation(5,*pvBuf,ulBufLen,NULL);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
if (*pvBuf!=NULL)
{
ExFreePool(*pvBuf);
*pvBuf = NULL;
}
ulBufLen += 0x2000;
}
else if (!NT_SUCCESS(status))
{
if (*pvBuf!=NULL)
{
ExFreePool(*pvBuf);
*pvBuf = NULL;
}
return FALSE;
}
}while(status == STATUS_INFO_LENGTH_MISMATCH);
return TRUE;
}
函数成功返回后 pvBuf 将包含一系列相关结构信息,并由调用者释放缓冲
SYSTEM_PROCESSES_INFORMATION的ProcessName域包含了进程的名称,
不过我选择了使用 PsLookupProcessByProcessId, 这个函数执行成功将返回进程的EPROCESS地址,
我们可以通过比较返回结果和 PsInitialSystemProcess 是否相同来判断是否为系统进程,
PsInitialSystemProcess 为系统导出变量,可以直接使用,它指向了 system 进程的EPROCESS
注意调用 PsLookupProcessByProcessId 成功之后要调用 ObDereferenceObject,具体原因在WDK上已明确指出 得到system进程的 SYSTEM_PROCESSES_INFORMATION 之后遍历一下其下的线程结构,
线程数量由 SYSTEM_PROCESSES_INFORMATION.ThreadCount 给出,
将线程结构数组中的 StartAddress 域和 ClientId 保存出来 接下来我们取得目标模块的相关信息,因为需要经常用到,我封装了一下这一操作,
不过我是通过遍历 PsLoadedModuleList 链来取得所有模块信息的,大家可以换成更稳妥的方式:
//遍历PsLoadedModuleList寻找指定模块的基址及大小<如果指定的话>
//执行成功返回指定模块基址,否则为0
ULONG GetSpecialModuleBase(PWCHAR pModuleName, PULONG pModuleSize)
{
PVOID pModuleList = NULL;
PLDR_DATA_TABLE_ENTRY pTableEntry = NULL;
ULONG ulTemp = 0;
ULONG ulReturnBytes = 0;
ULONG ulCount = 0;
UNICODE_STRING inputModuleName = {0};
UNICODE_STRING indexModuleName = {0};
UNICODE_STRING ntoskrnl = RTL_CONSTANT_STRING(L"ntoskrnl.exe");
UNICODE_STRING ntkrnlpa = RTL_CONSTANT_STRING(L"ntkrnlpa.exe");
RtlInitUnicodeString(&inputModuleName,pModuleName);
if ( inputModuleName.Length == 0 ) return 0;
//是否是内核ntoskrnl<没有考虑多处理器>
if ( (RtlCompareUnicodeString(&inputModuleName,&ntoskrnl,TRUE)==0) || (RtlCompareUnicodeString(&inputModuleName,&ntkrnlpa,TRUE)==0) )
RtlInitUnicodeString(&inputModuleName,L"ntoskrnl.exe");
ulReturnBytes = GetSystemModuleInformationByPsLoadedModuleList(&pModuleList);
if ( (ulReturnBytes == 0)||(pModuleList == NULL) ) return 0;
for (ulCount = 0; ulCount < (ulReturnBytes / sizeof(LDR_DATA_TABLE_ENTRY)); ulCount++ )
{
pTableEntry = (PLDR_DATA_TABLE_ENTRY)((ULONG)pModuleList+ulCount*sizeof(LDR_DATA_TABLE_ENTRY));
if ( pTableEntry->BaseName[0]!=(WCHAR)L"\0" )
RtlInitUnicodeString(&indexModuleName,(PWCHAR)pTableEntry->BaseName);
else
continue;
//比较字串如果发现了则break出去,此时pTableEntry包含了对应的模块信息
if ( RtlCompareUnicodeString(&inputModuleName,&indexModuleName,TRUE)==0 ) break;
}
//如果遍历完毕没有找到对应的模块,返回失败
if ( ulCount == (ulReturnBytes / sizeof(LDR_DATA_TABLE_ENTRY)) )
{
if ( (pModuleList!=NULL) )
{
ExFreePool(pModuleList);
pModuleList = NULL;
}
KdPrint(("GetSpecialModuleBase:未发现指定模块"));
return 0;
}
ulTemp = (ULONG)pTableEntry->DllBase;
if ( pModuleSize != NULL )
{
*pModuleSize = pTableEntry->SizeOfImage;
}
if ( (pModuleList!=NULL) )
{
ExFreePool(pModuleList);
pModuleList = NULL;
}
return ulTemp;
}
取到目标模块的地址空间范围之后,我们就可以遍历一下之前取得的线程 StartAddress 信息,
如果 StartAddress 位于模块的地址空间范围就说明该线程由此模块创建,
这时我们取出对应的 ClientId.UniqueThread,
通过 PsLookupThreadByThreadId 就能取得线程的ETHREAD结构地址了
与 PsLookupProcessByProcessId 一样, 成功调用 PsLookupThreadByThreadId 之后也要调用 ObDereferenceObject解除引用 封装了一下对于插APC的操作
VOID
ApcKernelRoutine(PKAPC pApc,PKNORMAL_ROUTINE *NormalRoutine,PVOID *NormalContext,PVOID *SystemArgument1,PVOID *SystemArgument2)
{
ExFreePool(pApc);
PsTerminateSystemThread(STATUS_SUCCESS);
}
BOOLEAN TerminateSpecialThreadByAPC(PETHREAD threadEThread)
{
PRKAPC pApc = NULL;
if ( MmIsAddressValidEx((PVOID)threadEThread) == VCS_INVALID ) return FALSE;
pApc = ExAllocatePoolWithTag(NonPagedPool,sizeof(KAPC),12345);
if ( pApc == NULL ) return FALSE;
KeInitializeApc(pApc,(PRKTHREAD)threadEThread,OriginalApcEnvironment,&ApcKernelRoutine,NULL,NULL,KernelMode,NULL);
return KeInsertQueueApc(pApc,NULL,NULL,1);
}
相对于我们现在的需要这貌似够用了,但是我想让它能再通用一些,
因为这个操作目前只能杀死system进程中的线程,
我想让它能杀死system进程中以外的线程,但是我们看一下WRK,
PsTerminateSystemThread 只能终结掉system进程中的线程:
看来是通过 IS_SYSTEM_THREAD 进行了判断,再看一下 IS_SYSTEM_THREAD:
嘿嘿,看来只是简单得检测了一个标志位,这样就简单了,我们手动添加这个标志到KTHREAD中不就行了吗?
不过这个标志位我看了一下在xp,2k3,win7 7600,win7 7601,下的偏移都不一样,这里就不做解释了
另外想直接利用PspTerminateThreadByPointer也是可以的,
从PsTerminateSystemThread开始搜索,可以得到PspTerminateThreadByPointer的地址
不过在xp中这个函数有两个参数,在2k3之后追加了一个参数,使用的时候需要注意 其实上面的所有操作都可以封装一下
BOOLEAN KillThreadsInSpecialModuleByApc(PWCHAR pModuleName);
以后自己使用就简单多了,这个比网上流传的那份特征码靠谱的多了
啊?什么特征码?好吧,当我没说..
当然这些操作对于断链的模块来说就无能为力了,而且主要的函数在调用前也没有进行hook检测
要解决这些还要做很多额外的工作.
如果大家有什么好的方法,也要共享出来啊~~
上传的附件: