Windows中每个进程会包含一个或多个线程,每个线程在0环都有一个对应的结构体:ETHREAD,这个结构体包含了线程所有重要的信息,下面来简单了解一下。
ETHREAD
Windbg中,执行dt _ETHREAD可以看到这个完成的结构:
+0x000 Tcb
- 成员名:Tcb
- 数据类型:_KTHREAD
- Windbg查询指令:dt _KTHREAD
- 结构图:
- 说明:KTHREAD这个结构应该已经比较眼熟了,在API函数的调用过程(保存现场),就多次用到了KTHREAD结构里的成员。
+0x000 Header
- 成员名:Header
- 数据类型:_DISPATCHER_HEADER
- 说明:结构体内若包含_DISPATCHER_HEADER这个数据类型,说明这是一个可等待对象
- 可等待对象:Mutex,Event都是可等待对象,可被作用于WaitForSingleObject这类函数
+0x018 InitialStack/+0x01c StackLimit/+0x028 KernelStack
- 成员名:InitialStack/StackLimit/KernelStack
- 数据类型:Ptr32 Void
- 说明:这三个成员与线程切换有关。有印象的话,在分析KiFastCallEntry函数保存现场的过程中,曾有一行代码获取了InitialStack的值,并存到了ebp中。 此外,线程切换发生时,会根据KernelStack修改TSS的ESP0。更多关于这三个成员的用法,会在后面线程切换的地方再提到
+0x020 Teb
- 成员名:Teb(Thread Environment Block),线程环境块
- 数据类型:Ptr32 Void
- 大小:4KB
- 结构图:
- 说明:
- 0x20位置存着一个指向Teb结构的指针
- Teb是在3环用来描述线程的一个结构。
- 0环时,FS:[0]指向KPCR;3环时,FS:[0]指向TEB
+0x02c DebugActive
- 成员名:DebugActive
- 数据类型:UChar
- 说明:在分析KiSystemService进行保存现场的过中遇到过,若这个位置的值不是-1,说明处于调试状态,程序会跳转到执行一个将Dr0~Dr7保存到_Trap_Frame里面的操作。从而衍生出了一个反调试手段,将这个位置的值置为-1,从而不能使用8个调试寄存器
+0x02d State
- 成员名:State
- 数据类型:UChar
- 说明:线程状态-就绪/等待/运行
+0x060 WaitListEntry/SwapListEntry
- 成员名:WaitListEntry/SwapListEntry
- 数据类型:WaitListEntry为_LIST_ENTRY / SwapListEntry为 _SINGLE_LIST_ENTRY
- 说明:此处为Wait链表或Ready链表。Windows线程总是处于Wait/Running/Ready这三种状态之一
+0x06c BasePrioirty
- 成员名:BasePriority
- 数据类型:Char
- 说明:其初始值是所属进程的BasePrioirty值(KPROCESS->BasePriority),以后可以通过KeSetBasePriorityThread()函数重新设定
+0x070 WaitBlock
- 成员名:WaitBlock
- 数据类型:[4]_KWAIT_BLOCK
- 说明:当前线程,正在等待哪个可等待对象(WaitForSingleObject)这个可等待对象的信息就会被写入这个_KWAIT_BLOCK结构的数组里。
+0x0E0 ServiceTable
- 成员名:ServiceTable
- 数据类型:Ptr32 Void
- 说明:这个应该很熟悉了,指向系统服务表基址;通过系统服务表,可以找到函数地址表,根据系统服务号提供的偏移,就可以在函数地址表中找到3环API接口对应的0环内核函数
+0x134 TrapFrame
- 成员名:TrapFrame
- 数据类型:Ptr32 _KTRAP_FRAME
- 说明:这个也很熟悉了,进0环时,保存现场的原理就是填充寄存器及相关数据到TrapFrame结构中,最后更新TrapFrame位置的值,使其指向新保存的TrapFrame
+0x140 PerviousMode
- 成员名:PerviousMode
- 数据类型:Char
- 说明:在调用0环函数,保存现场的过程时,会将先前模式保存到TrapFrame结构中,以便根据先前模式,能够正确的返回到调用它的函数。
+0x1b0 ThreadListEntry
- 成员名:ThreadListEntry
- 数据类型:_LIST_ENTRY
- 说明:
- 双向链表,一个进程所有的线程,都挂在一个链表中,挂的就是这个位置
- 链表头位于KPROCESS+0x50的位置以及EPROCESS+0x190的位置,相当于ThreadListEntry的PsThreadListHead
- 一共有两个这样的链表
APC相关(位于KTHREAD内)
成员位置 | 成员名 | 数据类型 |
---|---|---|
+0x034 | ApcState | _KAPC_STATE |
+0x0e8 | ApcQueueLock | Uint4B |
+0x138 | ApcStatePointer | [2] Ptr32 _KAPC_STATE |
+0x14c | SavedApcState | _KAPC_STATE |
+0x165 | ApcStateIndex | UChar |
+0x166 | ApcQueueable | UChar |
- 说明:这些均为与APC相关的结构,具体到APC章节再做分析,这里仅作了解
+0x1ec Cid
+0x220 ThreadProcess
- 成员名:ThreadProcess
- 数据类型:Ptr32 _EPROCESS
- 说明:指向自己所属进程
+0x22c ThreadListEntry
- 成员名:ThreadListEntry
- 数据类型:_LIST_ENTRY
- 说明:
- 双向链表,一个进程所有的线程,都挂在一个链表中,挂的就是这个位置
- 链表头位于KPROCESS+0x50的位置以及EPROCESS+0x194的位置,相当于ThreadListEntry的PsThreadListHead
- 一共有两个这样的链表
- 这个双向链表的内容和0x1b0位置的完全一样,构建2个双向链表主要是为了方便,一个位于KTHREAD内,一个位于ETHREAD内
- 结构图:
关于断链
在进程结构体中,我们通过断链实现了简单的进程隐藏,其原理在于,任务管理器在查询进程便是通过遍历ActiveProcessLink实现的。尽管进程已经不在活动进程链表上,但是仍然可以运行,原因是Windows调度的基本单位是线程,而不是进程,所以才有从进程链表上摘除自身进程的隐藏方法,这虽然从进程链表上摘除了自身,但不会影响操作系统的调度,所以不影响程序运行。
不过这里没能完成线程断链的实验,原因可能在于VMware虚拟机的指令问题,Windbg无法中断操作系统时间过长,所以经常实验到一半,就无法继续执行指令了,只能重启虚拟机(应该是VMware 15的问题,升级到VMware 16就没问题了)。
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=44
参考文章:https://blog.csdn.net/emaste_r/article/details/8916786
参考笔记:张嘉杰的笔记