线程注入,说到底还是创建线程,只不过创建的线程在别的进程中运行而已,那么,我们需要下手的地方跟监控普通线程创建需要下手的地方应当是一样的——NtCreateThread(不过这不是一个太好的选择,因为NtCreateThread的定位稍显麻烦。这儿只是一个为了说明如何监控而做一个例子而已,所以尽可能选择简单的方法来挂钩,修改ssdt可以挂这个函数,所以就选择了它。^-^更好的选择见后文)。
我认为我有必要先列出NtCreateThread的原型:
NTSTATUS
NtCreateThread(
__out PHANDLE ThreadHandle,
__in ACCESS_MASK DesiredAccess,
__in_opt POBJECT_ATTRIBUTES ObjectAttributes,
__in HANDLE ProcessHandle,
__out PCLIENT_ID ClientId,
__in PCONTEXT ThreadContext,
__in PINITIAL_TEB InitialTeb,
__in BOOLEAN CreateSuspended
)
__out的那些就都不解释了,主要解释需要用到的。
access_mask是访问权限,不解释;ObjectAttr是对象属性,_opt的,不解释;ProcessHandle就是将要被创建线程的进程(新线程在这个进程中运行)的句柄了;剩下几个也跟本文无关,也不解释,需要了解详细信息可以查MSDN。
需要关注的参数我想我已经说的很明确了——ProcessHandle。我们可以使用ObReferenceObjectByHandle来获取这个Handle所指向的进程的EPROCESS的地址(PEPROCESS)。
剩下的就是判断这个线程是正常创建还是远程创建的问题了,这个判断将会变得很容易,因为我们可以用获得到PEPROCESS和IoGetCurrentProcess的结果相比较——NtCreateThread总是应当在创建者的进程上下文中被执行。
不过当我们编译运行之后就会发现,还有一个问题是我们不得不关注的——运行程序的时候父进程会“帮助”子进程创建子进程的主要线程(因为这个时候子进程还没有线程,所以不可能自己创建),所以这就引出了另外一个问题——如何判断这个远程线程创建是注入还是正常的程序运行呢?
简单分析下我们不难发现,二者的区别在于将被创建线程的进程是否还存在其他线程——因为我们的代码是在NtCreateThread之前执行的,所以如果是正常运行的程序的话,这个时候它不应当有任何的线程,而线程注入则不同,线程注入的话目标进程应当已经有了至少一个线程(一个主线程和若干个附属线程(或者没有附属线程))。
那么如何判断目标进程是否已经存在线程呢?在EPROCESS结构中:
+0x190 ThreadListHead : _LIST_ENTRY
我想这个ThreadListHead这是很容易理解的。。。。
实现方法(因为只是为了演示,所以所有的地址、偏移等都是硬编码):
ThreadListHead = 0x190;
//==========================================
BOOLEAN
ProcessNoThread( PEPROCESS Process)
{
PLIST_ENTRY Entry;
PLIST_ENTRY ThreadListEntry;
PLIST_ENTRY ListHead;
ThreadListEntry = (PLIST_ENTRY)((ULONG)Process + ThreadListHead);
Entry = ThreadListEntry->Flink;
return (Entry==ThreadListEntry);
}
//==========================================
NTSTATUS new_NtCreateThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PVOID InitialTeb,
IN BOOLEAN CreateSuspended )
{
NTSTATUS st;
PVOID pepCurrentProcess=0;
st=ObReferenceObjectByHandle(ProcessHandle,(ACCESS_MASK)PROCESS_ALL_ACCESS,NULL,KernelMode,&pepCurrentProcess,NULL);
if (NT_SUCCESS(st))
{
if (IoGetCurrentProcess()!=pepCurrentProcess)
{
if (!ProcessNoThread((PEPROCESS)pepCurrentProcess)) DbgPrint("PROCESS 0x%X have created a thread into PROCESS 0x%X, NtCreateThread return value = 0x%x",IoGetCurrentProcess(),pepCurrentProcess,st);
}
ObDereferenceObject((PVOID)pepCurrentProcess);
}
st=old_NtCreateThread(ThreadHandle,DesiredAccess,ObjectAttributes,ProcessHandle,ClientId,ThreadContext,InitialTeb,CreateSuspended);
return st;
}
//==========================================
我想这段代码也不至于太难理解。。
是时候说说其他的一些问题了:
1、其实hook PspCreateThread要比hook NtCreateThread相对容易一些(两个函数同样都没有被导出,而PspCreateThread可以很容易的在PsCreateSystemThread中被定位,但NtCreateThread的定位就需要分析PE文件了(改SSDT另当别论,不过改SSDT的强度太差))。
2、遍历进程的线程是使用硬编码的,这使得通用性变得很差,而通过遍历PspCidTable枚举系统中的线程则成为一种不错的方法(从PspCreateThread中的代码来看,是由PspCreateThread在PspCidTable中ExCreateHandle的,但是我没有做测试)
就这样把,我自负的认为我的语言表达能力还算是不错的。
---EOF---
过程是跳转到自己的一个特定的地址处理),隐藏进程的 *** 作,也就是链表的删除其实没有什么神秘的。
检测的方法同样也很简单,今天我们说的方法算是最稳定、最简单的在Ring0下检测隐藏进程的了,不需要牵扯到稍微复杂的
PspCidTable(进程句柄表),毕竟PspCidTable的结构对于每一个系统来说是不一样的,在Windows
2000中为两层表,到了XP以后就变成三层了;同时,它在每个系统中获得的地址也不一样,所以我们仍使用
ZwQuerySystemInformation来枚举。或许有人会说它只能枚举出Ring3中Hook EnumProcess或Hook
CreateToolhelp32Snapshot这样的Win32
API隐藏的进程。由于篇幅的问题,如何获取真实的SSDT(系统服务描述表)地址,我会在下一期中告诉大家通用的方法获取该值,所以大家不用担心无法检测到灰鸽子、PcShare木马。首先要说的是在驱动中分配内存的函数不再是new和delete,而是ExAllocatePool和
ExFreePool。如果系统进程过多,ZwQuerySystemInformation的调用可能会失败,本文采用一个循环重新分配缓冲区的大小,每次增加两倍的缓冲区大小,直到成功为止。文中缓冲区的初始值大小为32KB,如果首次分配过多会导致SYSTEM内核进程内存占用过多,影响系统性能等问题,同时内核驱动为Unicode编码,这点需要大家注意。
原来进程Id和线程Id都是基于全局的句柄表PspCidTable生成,也就是句柄表的索引号。句柄表除了作为对象引用的容器以外,还有另一个用法:作为分配进程和线程的唯一ID 的有效手段。进程有一个唯一ID,称为UniqueProcessId;线程有一个CLIENT_ID 成员Cid,其中包含了所属进程的唯一ID 和线程自身的唯一ID。这些唯一ID 是怎么生成的呢?它们是通过调用ExCreateHandle 函数,在一个全局的句柄表PspCidTable 中创建的句柄索引值。此句柄表也称为CID 句柄表(Client ID handle table),它没有被加入到系统的句柄表链表中。CID 句柄表中的每个句柄表项都包含了进程或线程的对象地址。
因此,进程和线程的唯一ID 值都是4 的倍数,其中4 是第一个句柄索引值,它被分配给System 进程的唯一ID(因为System 进程是第一个通过PspCreateProcess 函数创建的进程)。0 是专门保留给空闲进程的,它并非通过ExCreateHandle 函数而获得。CID 句柄表严格按照FIFO 来重用句柄表项,所以,一个句柄表项被释放以后,要等到其他的空闲句柄表项都被重用一遍以后才会被再次使用。由于CID 句柄表项保存了进程或线程的对象地址,所以,在内核中,根据进程或线程的唯一ID 值,总是可以很方便地找到相应的对象地址,函数PsLookupProcessThreadByCid、PsLookupProcessByProcessId 和PsLookup-ThreadByThreadId 正是利用了CID 句柄表的这一能力,参见base\ntos\ps\pscidc 文件中的代码。
PspCidTable的说明如下:
PspCidTable也是一个句柄表,其格式与普通的句柄表是完全一样的但它与每个进程私有的句柄表有以下不同:
1PspCidTable中存放的对象是系统中所有的进线程对象,其索引就是PID和TID
2PspCidTable中存放的直接是对象体(EPROCESS和ETHREAD),而每个进程私有的句柄表则存放的是对象头(OBJECT_HEADER)
3PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来。
至于为什么句柄表的号都是4的倍数呢?
一个进程的句柄表包含了所有已被该进程打开的那些对象的指针。对象句柄是用来检索句柄表的一个“伪索引”。对于句柄表机制,achillis <<Windows句柄表>>系列文章已经分析得很透彻了,只是对“句柄以4为步进”来源不明。经查,根源如下:
typedef struct _EXHANDLE
{
union
{
struct
{
ULONG TagBits:2;
ULONG Index:30;
}
HANDLE GenericHandleOverlay;
#define HANLE_VALUE_INC 4
ULONG_PTR Value;
}
}EXHANDLE,PEXHANDLE;
此结构正是用来定义句柄类型。低2位TagBits为标志位Windows用于其它用途,故句柄值低2位对其作为句柄表索引本身无意义,所以等于4的倍数。有了以上分析,自然,在用句柄值为索引取句柄表项时,句柄值必须/4。因此程序中用到的句柄值并不能直接用来索引句柄表,也就有了“伪索引”说法。
以上就是关于HOOK NtCreateThread 怎么获得 创建进程的命令行参数全部的内容,包括:HOOK NtCreateThread 怎么获得 创建进程的命令行参数、驱动层检测R3进程API有没有被hook、Windows的PID为什么是4的倍数等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)