进程线程篇——总结与提升(上)
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?系统调用篇学会了吗?练习做完了吗?没有的话就不要继续了。
🔒 华丽的分割线 🔒
模拟线程切换分析
之前我们详细分析了模拟线程切换的本质,里面模拟了两处线程切换,一处模拟了时钟切换,一处模拟了主动切换,也就是通过API
的方式进行的切换。
我们来看一下模拟时钟切换的部分:
while(TRUE) {
Sleep(20);
Scheduling();
}
上面的Sleep(20)
就是代表我每20ms
继续调用Scheduling()
来实现线程切换,也就是说时钟周期是20ms
。
模拟通过API
的方式进行的主动切换的就是如下函数:
void GMSleep(int MilliSeconds)
{
GMThread_t* GMThreadp;
GMThreadp = &GMThreadList[CurrentThreadIndex];
if (GMThreadp->Flags != 0) {
GMThreadp->Flags = GMTHREAD_SLEEP;
GMThreadp->SleepMillsecondDot = GetTickCount() + MilliSeconds;
}
Scheduling();
return;
}
而我们的每一个线程,都会调用GMSleep
这个函数,这个函数模拟的就是模拟调用WinAPI
:
void Thread1(void*) {
while(1){
printf("Thread1\n");
GMSleep(500);
}
}
我假设上面的你都搞明白了。那么,接着上篇的课后练习,怎样实现线程的挂起和恢复呢?这个问题你思考了吗?没思考的话就不要继续了。
线程挂起恢复分析实现
为了实现线程的挂起恢复函数,我们应该把思路放在它是如何实现线程调度上。让线程挂起,无非就是不让给这个线程CPU
时间了,看看下面负责调度的函数是怎样找到线程的:
void Scheduling(void)
{
int i;
int TickCount;
GMThread_t* SrcGMThreadp;
GMThread_t* DstGMThreadp;
TickCount = GetTickCount();
SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
DstGMThreadp = &GMThreadList[0];
for (i = 1; GMThreadList[i].name; i++) {
if (GMThreadList[i].Flags & GMTHREAD_SLEEP) {
if (TickCount > GMThreadList[i].SleepMillsecondDot) {
GMThreadList[i].Flags = GMTHREAD_READY;
}
}
if (GMThreadList[i].Flags & GMTHREAD_READY) {
DstGMThreadp = &GMThreadList[i];
break;
}
}
CurrentThreadIndex = DstGMThreadp - GMThreadList;
SwitchContext(SrcGMThreadp, DstGMThreadp);
return;
}
可以看到,只要模拟线程是GMTHREAD_READY
状态,它就会调换线程,也就是说我们改一改这个标志线程状态的参数一改,就能实现我想要的挂起和恢复,我们来实现它。
首先,我们在ThreadSwitch.h
来添加两处声明:
bool SyspendThread(char* Name);
bool ResumeThread(char* Name);
然后我们对它进行实现:
bool SyspendThread(char* Name)
{
for (int i=1;i<MAXGMTHREAD;i++)
{
if (!strcmp(Name,GMThreadList[i].name))
{
GMThreadList[i].Flags = GMTHREAD_EXIT;
return true;
}
}
return false;
}
bool ResumeThread(char* Name)
{
for (int i=1;i<MAXGMTHREAD;i++)
{
if (!strcmp(Name,GMThreadList[i].name))
{
GMThreadList[i].Flags = GMTHREAD_READY;
return true;
}
}
return false;
}
为了方便观察,我们只保留了Thread1
和Thread2
,最终main.cpp
的内容如下:
#include "stdafx.h"
#include <windows.h>
#include "ThreadSwitch.h"
extern int CurrentThreadIndex;
extern GMThread_t GMThreadList[MAXGMTHREAD];
void Thread1(void*) {
while(1){
printf("Thread1\n");
GMSleep(500);
}
}
void Thread2(void*) {
while (1) {
printf("Thread2\n");
GMSleep(500);
}
}
int main()
{
RegisterGMThread("Thread1", Thread1, NULL);
RegisterGMThread("Thread2", Thread2, NULL);
//SyspendThread("Thread2");
//ResumeThread("Thread2");
while(TRUE) {
Sleep(20);
Scheduling();
}
return 0;
}
我们接下来以动图的形式进行演示:
上一篇留下的第0题到此就解决完毕了。
SwapContext 分析
本分析对于逆向水平有一定的要求,如果不行的话建议做多一些有关IDA
的CrakeMe
练习,分析流程以做熟悉和练习。不过没有经验也无所谓,仔细看看本部分,回去重新做一遍。
当你找到这个函数的是哦胡,你看到的应该是下面的情况:
SwapContext proc near ; CODE XREF: KiUnlockDispatcherDatabase(x)+72↑p
; KiSwapContext(x)+29↑p ...
or cl, cl
mov byte ptr es:[esi+2Dh], 2
pushf
loc_46A8E8: ; CODE XREF: KiIdleLoop()+5A↓j
mov ecx, [ebx] ; ebx = KPCR
cmp dword ptr [ebx+994h], 0
push ecx
jnz loc_46AA2D
cmp ds:_PPerfGlobalGroupMask, 0
jnz loc_46AA04
loc_46A905: ; CODE XREF: SwapContext+12C↓j
; SwapContext+13D↓j ...
mov ebp, cr0
mov edx, ebp
mov cl, [esi+2Ch]
mov [ebx+50h], cl
既然分析它的参数是什么,首先我们得知道它们是如何传参的,传的是什么,这样我们才能解决上一篇留下的思考题。
然后我们找到它的来自KiSwapContext
的一个引用,结果如下:
; __fastcall KiSwapContext(x)
@KiSwapContext@4 proc near ; CODE XREF: KiSwapThread()+41↑p
var_200FE4 = dword ptr -200FE4h
var_10 = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
sub esp, 10h
mov [esp+10h+var_4], ebx
mov [esp+10h+var_8], esi
mov [esp+10h+var_C], edi
mov [esp+10h+var_10], ebp
mov ebx, ds:0FFDFF01Ch ; ebx = &_KPCR
mov esi, ecx ; esi = ecx = NextReadyThread
mov edi, [ebx+124h]
mov [ebx+124h], esi
mov cl, [edi+58h]
call SwapContext
mov ebp, [esp+10h+var_10]
mov edi, [esp+10h+var_C]
mov esi, [esp+10h+var_8]
mov ebx, [esp+10h+var_4]
add esp, 10h
retn
@KiSwapContext@4 endp
但是这样也看不出来参数是什么,我们再往上找一级:
@KiSwapThread@0 proc near ; CODE XREF: KiAttachProcess(x,x,x,x)+F2↑p
; KeDelayExecutionThread(x,x,x):loc_42279A↑p ...
mov edi, edi
push esi
push edi
db 3Eh
mov eax, ds:0FFDFF020h
mov esi, eax
我们找到了KiSwapThread
这个函数,明显望文生义就是用来切换线程用的内核函数。前面我们知道0xFFDFF000
这个地址存放的就是KPCR
结构体的地址,那么根据结构体可以知道0xFFDFF020
存放的就是KPRCB
这个结构体的首地址,最终分析得到如下结果:
@KiSwapThread@0 proc near ; CODE XREF: KiAttachProcess(x,x,x,x)+F2↑p
; KeDelayExecutionThread(x,x,x):loc_42279A↑p ...
mov edi, edi
push esi
push edi
db 3Eh
mov eax, ds:0FFDFF020h
mov esi, eax
mov eax, [esi+_KPRCB.NextThread] ; eax = NextThread
test eax, eax ; 测一测有没有
mov edi, [esi+_KPRCB.CurrentThread] ; edi = CurrentThread
jz short loc_429CAC ; 没有 NextThread 就跳
and [esi+_KPRCB.NextThread], 0 ; 把 NextThread 清零
jmp short loc_429CCF
; ---------------------------------------------------------------------------
loc_429CAC: ; CODE XREF: KiSwapThread()+14↑j
push ebx
movsx ebx, [esi+_KPRCB.Number]
xor edx, edx
mov ecx, ebx
call @KiFindReadyThread@8 ; KiFindReadyThread(x,x)
test eax, eax
jnz short loc_429CCE ; 如果找到了下一个线程就跳走
mov eax, [esi+_KPRCB.IdleThread]
xor edx, edx
inc edx
mov ecx, ebx
shl edx, cl
or _KiIdleSummary, edx
loc_429CCE: ; CODE XREF: KiSwapThread()+2C↑j
pop ebx
loc_429CCF: ; CODE XREF: KiSwapThread()+1A↑j
mov ecx, eax
call @KiSwapContext@4 ; KiSwapContext(x)
经过简单的分析,我们很容易地判断出执行到KiSwapContext
前的ecx
为一个线程结构体,如下图所示:
根据符号显示KiSwapContext
是只有一个参数,经过简单的分析可以得到下面的结果:
; __fastcall KiSwapContext(x)
@KiSwapContext@4 proc near ; CODE XREF: KiSwapThread()+41↑p
var_200FE4 = dword ptr -200FE4h
var_10 = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
sub esp, 10h
mov [esp+10h+var_4], ebx
mov [esp+10h+var_8], esi
mov [esp+10h+var_C], edi
mov [esp+10h+var_10], ebp
mov ebx, ds:0FFDFF01Ch ; ebx = &_KPCR
mov esi, ecx ; esi = ecx = NextReadyThread
mov edi, [ebx+_KPCR.PrcbData.CurrentThread]
mov [ebx+_KPCR.PrcbData.CurrentThread], esi
mov cl, [edi+_KTHREAD.WaitIrql]
call SwapContext
mov ebp, [esp+10h+var_10]
mov edi, [esp+10h+var_C]
mov esi, [esp+10h+var_8]
mov ebx, [esp+10h+var_4]
add esp, 10h
retn
@KiSwapContext@4 endp
执行到SwapContext
这个函数前,esi
成了下一个切换新线程,而edi
成了需要被切换的老线程,而ebx
是KPCR
结构体,也就是说,改函数一共有3个参数,每一个参数的含义我们都已经知道了,我们利用IDA
的F5
也可以得到一定的验证:
char __usercall SwapContext@<al>(int *a1@<ebx>, int a2@<edi>, int a3@<esi>)
接下来我们分析一下在哪里实现了线程切换。既然操作系统线程切换是基于堆栈的,esp
换了,必然导致线程的切换,我们很容易跟到下面的汇编:
loc_46A94C: ; CODE XREF: SwapContext+67↑j
mov ecx, [ebx+_KPCR.TSS]
mov [ecx+_KTSS.Esp0], eax
mov esp, [esi+_KTHREAD.KernelStack]
mov eax, [esi+_KTHREAD.Teb]
mov [ebx+_KPCR.NtTib.Self], eax
经历过堆栈的弹出恢复操作,再调用retn
,即可完成线程的切换。接下来我们看看什么时候切换的CR3
:
mov eax, [edi+_KTHREAD.ApcState.Process]
cmp eax, [esi+_KTHREAD.ApcState.Process]
mov [edi+_KTHREAD.IdleSwapBlock], 0
jz short loc_46A994
mov edi, [esi+_KTHREAD.ApcState.Process]
test word ptr [edi+_KTHREAD.Teb], 0FFFFh
jnz short loc_46A9CE
xor eax, eax
loc_46A975: ; CODE XREF: SwapContext+117↓j
lldt ax
xor eax, eax
mov gs, eax
assume gs:GAP
mov eax, [edi+_EPROCESS.Pcb.DirectoryTableBase]
mov ebp, [ebx+_KPCR.TSS]
mov ecx, dword ptr [edi+_KTHREAD.Iopl]
mov [ebp+_KTSS.CR3], eax
mov cr3, eax
mov [ebp+_KTSS.IoMapBase], cx
jmp short loc_46A994
; ---------------------------------------------------------------------------
align 4
loc_46A994: ; CODE XREF: SwapContext+86↑j
; SwapContext+AF↑j
mov eax, [ebx+_KPCR.NtTib.Self]
经过分析,切换CR3
是需要条件的,它会判断新的线程和老的线程的Process
是不是一样的,然后决定是否处理:
mov eax, [edi+_KTHREAD.ApcState.Process]
cmp eax, [esi+_KTHREAD.ApcState.Process]
mov [edi+_KTHREAD.IdleSwapBlock], 0
jz short loc_46A994
如果不相同的话,就会执行下面的代码切换CR3
:
mov eax, [edi+_EPROCESS.Pcb.DirectoryTableBase]
mov ebp, [ebx+_KPCR.TSS]
mov ecx, dword ptr [edi+_KTHREAD.Iopl]
mov [ebp+_KTSS.CR3], eax
mov cr3, eax
看明白到这个地方的时候,第2题就解决了。一个CPU
一套寄存器,也就说明里面只能存储一个TSS
地址的寄存器,那么中断门提权时TSS
中存储的一定是当前线程的ESP0
和SS0
吗?我们接下来分析一下:
mov eax, [esi+_KTHREAD.InitialStack]
mov ecx, [esi+_KTHREAD.StackLimit]
sub eax, 210h
mov [ebx+_KPCR.NtTib.StackLimit], ecx
mov [ebx+_KPCR.NtTib.StackBase], eax
xor ecx, ecx
mov cl, [esi+_KTHREAD.NpxState]
and edx, 0FFFFFFF1h
or ecx, edx
or ecx, [eax+20Ch]
cmp ebp, ecx
jnz loc_46A9FC
lea ecx, [ecx+0]
loc_46A940: ; CODE XREF: SwapContext+11F↓j
test dword ptr [eax-1Ch], 20000h ; 检查是否为虚拟8086模式
jnz short loc_46A94C
sub eax, 10h
loc_46A94C: ; CODE XREF: SwapContext+67↑j
mov ecx, [ebx+_KPCR.TSS]
mov [ecx+_KTSS.Esp0], eax ; 将修正好的栈顶放入到 TSS 中
mov esp, [esi+_KTHREAD.KernelStack]
mov eax, [esi+_KTHREAD.Teb]
可以看出在mov esp, [esi+_KTHREAD.KernelStack]
之前,上面的代码已经处理好并修正ESP0
,所以中断门提权时TSS
中存储的一定是当前线程的ESP0
和SS0
。至此,第3题解答完毕。
在我测试的虚拟机中,fs
的段选择子都是0x3B
,但为什么不同的线程段选择子指向的TEB
却不一样呢?是因为它直接修改了GDT
表的内容,使它指向的地址是我们现在线程的TEB
,代码如下所示:
mov eax, [ebx+_KPCR.NtTib.Self]
mov ecx, [ebx+_KPCR.GDT]
mov [ecx+3Ah], ax
shr eax, 10h
mov [ecx+3Ch], al
mov [ecx+3Fh], ah
至此,第4题解答完毕。我们来看看0环的ExceptionList
在哪里备份的:
pop ecx
mov [ebx+_KPCR.NtTib.ExceptionList], ecx
第5题也就解决了,它把新线程的ExceptionList
存于KPCR
中。下面我们来看看IdleThread
如何查找:
我们知道IdleThread
存储于KPCR
之中,我们通过结构体的方式进行查询,找到该成员存储的地址。
kd> dt _KPCR 0xffdff000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : 0xffdff000 _KPCR
+0x020 Prcb : 0xffdff120 _KPRCB
+0x024 Irql : 0x1c ''
+0x028 IRR : 4
+0x02c IrrActive : 0
+0x030 IDR : 0xffff20f8
+0x034 KdVersionBlock : 0x80546ab8 Void
+0x038 IDT : 0x8003f400 _KIDTENTRY
+0x03c GDT : 0x8003f000 _KGDTENTRY
+0x040 TSS : 0x80042000 _KTSS
+0x044 MajorVersion : 1
+0x046 MinorVersion : 1
+0x048 SetMember : 1
+0x04c StallScaleFactor : 0x64
+0x050 DebugActive : 0 ''
+0x051 Number : 0 ''
+0x052 Spare0 : 0 ''
+0x053 SecondLevelCacheAssociativity : 0 ''
+0x054 VdmAlert : 0
+0x058 KernelReserved : [14] 0
+0x090 SecondLevelCacheSize : 0
+0x094 HalReserved : [16] 0
+0x0d4 InterruptMode : 0
+0x0d8 Spare1 : 0 ''
+0x0dc KernelReserved2 : [17] 0
+0x120 PrcbData : _KPRCB
kd> dx -id 0,0,805539a0 -r1 ((ntkrnlpa!_KPRCB *)0xffdff120)
((ntkrnlpa!_KPRCB *)0xffdff120) : 0xffdff120 [Type: _KPRCB *]
[+0x000] MinorVersion : 0x1 [Type: unsigned short]
[+0x002] MajorVersion : 0x1 [Type: unsigned short]
[+0x004] CurrentThread : 0x80553740 [Type: _KTHREAD *]
[+0x008] NextThread : 0x0 [Type: _KTHREAD *]
[+0x00c] IdleThread : 0x80553740 [Type: _KTHREAD *]
[+0x010] Number : 0 [Type: char]
[+0x011] Reserved : 0 [Type: char]
然后我们dt
一下这个结构体:
kd> dt _ETHREAD 0x80553740
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x1c0 CreateTime : _LARGE_INTEGER 0x0
+0x1c0 NestedFaultCount : 0y00
+0x1c0 ApcNeeded : 0y0
+0x1c8 ExitTime : _LARGE_INTEGER 0x0
+0x1c8 LpcReplyChain : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1c8 KeyedWaitChain : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1d0 ExitStatus : 0n0
+0x1d0 OfsChain : (null)
+0x1d4 PostBlockList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1dc TerminationPort : (null)
+0x1dc ReaperLink : (null)
+0x1dc KeyedWaitValue : (null)
+0x1e0 ActiveTimerListLock : 0
+0x1e4 ActiveTimerListHead : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1ec Cid : _CLIENT_ID
+0x1f4 LpcReplySemaphore : _KSEMAPHORE
+0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
+0x208 LpcReplyMessage : (null)
+0x208 LpcWaitingOnPort : (null)
+0x20c ImpersonationInfo : (null)
+0x210 IrpList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x218 TopLevelIrp : 0
+0x21c DeviceToVerify : (null)
+0x220 ThreadsProcess : (null)
+0x224 StartAddress : (null)
+0x228 Win32StartAddress : (null)
+0x228 LpcReceivedMessageId : 0
+0x22c ThreadListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x234 RundownProtect : _EX_RUNDOWN_REF
+0x238 ThreadLock : _EX_PUSH_LOCK
+0x23c LpcReplyMessageId : 0
+0x240 ReadClusterSize : 0
+0x244 GrantedAccess : 0x1f03ff
+0x248 CrossThreadFlags : 0
+0x248 Terminated : 0y0
+0x248 DeadThread : 0y0
+0x248 HideFromDebugger : 0y0
+0x248 ActiveImpersonationInfo : 0y0
+0x248 SystemThread : 0y0
+0x248 HardErrorsAreDisabled : 0y0
+0x248 BreakOnTermination : 0y0
+0x248 SkipCreationMsg : 0y0
+0x248 SkipTerminationMsg : 0y0
+0x24c SameThreadPassiveFlags : 0
+0x24c ActiveExWorker : 0y0
+0x24c ExWorkerCanWaitUser : 0y0
+0x24c MemoryMaker : 0y0
+0x250 SameThreadApcFlags : 0
+0x250 LpcReceivedMsgIdValid : 0y0
+0x250 LpcExitThreadCalled : 0y0
+0x250 AddressSpaceOwner : 0y0
+0x254 ForwardClusterOnly : 0 ''
+0x255 DisablePageFaultClustering : 0 ''
在上面的结构体中+0x224 StartAddress
存储的就是线程开始执行的地址,也就是我们在3环调用CreateThread
传递的函数地址。但是对于这个线程比较特殊,直接全为空,那么我们如何找到函数地址呢?
程序执行的时候,一定会用到堆栈。我们可以通过堆栈就可以定位程序的行为。我们把关注点放到KTHREAD
的KernelStack
上。涉及该成员的操作存在于线程切换中,我们来看看与堆栈操作相关的局部汇编代码:
pushf
mov ecx, [ebx] ; ebx = KPCR
cmp [ebx+_KPCR.PrcbData.DpcRoutineActive], 0
push ecx ;KPCR.NtTib.ExceptionList
……
mov esp, [esi+_KTHREAD.KernelStack]
……
pop ecx
xor eax, eax
retn
先看看堆栈地址的地址是啥:
kd> dt _KTHREAD 0x80553740
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY [ 0x80553750 - 0x80553750 ]
+0x018 InitialStack : 0x8054af00 Void
+0x01c StackLimit : 0x80547f00 Void
+0x020 Teb : (null)
+0x024 TlsArray : (null)
+0x028 KernelStack : 0x8054ac4c Void
+0x02c DebugActive : 0 ''
+0x02d State : 0x2 ''
+0x02e Alerted : [2] ""
+0x030 Iopl : 0 ''
+0x031 NpxState : 0xa ''
+0x032 Saturation : 0 ''
+0x033 Priority : 16 ''
+0x034 ApcState : _KAPC_STATE
+0x04c ContextSwitches : 0x1736
+0x050 IdleSwapBlock : 0 ''
+0x051 Spare0 : [3] ""
+0x054 WaitStatus : 0n0
+0x058 WaitIrql : 0x2 ''
+0x059 WaitMode : 0 ''
+0x05a WaitNext : 0 ''
+0x05b WaitReason : 0 ''
+0x05c WaitBlockList : 0x805537b0 _KWAIT_BLOCK
+0x060 WaitListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 WaitTime : 0x555
+0x06c BasePriority : 0 ''
+0x06d DecrementCount : 0 ''
+0x06e PriorityDecrement : 0 ''
+0x06f Quantum : -17 ''
+0x070 WaitBlock : [4] _KWAIT_BLOCK
+0x0d0 LegoData : (null)
+0x0d4 KernelApcDisable : 0
+0x0d8 UserAffinity : 0xffffffff
+0x0dc SystemAffinityActive : 0 ''
+0x0dd PowerState : 0 ''
+0x0de NpxIrql : 0 ''
+0x0df InitialNode : 0 ''
+0x0e0 ServiceTable : 0x80553fa0 Void
+0x0e4 Queue : (null)
+0x0e8 ApcQueueLock : 0
+0x0f0 Timer : _KTIMER
+0x118 QueueListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x120 SoftAffinity : 1
+0x124 Affinity : 1
+0x128 Preempted : 0 ''
+0x129 ProcessReadyQueue : 0 ''
+0x12a KernelStackResident : 0x1 ''
+0x12b NextProcessor : 0 ''
+0x12c CallbackStack : (null)
+0x130 Win32Thread : (null)
+0x134 TrapFrame : (null)
+0x138 ApcStatePointer : [2] 0x80553774 _KAPC_STATE
+0x140 PreviousMode : 0 ''
+0x141 EnableStackSwap : 0x1 ''
+0x142 LargeStack : 0 ''
+0x143 ResourceIndex : 0 ''
+0x144 KernelTime : 0x2d1a
+0x148 UserTime : 0
+0x14c SavedApcState : _KAPC_STATE
+0x164 Alertable : 0 ''
+0x165 ApcStateIndex : 0 ''
+0x166 ApcQueueable : 0x1 ''
+0x167 AutoAlignment : 0 ''
+0x168 StackBase : 0x8054af00 Void
+0x16c SuspendApc : _KAPC
+0x19c SuspendSemaphore : _KSEMAPHORE
+0x1b0 ThreadListEntry : _LIST_ENTRY [ 0x805539f0 - 0x805539f0 ]
+0x1b8 FreezeCount : 0 ''
+0x1b9 SuspendCount : 0 ''
+0x1ba IdealProcessor : 0 ''
+0x1bb DisableBoost : 0 ''
再看看堆栈长什么样子:
kd> dd 0x8054ac4c
8054ac4c 00000000 ffdff980 80542af0 00000000
8054ac5c 0000000e 00000000 00000000 00000000
8054ac6c 00000000 00000000 00000000 00000000
8054ac7c 00000000 00000000 00000000 00000000
8054ac8c 00000000 00000000 00000000 00000000
8054ac9c 00000000 00000000 00000000 00000000
8054acac 00000000 00000000 00000000 00000000
8054acbc 00000000 00000000 00000000 00000000
也就是说,第一个就是ExceptionList
,第二个就是Eflag
,第三个就是切换线程后跳转的地址,在这里也就是IdleThread
继续走的地址。这个地址肯定就在IdleThread
当中。我们u
一下:
kd> u 80542af0
ReadVirtual: 80542af0 not properly sign extended
80542af0 fb sti
ReadVirtual: 80542b00 not properly sign extended
80542af1 90 nop
ReadVirtual: 80542b01 not properly sign extended
80542af2 90 nop
ReadVirtual: 80542b02 not properly sign extended
80542af3 fa cli
ReadVirtual: 80542b03 not properly sign extended
80542af4 3b6d00 cmp ebp,dword ptr [ebp]
80542af7 740d je nt!KiIdleLoop+0x26 (80542b06)
80542af9 b102 mov cl,2
80542afb ff15a8864d80 call dword ptr [nt!_imp_HalClearSoftwareInterrupt (804d86a8)]
也就是函数是KiIdleLoop
,我们通过IDA
看看该函数:
; _DWORD __cdecl KiIdleLoop()
@KiIdleLoop@0 proc near ; CODE XREF: KiSystemStartup(x)+2E2↓j
lea ebp, [ebx+980h]
jmp short loc_46AAF0
; ---------------------------------------------------------------------------
loc_46AAE8: ; CODE XREF: KiIdleLoop()+2D↓j
lea ecx, [ebx+0C50h]
call dword ptr [ecx]
loc_46AAF0: ; CODE XREF: KiIdleLoop()+6↑j
; KiIdleLoop()+65↓j
sti
nop
nop
cli
cmp ebp, [ebp+0]
jz short loc_46AB06
mov cl, 2
call ds:__imp_@HalClearSoftwareInterrupt@4 ; HalClearSoftwareInterrupt(x)
call KiRetireDpcList
loc_46AB06: ; CODE XREF: KiIdleLoop()+17↑j
cmp dword ptr [ebx+128h], 0
jz short loc_46AAE8
sti
mov esi, [ebx+128h]
mov edi, [ebx+124h]
or ecx, 1
mov [ebx+124h], esi
mov byte ptr es:[esi+2Dh], 2
mov dword ptr [ebx+128h], 0
push offset loc_46AB3F
pushf
jmp loc_46A8E8
; ---------------------------------------------------------------------------
loc_46AB3F: ; DATA XREF: KiIdleLoop()+54↑o
lea ebp, [ebx+980h]
jmp short loc_46AAF0
@KiIdleLoop@0 endp
这个函数没有任何意义,就是让CPU
别闲着,执行一波无任何意义的代码。至此第6题解决。
KiFindReadyThread
分析这块涉及算法,经查阅是通过二分法进行查找的。算法实现原理是我的知识盲区,我仅把流程说一下:首先该函数会解析KiReadySummary
,找到从左起第一个为1的位数,再用该位获取从KiDispatchReadListHead
中的第一个_KTHREAD
线程,将其从链表中摘除再判断如果摘除后该链表为空,则找到相应的KiReadySummary
位将其置0,然后将对应找到的线程结构体返回。至于其中的详细细节,可以参考这位博友的分析:KiFindReadyThread分析 - 查找下一个就绪线程 。如下是IDA
的伪C代码,仅供参考:
PETHREAD __fastcall KiFindReadyThread(ULONG ProcessorNumber, KPRIORITY LowPriority)
{
int v2; // ecx
unsigned int v3; // eax
unsigned int v4; // edx
int v5; // edx
int v6; // eax
_LIST_ENTRY *v7; // esi
PETHREAD ReadyThread; // eax
_LIST_ENTRY *v9; // ecx
_LIST_ENTRY *v10; // edi
v2 = 16;
v3 = KiReadySummary & ~((1 << LowPriority) - 1);
v4 = HIWORD(v3);
if ( !HIWORD(v3) )
{
v2 = 0;
v4 = v3;
}
if ( (v4 & 0xFFFFFF00) != 0 )
v2 += 8;
v5 = v2 + KiFindFirstSetLeft[v3 >> v2];
v6 = v3 << (31 - v5);
v7 = &KiDispatcherReadyListHead[2 * v5];
if ( !v6 )
return 0;
while ( v6 >= 0 )
{
LOBYTE(v5) = v5 - 1;
--v7;
v6 *= 2;
if ( !v6 )
return 0;
}
v9 = v7->Flink->Flink;
ReadyThread = &v7->Flink[-12];
v10 = ReadyThread->WaitListEntry.Blink;
v10->Flink = v9;
v9->Blink = v10;
if ( IsListEmpty(v7) )
KiReadySummary &= ~(1 << v5);
return ReadyThread;
}
至此第7题解决。
模拟线程切换与Windows
的线程切换有哪些区别?真正的线程有两个栈,一个内核0环的栈,一个是3环的栈,发生线程切换在0环;模拟线程切换没用到FS
、异常列表之类的东西,其他的区别可以自行总结。
接下来是最后一题,我们走一下时钟中断的流程,中断都是在IDT
表中的,首先我们跟着走一下,首先定位该表,只需g
到_IDT
,效果如下所示:
_IDT dd offset _KiTrap00 ; DATA XREF: KiSystemStartup(x)+1D5↑o
db 0, 8Eh
word_5B8B02 dw 8 ; DATA XREF: KiSwapIDT()↓o
dd offset _KiTrap01
dd 88E00h
dd offset _KiTrap02
dd 88E00h
dd offset _KiTrap03
dd 8EE00h
dd offset _KiTrap04
dd 8EE00h
dd offset _KiTrap05
dd 88E00h
dd offset _KiTrap06
dd 88E00h
dd offset _KiTrap07
dd 88E00h
dd offset _KiTrap08
dd 88E00h
dd offset _KiTrap09
时钟中断的中断号是0x30
,我们定位到这个函数,如何定位呢?看下面的图:
然后跳转到这个函数:
; _DWORD __stdcall KiStartUnexpectedRange()
_KiStartUnexpectedRange@0 proc near ; DATA XREF: KiGetVectorInfo(x,x)+68↑o
; INIT:005B8C7C↓o
push 30h ; '0'
jmp _KiEndUnexpectedRange@0 ; KiEndUnexpectedRange()
_KiStartUnexpectedRange@0 endp
我们看看跳转到哪里:
; _DWORD __stdcall KiEndUnexpectedRange()
_KiEndUnexpectedRange@0 proc near ; CODE XREF: KiStartUnexpectedRange()+5↑j
; _KiUnexpectedInterrupt1+5↑j ...
jmp cs:off_46632E
_KiEndUnexpectedRange@0 endp
; ---------------------------------------------------------------------------
off_46632E dd offset _KiUnexpectedInterruptTail
; DATA XREF: KiEndUnexpectedRange()↑r
继续跟着,为了节约篇幅,只保留调用流程部分:
_KiUnexpectedInterruptTail proc near ; CODE XREF: KiEndUnexpectedRange()↑j
; DATA XREF: .text:off_46632E↑o
……
loc_466E7E: ; CODE XREF: Dr_kui_a+10↑j
; Dr_kui_a+7C↑j
inc dword ptr ds:0FFDFF5C4h
mov ebx, [esp+68h+var_68]
sub esp, 4
push esp
push ebx
push 1Fh
call ds:__imp__HalBeginSystemInterrupt@12 ; HalBeginSystemInterrupt(x,x,x)
or eax, eax
jnz short loc_466E9D
add esp, 8
jmp short loc_466EEC
; ---------------------------------------------------------------------------
loc_466E9D: ; CODE XREF: _KiUnexpectedInterruptTail+BF↑j
cli
call ds:__imp__HalEndSystemInterrupt@8 ; HalEndSystemInterrupt(x,x)
jmp short Kei386EoiHelper@0 ; Kei386EoiHelper()
_KiUnexpectedInterruptTail endp
public HalEndSystemInterrupt
HalEndSystemInterrupt proc near ; CODE XREF: sub_80010EF0+E8↑p
; sub_80017144+B3↓p
; DATA XREF: ...
arg_0 = byte ptr 4
movzx ecx, [esp+arg_0]
cmp byte ptr ds:0FFDFF024h, 2
jbe short loc_8001123E
mov eax, ds:dword_800176EC[ecx*4]
or eax, ds:0FFDFF030h
out 21h, al ; Interrupt controller, 8259A.
shr eax, 8
out 0A1h, al ; Interrupt Controller #2, 8259A
loc_8001123E: ; CODE XREF: HalEndSystemInterrupt+C↑j
mov ds:0FFDFF024h, cl
mov eax, ds:0FFDFF028h
mov al, ds:byte_80017784[eax]
cmp al, cl
ja short loc_80011256
retn 8
; ---------------------------------------------------------------------------
loc_80011256: ; CODE XREF: HalEndSystemInterrupt+35↑j
add esp, 0Ch
jmp ds:pKiUnexpectedInterrupt[eax*4]
HalEndSystemInterrupt endp ; sp-analysis failed
pKiUnexpectedInterrupt dd offset KiUnexpectedInterrupt
; DATA XREF: HalEndSystemInterrupt+3D↑r
; sub_80011260+3D↑r
dd offset sub_80016BDD
dd offset sub_80016A45
sub_80016A45 proc near ; CODE XREF: KfLowerIrql:loc_800110AC↑p
; KfReleaseSpinLock:loc_8001111C↑p ...
push dword ptr ds:0FFDFF024h
mov byte ptr ds:0FFDFF024h, 2
and dword ptr ds:0FFDFF028h, 0FFFFFFFBh
sti
call ds:KiDispatchInterrupt
cli
call sub_80011260
jmp ds:Kei386EoiHelper
sub_80016A45 endp
; _DWORD __stdcall KiDispatchInterrupt()
public _KiDispatchInterrupt@0
_KiDispatchInterrupt@0 proc near ; DATA XREF: .edata:off_58D2A8↓o
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
mov ebx, ds:0FFDFF01Ch ; a1
lea eax, [ebx+980h]
cli
cmp eax, [eax]
jz short loc_46A85E
push ebp
push dword ptr [ebx]
mov dword ptr [ebx], 0FFFFFFFFh
mov edx, esp
mov esp, [ebx+988h]
push edx
mov ebp, eax
call KiRetireDpcList
pop esp
pop dword ptr [ebx]
pop ebp
loc_46A85E: ; CODE XREF: KiDispatchInterrupt()+F↑j
sti
cmp dword ptr [ebx+9ACh], 0
jnz short loc_46A8BE
cmp dword ptr [ebx+128h], 0
jz short locret_46A8BD
mov eax, [ebx+128h]
loc_46A877: ; CODE XREF: KiDispatchInterrupt()+9F↓j
sub esp, 0Ch
mov [esp+0Ch+var_4], esi
mov [esp+0Ch+var_8], edi
mov [esp+0Ch+var_C], ebp
mov esi, eax ; NewThread
mov edi, [ebx+124h] ; oldThread
mov dword ptr [ebx+128h], 0
mov [ebx+124h], esi
mov ecx, edi
mov byte ptr [edi+50h], 1
call @KiReadyThread@4 ; KiReadyThread(x)
mov cl, 1
call SwapContext
mov ebp, [esp+0Ch+var_C]
mov edi, [esp+0Ch+var_8]
mov esi, [esp+0Ch+var_4]
add esp, 0Ch
locret_46A8BD: ; CODE XREF: KiDispatchInterrupt()+3F↑j
retn
; ---------------------------------------------------------------------------
loc_46A8BE: ; CODE XREF: KiDispatchInterrupt()+36↑j
mov dword ptr [ebx+9ACh], 0
call _KiQuantumEnd@0 ; KiQuantumEnd()
or eax, eax
jnz short loc_46A877
retn
_KiDispatchInterrupt@0 endp
进程挂靠
在讲编程的时候,我们都听过:一个进程可以包含多个线程,一个进程至少要有一个线程。进程为线程提供资源,也就是提供Cr3
的值,Cr3
中存储的是页目录表基址,Cr3
确定了,线程能访问的内存也就确定了。
对于这一行代码:mov eax,dword ptr ds:[0x12345678]
,CPU
如何解析这个地址呢?CPU
解析线性地址时要通过页目录表来找对应的物理页,页目录表基址存在Cr3
寄存器中。当前的Cr3
的值来源于当前的进程结构体的_KPROCESS.DirectoryTableBase
当中。那么进程挂靠又是怎么回事呢?我们先来看个结构体:
kd> dt _KTHREAD
ntdll!_KTHREAD
……
+0x032 Saturation : Char
+0x033 Priority : Char
+0x034 ApcState : _KAPC_STATE
+0x04c ContextSwitches : Uint4B
+0x050 IdleSwapBlock : UChar
……
kd> dt _KAPC_STATE
ntdll!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UChar
ApcState
这个成员我们在逆向线程切换的时候遇到过,也就是解决我们第2题的时候,这个结构体的Process
成员就是存储的进程挂靠上的进程CR3
。可以打一个比方,EPROCESS
的DirectoryTableBase
存储的是亲父母,而ApcState
存储的是养父母,我想要资源时从养父母来拿。正常情况下,CR3
的值是由养父母提供的,但CR3
的值也可以改成和当前线程毫不相干的其他进程的DirectoryTableBase
。将当前CR3
的值改为其他进程,称为“进程挂靠”。
跨进程内存读写
跨进程内存读写根据之前所学肯定必须切换CR3
,并且读取内存肯定会落实到类似如下汇编:
mov eax,dword ptr ds:[0x12345678]
mov dword ptr ds:[0x00401234],eax
我们自己实现一个跨进程内存读写一个int
还好说,如果是一个指定长度的Buffer
,那咋办呢?
我们都知道,应用程序的高2G
的内核空间是共用的,也就是说,无论是哪个应用程序,高2G
的内容寻址都能寻到的。那么我把读取进程的内存写到高2G
的空间,然后切换CR3
回去,然后重新把高2G缓存的东西写到指定Buffer
中,我们就完成了。上面的读的操作,写得操作也是类似的。
跨进程读
跨进程写
我们下面来简单分析一下Windows
实现的跨进程读内存的函数NtReadVirtualMemory
和跨进程写NtWriteVirtualMemory
的函数,NtWriteVirtualMemory
和NtReadVirtualMemory
实现十分相似,我就只分析前者,下面的自行分析。为什么说是浅析是因为里面有大量的其他前置知识,比如APC
和内存管理。三环怎么进内核的我就不再赘述了,为了方便。为了缩短篇幅增加可读性,我会尽可能使用IDA
翻译的伪代码,你的伪代码结果应该和我的不一样,因为我进行了一些重命名操作。
NtReadVirtualMemory 浅析
我们先定位到NtReadVirtualMemory
这个伪代码:
NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
{
_KTHREAD *v5; // edi
PSIZE_T v6; // ebx
int a7; // [esp+10h] [ebp-28h] BYREF
PRKPROCESS PROCESS; // [esp+14h] [ebp-24h] BYREF
KPROCESSOR_MODE AccessMode[4]; // [esp+18h] [ebp-20h]
NTSTATUS res; // [esp+1Ch] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+20h] [ebp-18h]
v5 = KeGetCurrentThread();
AccessMode[0] = v5->PreviousMode;
if ( AccessMode[0] )
{
if ( BaseAddress + NumberOfBytesToRead < BaseAddress
|| Buffer + NumberOfBytesToRead < Buffer
|| BaseAddress + NumberOfBytesToRead > MmHighestUserAddress
|| Buffer + NumberOfBytesToRead > MmHighestUserAddress )
{
return 0xC0000005;
}
v6 = NumberOfBytesRead;
if ( NumberOfBytesRead )
{
ms_exc.registration.TryLevel = 0;
if ( NumberOfBytesRead >= MmUserProbeAddress )
*MmUserProbeAddress = 0;
*NumberOfBytesRead = *NumberOfBytesRead;
ms_exc.registration.TryLevel = -1;
}
}
else
{
v6 = NumberOfBytesRead;
}
a7 = 0;
res = 0;
if ( NumberOfBytesToRead )
{
res = ObReferenceObjectByHandle(ProcessHandle, 0x10u, PsProcessType, AccessMode[0], &PROCESS, 0);
if ( !res )
{
res = MmCopyVirtualMemory(
PROCESS,
BaseAddress,
v5->ApcState.Process,
Buffer,
NumberOfBytesToRead,
AccessMode[0],
&a7);
ObfDereferenceObject(PROCESS);
}
}
if ( v6 )
{
*v6 = a7;
ms_exc.registration.TryLevel = -1;
}
return res;
}
我们可以看到,该函数实现内存拷贝是通过MmCopyVirtualMemory
这个函数实现的,我们点击去看看:
NTSTATUS __stdcall MmCopyVirtualMemory(PRKPROCESS PROCESS, PVOID BaseAddress, PRKPROCESS KPROCESS, char *buffer, SIZE_T Length, KPROCESSOR_MODE AccessMode, int *a7)
{
PRKPROCESS process; // ebx
_EPROCESS *eprocess; // ecx
NTSTATUS res; // esi
_EX_RUNDOWN_REF *RunRefa; // [esp+8h] [ebp+8h]
if ( !Length )
return 0;
process = PROCESS;
eprocess = PROCESS;
if ( PROCESS == KeGetCurrentThread()->ApcState.Process )
eprocess = KPROCESS;
RunRefa = &eprocess->RundownProtect;
if ( !ExAcquireRundownProtection(&eprocess->RundownProtect) )
return STATUS_PROCESS_IS_TERMINATING;
if ( Length <= 0x1FF )
goto LABEL_10;
res = MiDoMappedCopy(process, BaseAddress, KPROCESS, buffer, Length, AccessMode, a7);
if ( res == STATUS_WORKING_SET_QUOTA )
{
*a7 = 0;
LABEL_10:
res = MiDoPoolCopy(process, BaseAddress, KPROCESS, buffer, Length, AccessMode, a7);
}
ExReleaseRundownProtection(RunRefa);
return res;
}
你可能看到一个新奇的函数ExAcquireRundownProtection
,这个函数是申请一个锁,从网上查阅翻译过来是停运保护(RundownProtection
)锁,名字怪怪的听起来怪怪的。
这个不涉及我们的核心,我们继续分析,发现它内部又是通过MiDoMappedCopy
实现进程内存读取的:
NTSTATUS __stdcall MiDoMappedCopy(PRKPROCESS PROCESS, char *src, PRKPROCESS process, char *buffer, SIZE_T Length, KPROCESSOR_MODE AccessMode, int *a7)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v13 = 0;
src_0 = src;
buffer_1 = buffer;
v7 = 0xE000;
if ( Length <= 0xE000 )
v7 = Length;
v16 = &MemoryDescriptorList;
Length_1 = Length;
v19 = v7;
v20 = 0;
v14 = 0;
v15 = 0;
while ( Length_1 )
{
if ( Length_1 < v19 )
v19 = Length_1;
KeStackAttachProcess(PROCESS, &ApcState);
BaseAddress = 0;
v12 = 0;
v11 = 0;
ms_exc.registration.TryLevel = 0;
if ( src_0 == src && AccessMode )
{
v20 = 1;
if ( Length && (&src[Length] < src || &src[Length] > MmUserProbeAddress) )
ExRaiseAccessViolation();
v20 = 0;
}
MemoryDescriptorList.Next = 0;
MemoryDescriptorList.Size = 4 * (((src_0 & 0xFFF) + v19 + 0xFFF) >> 12) + 28;
MemoryDescriptorList.MdlFlags = 0;
MemoryDescriptorList.StartVa = (src_0 & 0xFFFFF000);
MemoryDescriptorList.ByteOffset = src_0 & 0xFFF;
MemoryDescriptorList.ByteCount = v19;
MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess);
v12 = 1;
BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0u, 0u, 0x20u);
if ( !BaseAddress )
{
v13 = 1;
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
}
KeUnstackDetachProcess(&ApcState);
KeStackAttachProcess(process, &ApcState);
if ( src_0 == src )
{
if ( AccessMode )
{
v20 = 1;
ProbeForWrite(buffer, Length, 1u);
v20 = 0;
}
}
v11 = 1;
qmemcpy(buffer_1, BaseAddress, v19);
ms_exc.registration.TryLevel = -1;
KeUnstackDetachProcess(&ApcState);
MmUnmapLockedPages(BaseAddress, &MemoryDescriptorList);
MmUnlockPages(&MemoryDescriptorList);
Length_1 -= v19;
src_0 += v19;
buffer_1 += v19;
}
*a7 = Length;
return STATUS_SUCCESS;
}
KeStackAttachProcess
和KeUnstackDetachProcess
这俩函数与APC
相关,在这里你可以简单理解就是切换CR3
,实现进程挂靠和解除挂靠。我们注意一下下面的伪代码:
MemoryDescriptorList.Next = 0;
MemoryDescriptorList.Size = 4 * (((src_0 & 0xFFF) + v19 + 0xFFF) >> 12) + 28;
MemoryDescriptorList.MdlFlags = 0;
MemoryDescriptorList.StartVa = (src_0 & 0xFFFFF000);
MemoryDescriptorList.ByteOffset = src_0 & 0xFFF;
MemoryDescriptorList.ByteCount = v19;
MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess);
v12 = 1;
BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0u, 0u, 0x20u);
MmMapLockedPagesSpecifyCache
这个函数就是映射里面描述的物理页,下面是微软对该函数的描述:
The MmMapLockedPagesSpecifyCache routine maps the physical pages that are described by an MDL to a virtual address, and enables the caller to specify the cache attribute that is used to create the mapping.
上面的操作就算锁住物理页,并把它重新映射到高2G
地址,我们直接写到里面,就少了重新把高2G
的内容重新写到程序空间的步骤了。
未完待续
由于减轻加载负担,所以将一半的内容放到下一篇,如果想继续学习请点击下一篇的链接。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15861519.html