前言
异常描述:
在操作系统层次主要有两种异常,即CPU异常和软件模拟异常。
当发生异常时,系统会在IDT中查找对应的处理程序。
例如:当代码执行到中断指令Int 3时,会产生一个中断号为3的中断,系统会指引CPU在中断描述符表(IDT)中查找对应于该中断号的中断处理程序,并执行相应的处理代码。
需要先了解的几个结构体:_KTRAP_FRAME
、_EXCEPTION_RECORD
、_KEXCEPTION_FRAME
、_KTRAP_FRAME
、_CONTEXT
、_KPCR
、_KPRCB
。
分析
KiBreakpointTrap
这里从定位到IDT表对应处理函数(KiBreakpointTrap)开始分析:
填充(栈上的)_KTRAP_FRAME
结构。刚开始此异常因为没有ErrorCode,所以sub rsp,8 直接把栈提升8个字节,保存rbp后将rsp-0x158即提升到_KTrap_Frame
结构顶部,随后将rbp定位到_KTrap_Frame
E结构上的 struct _M128A Xmm1后使用rbp寻址。
将ExceptionActive设置为1,接着填充栈上_KTrap_Frame
结构的rax、rcx、rdx、r8、r9、r10、r11。
根据段寄存器CS的值判断是从r0来的还是r3来的(为0是r0,为1是r3)。
如果为R3来的话(R0来的话较简单,不作过多解释),先判断KvaShadow(影子堆栈ShadowStack,主要用于防止栈溢出攻击,这里不做过多解释。),如果为1不需要切换gs,为0则需要切换gs。
之后处理控制流保护(CET机制)。
随后判断当前线程是否被调试(即判断结构体 _DISPATCHER_HEADER 的UCHAR ActiveDR7:1和 UCHAR Instrumented:1)并将_KTRAP_FRAME
结构上存储DR7位置的前两个字节设置为0,如果当前线程在被调试的话,就保存当前调试寄存器的状态。
然后跳转到r0与r3(从r0还是r3来的)的汇合处(loc_140420692)。将RFlags的方向标志位(DF)清零,把MXCSR寄存器中的值保存到栈上(_KTRAP_FRAME
结构的+0x2C处),将gs:180h (_KPRCB结构的第一个成员ULONG MxCsr)加载到MXCSR寄存器上,保存浮点寄存器。
判断KeSmapEnabled标志位,是否开启Smap(Smap:位于CR4寄存器的第21位,该位的作用是阻止处于内核权限的CPU读取或写入用户代码。)。
如果开启了Smap的话,判断从r3还是r0来的,如果是r3来的执行stac,要不然访问不了用户态的数据。
随后,判断一下Eflags的IF位,为1,执行stac;为0,跳转。
EFlags 0x200 对应 IF 位 ,KTrap_Frame 结构上保存的是 进入异常之前的EFlags(IDT表上的是中断门,进入当前处理函数的时候,IF位自动清除了,即当前使用的EFlags IF为0)。如果之前有,那就设置上,如果之前没有,那就不设置,保持与进入当前异常处理之前的IF一致。
之后传参,然后调用KiExceptionDispatch函数。
将ecx设置为断点状态(NTSTATUS 0x80000003对应断点状态);将edx设置为1,意思是第一次执行;将r8设置为rip-1(int3断点指令没有意义,减一回到上一条指令。),将r9设置为0。
KiExceptionDispatch
调用call之后,rsp-8,然后又rsp-1D8 ,提升栈(内核线程堆栈图如下)。
![]
将rax设置为rsp+0x100。
保存xmm寄存器,保存rbx、rdi、rsi、r12、r13、r14、r15(填充到了栈上_KEXCEPTION_FRAME
结构对应的位置)。
让rax等于rsp+0x138里的值即_KEXCEPTION_FRAME
结构的ULONGLONG Return
成员,猜测这个值是栈上_EXCEPTION_RECORD
结构的地址。
初始化栈上的_EXCEPTION_RECORD
结构。ecx是参数,ecx的值是0x0x80000003(表示断点状态),将ecx的值赋值给栈上_EXCEPTION_RECORD
结构的ExceptionCode
成员,对应断点状态。之后清零ecx,再将ecx(值是0)赋值给_EXCEPTION_RECORD
结构的ExceptionFlags
成员,将rcx的值(在64位下操作64位寄存器的32位寄存器,高位为0)赋值给_EXCEPTION_RECORD
结构的ExceptionRecord
成员,将r8、edx、r9、r10、r11依次赋值给_EXCEPTION_RECORD
结构的ExceptionAddress、NumberParameters、ExceptionInformation[0]、ExceptionInformation[1]、ExceptionInformation[2]。
让r9b等于 cs寄存器的值,再跟1进行按位与操作。
将r8设置为_KTRAP_FRAME
结构的地址(在栈上),让rdx等于栈顶(_KEXCEPTION_FRAME
结构的地址),让rcx等于rax即_EXCEPTION_RECORD
地址。
随后判断是从r0来的还是r3来的。
如果是从r3来的话,调用KiDispatchException函数。
如果是从r0来的话,让r10等于当前irql,r11等于2。
判断EFLAGS寄存器IF位是否为1,如果被置位,则r10等于r11即2。
如果r10小于r11,则会跳转,执行KiDispatchException函数。
判断异常栈活动性。如果kprcb处理器控制块结构的ExceptionStackActive不为0,则会跳转,执行KiDispatchException函数。
将异常栈与当前栈比较。让r10等于异常栈(kprcb处理器控制块结构的ExceptionStack),再让r10加0x50,如果当前栈大于r10,会跳转到loc_140427EAC处;否则将当前栈与r10减0x6000后比较,如果当前栈不小于r10,则会跳转执行KiDispatchException函数,反之执行到到loc_140427EAC处。
大致上检测用不用IsrStack。loc_140427EAC处,让r10等于IsrStack,如果当前栈大于r10(IsrStack),则跳转到loc_140427EC6处;否则将当前栈与r10减0x6000后比较,如果当前栈不小于r10则跳转执行KiDispatchException函数,反之执行到loc_140427EC6处。
loc_140427EC6处,让r10等于栈基址StackBase,如果rsp大于r10,则跳转到loc_140427EE4处;否则将当前栈与r10减0x6000之后比较,如果当前栈不小于r10,则跳转执行KiDispatchException函数,反之执行到loc_140427EE4处。
loc_140427EE4处,如果Nmi或Mce有活动性(CombinedNmiMceActive不为0),则执行KiDispatchException函数,反之执行KiExceptionDispatchOnExceptionStack函数。
这个流程一般是执行KiDispatchException函数的,这个函数会在后面讲解。
执行完KiDispatchException函数或KiExceptionDispatchOnExceptionStack函数后,恢复xmm6-xmm15寄存器、rbx、rdi、rsi、r12、r13、r14、r15,关闭中断。
判断处理器是否支持CET影子堆栈,如果支持的话还要将影子堆栈增加8个字节。(猜测)
随后,根据cs判断是从r0来的还是r3来的。
如果是从r0来的,将MXCSR寄存器设置为rbp-0x54里的值即栈上_KTRAP_FRAME
结构的ULONG MxCsr
的值,恢复xmm0-xmm5寄存器, 恢复r11、r10、r9、r8、rdx、rcx、rax、rbp、rsp,随后函数返回 。
如果是从r3来的,判断KeSmapEnabled标志位,是否开启Smap,如果开启的话需要执行一下stac。
!
之后来到loc_140427F84处,判断当前线程是否有有等待执行的用户apc,如果有,则去执行,直到执行完毕。
随后执行到loc_140427FB1处,如果gs:27E PairRegister (建立内核对象之间的联系)第二位不为1,则需要将rcx清0后执行KiUpdateStibpPairing函数,然后执行到loc_140427FC3处。
loc_140427FC3处,判断线程对象头_DISPATCHER_HEADER 的第32位是否为1 (判断线程是否失效? 猜测),如果为1,则需要调用KiRestoreSetContextState函数重新设置上下文状态再继续执行。
随后,判断线程对象头_DISPATCHER_HEADER 的第17位(猜测:周期分析,CPU使用情况 猜测),如果为0,则直接跳转到loc_140427FFE处;如果为1,则需要再判断一遍,如果还为1,则需要执行KiCopyCounters函数,让rcx等于当前线程对象结构,随后执行到loc_140427FFE处。
loc_140427FFE处,将MXCSR寄存器设置为栈上_KTRAP_FRAME
结构的ULONG MxCsr
成员的值。
判断DR7寄存器是否为0(是否有硬件断点),如果不为0则需要调用KiRestoreDebugRegisterState函数重置调试寄存器状态再继续执行。
判断线程**_KTHREAD** +74个字节偏移处的 第22位 (是否支持CET内核影子堆栈)。如果为0,进位标志位CF为0,跳转到loc_14042804B处;如果为1,即支持CET机制,进位标志位CF为1,让当前的影子堆栈与内核影子堆栈的栈底加8相比较,如果不相等,跳转到loc_14042804B处执行;如果相等,将影子栈设置为TransitionShadowStack 过渡影子堆栈,在先前的影子栈上保存一个恢复影子堆栈token,然后执行到loc_14042804B处。
loc_14042804B处,恢复xmm0-5、r11、r10、r9、r8,将_KPRCB
结构的+6d3偏移处的BpbRetpolineState成员(跟踪PRCB结构所属处理器的返回重定向状态)设置为0。
判断当前特权级别用用户特权级别是否相等,如果不相等,需要让当前特权级别等于用户特权级别,并将用户特权级别写到MSR的0x48处,随后继续执行。
判断异常处理完成后需不需要将处理器状态切换回用户模式,读取_KPRCB
结构的BpbIbpbOnReturn字段的值,并将其设置为0,如果BpbIbpbOnReturn字段的值为1(表示需要将处理器状态切换回用户模式),则需要将1写入MSR这组寄存器的0x49处,随后继续执行。
随后,处理控制流保护(这里不做详细解释),执行到loc_140428203处。
恢复rdx、rcx、rax、rbp、rsp。
判断KvaShadow,如果为1,跳转到KiKernelExit函数执行;如果为0,切换一下gs,函数返回。
KiDispatchException
此函数建议用伪代码+汇编分析,但是伪代码应该是有错误的。
内核模式
由KiExceptionDispatch 调用后,参数是 _EXCEPTION_RECORD*
,_KEXCEPTION_FRAME*
,_KTRAP_FRAME*
还有PreviousMode先前模式和是不是第一次机会。
开始主要做了让局部变量等于参数,获取当前线程、进程,异常派遣次数+1。
如果是第一次机会并且EProcess不为0并且是pico进程 ,则将异常码转换。
在满足上面的条件下,如果当前的IRQL小于2,并且先前模式为用户模式或异常为0xC0000005、0x80000001、0xC0000006,还要满足ExceptionInformation <=0x7FFFFFFF0000 即地址要在用户模式下,这样的话就会调用一个别的异常处理函数xmmword_140C37D20,如果该函数正确处理,则返回这个函数的返回值,如果没有处理成功,就把ExceptionCode还原回去,按照正常的处理。
判断先前模式。
如果是内核模式,若处理器支持CET机制,rbp+8里的值为0x10009F,若不支持CET机制,则rbp+8里的值为0x1000F,之后执行到loc_140274DED处。
如果是用户模式,将变量KeFeatureBits和0x800000进行与运算即判断变量KeFeatureBits第23位(从0开始算)是否为0,如果为0,则跳转到loc_140274DED处,如果不为0,则r14里的值为0x10005F,rbp+8里的值也为0x10005F;将地址为0x0FFFFF780000003EC全局变量里的值跟2做与运算即判断第1位是否为0,如果为0,跳转到loc_140274DED处执行,如果不为0,将0x0FFFFF78000000708里的值跟0x0FFFFF780000003D8里的值做逻辑或运算,然后判断结果v20的第11位,如果为0,跳转到loc_14048AD78处执行,如果为1,判断_EPROCESS的+0x9D4偏移处的CetUserShadowStacks字段即判断用户模块下的影子堆栈状态,如果为0,将v20的第11位复制到CF标志位并将其置0,随后执行到loc_14048AD78处。
在loc_14048AD78处,将v20跟0xFFFFFFFFFFF9FFFF做与运算,结果放到v11中,再判断_EXCEPTION_RECORD
结构ExceptionFlags成员的第7位是否为0,如果为0,v11等于v20,随后,以_EPROCESS结构体为参数调用PsWow64GetProcessMachine,如果返回值等于0x14C,需要将v11与0x0FFFFFFFFFFF9FFFF做与运算,然后执行到loc_140274DED处。
loc_140274DED处,调用RtlGetExtendedContextLength2,获取扩展上下文长度,经过分析第二个参数为所获取的长度,与CET机制有关,随后会调用RtlInitializeExtendedContext2,初始化扩展上下文,然后调用KeContextFromKframes,将KTRAP_FRAME
和EXCEPTION_FRAME
复制到 _CONTEXT
。
如果异常码为0x80000003,跳转到loc_140274F0A处执行。
rip-1,这个_CONTEXT.RIP是从_KTRAP_FRAME
获取的,_KTRAP_FRAME
结构上面之前并没有减1, 所以在这里减1, 并不是减了两次。
判断KiDynamicTraceMask第一位即判断是否开启了ETW记录,如果开启了ETW,_KTRAP_FRAME.Rip
减1。调用KiTpHandleTrap交给内核调试器处理,如果内核调试器成功处理,则KeContextToKframes恢复后返回,如果没有内核调试器或者内核调试器没有处理异常,_KTRAP_FRAME.Rip
加1,然后执行到loc_140274E66处。
如果没有开启ETW或者异常码不为0x80000003,跳转到loc_140274E66处执行。
!
调用KiPreprocessFault预处理,如果异常是内部通用保护错误,无效操作码或者除零错误,则尝试在不实际引发异常的情况下解决问题。
如果KiPreprocessFault预处理成功,则KeContextToKframes恢复后返回。
如果KiPreprocessFault预处理没有成功,判断先前模式。
如果先前模式为内核模式,判断是不是第一次机会。
如果是第一次机会,发给内核调试器处理,内核调试器处理成功的话,则KeContextToKframes恢复后返回;内核调试器没有处理成功的话,调用RtlDispatchException(内核SEH,内核中没有VEH),处理成功的话,KeContextToKframes恢复后返回,没有处理成功的话,再发给调试器处理,处理成功的话,KeContextToKframes恢复后返回,如果还没有处理成功的话就会蓝屏。
如果不是第一次机会,发给内核调试器处理,处理成功的话,调用KeContextToKframes恢复后返回;处理不了的话就会蓝屏。
内核模式异常处理大致流程图:
用户模式
如果先前模式为用户模式。
判断_EPROCESS.Flags3.Minimal,判断是否是最小进程。
如果不为0,跳转到loc_14048AF13处执行;如果为0,判断当前进程是否为32位进程,如果为64位进程,跳转到loc_14048AECD处执行;如果为32位进程,判断异常码是否是0x80000002。
如果异常码不是0x80000002,跳转到loc_14048AECD处执行;如果异常码是0x80000002,判断EFlags寄存器第18位即判断对齐检查,如果为0,跳转到loc_14048AECD处执行。
loc_14048AECD处,将cs与0xFFF8 进行逻辑与操作,判断结果的第5位。
第5位为0,cmp 后 执行 jnz 跳转到loc_14048AF13处执行。
第5位为1,如果异常码是0x80000003,则将异常码改为0x4000001F;如果异常码是0x80000004,则将异常码改为0x4000001E。
将_CONTEXT.RSP以16个字节对齐后赋值,随后执行到loc_14048AF13处。
loc_14048AF13处,判断是否是第一次机会。
如果是第一次机会,调用KdIsThisAKdTrap,后面会根据此函数的返回值判断是否需要内核调试器处理,
随后判断当前线程所在进程的调试端口是否为0,如果不为0则跳转到loc_14048AF5F处。如果为0,则判断KdIgnoreUmExceptions即判断内核调试器是否忽视此异常,如果为1表示忽视,跳转到loc_14048AF5F处执行,如果为0表示不忽视,判断异常码是否是0x80000033,如果是执行到loc_14048AF5F处,如果不是,跳转到loc_14048AF63处执行。
在loc_14048AF5F处,判断KdIsThisAKdTrap函数返回值。
如果返回值为1,执行到loc_14048AF63处,发给内核调试器处理,如果内核调试器处理成功,调用KeContextToKframes恢复 _KTRAP_FRAME
和 _EXCEPTION_FRAME
后返回,如果内核调试器处理失败,让r9等于异常码,然后执行到loc_14048AF8B处。
如果返回值为0,跳转到loc_14048AF8B处执行。
loc_14048AF8B处,判断异常码是否是0x80000033,如果是,跳转到loc_14048AFAA处执行;如果不是,发给用户态调试器处理。
如果处理成功,跳转到loc_140274EEA处恢复寄存器后返回;如果没有用户态调试器或处理失败,则会执行到loc_14048AFAA处。
loc_14048AFAA处,关闭中断,将_KTRAP_FRAME.EFlags的第8位(TrapFlag,陷阱标志,单步调试)复制到CF并置为0。随后判断_KPCR._PRCB.SchedulerAssist
,如果为0,跳转到loc_14048AFE7处执行,如果不为0,执行一些操作后也会执行到loc_14048AFE7处。
loc_14048AFE7处,打开中断,进行一些赋值操作,判断r14d的第6位与第20位。
如果任意一个为0,跳转到loc_14048B026处执行。
如果都为1,则需要进行一些赋值操作,随后执行到loc_14048B026处。
loc_14048B026处,简单来说,调用ProbeForWrite测试一下地址是否可写、是否正确对齐、没有越界、正常。调用KeCopyExceptionRecord复制异常信息。调用RtlpCopyExtendedContext复制contextex。调用KePopulateContinuationContext(此函数大致内容为如果线程_KTHREAD.MiscFlags.CetUserShadowStack`支持cet机制,操作msr的寄存器的0x6A7位置)。
执行完这几个函数后,调用KiSetupForInstrumentationReturn,设置进入到用户态的地址为r3的异常分发函数KeUserExceptionDispatcher(_TRAP_FRAME.Rip
),这里的KeUserExceptionDispatcher
存放的就是用户态下KiUserExceptionDispatcher
,设置新的CS寄存器(_TRAP_FRAME.Rip
),设置用户态栈顶(_TRAP_FRAME.Rsp
)。
之后进行一些操作如执行apc等 然后返回。
第一次异常分发完毕。
第二次异常分发(第二次机会),调用DbgkForwardException(第二个参数为1时,发给用户调试器;为0时,发给“监控程序”)发给用户态调试器,如果没有用户态调试器或没有处理成功,那么会再次调用DbgkForwardException发给监控程序_EPROCESS.ExceptionPort
处理,如果还是没有返回false,则ZwTerminateProcess结束进程。这里直接贴上伪代码了。
用户模式异常处理大致流程图:
总执行大致流程图
KiUserExceptionDispatcher
此函数在ntdll.dll里。
来看一下进入用户态时的栈:
将DF设置为0,判断是否是32位程序发生的异常,如果是,则需要调用Wow64PrepareForException 然后执行到loc_1800A347C处;如果不是,则直接跳转到loc_1800A347C处。
loc_1800A347C处,让rcx等于当前的栈顶,随后将栈增加(下移)4F0之后,再让rdx等于栈顶。以栈上_CONTEXT
和_KEXCEPTION_FRAME
结构为参数调用RtlDispatchException(此函数后面会讲)处理。
如果处理成功, 调用RtlGuardRestoreContext, ,进入r0,将Context和ExpetionRecord恢复到 TrapFrame和ExceptionFrame ,再返回到r3。
如果处理失败,调用ZwRaiseException触发第二次异常。
RtlDispatchException
简单讲讲。
调用RtlpLogExceptionDispatch将异常记录到日志中。
以Context、ExceptionRecord以及0为参数调用RtlpCallVectoredHandlers(VEH),第三个参数是0的话就是VEH, 1的话就是VCH。VCH其实没干什么事。
RtlpCallVectoredHandlers返回值如果为1即处理成功,跳转到loc_18001E543处,让bl等于1,随后执行到loc_18001E545处。
在loc_18001E545处,以Context、ExceptionRecord以及1为参数再次调用RtlpCallVectoredHandlers(VCH),然后让al等于bl,也就是本函数的返回值(如果经过loc_18001E543处将bl赋值为1后,此函数返回1),随后进行一些收尾工作后返回。
RtlpCallVectoredHandlers返回值如果为0即处理失败,之后主要调用RtlpExecuteHandlerForException(结构化异常处理,SEH)。
如果没有调用到SEH(由于某些条件未符合),跳转到了loc_18001E545处,本函数会返回0。
如果SEH可以处理,则去处理处。
如果SEH返回值为1,要么通过其它方式去处理,要么本函数返回0。
如果SEH的返回值为2,会继续遍历SEH。
如果SEH返回0(这种情况很少会遇到),本函数很可能会返回1。
SEH具体处理方可能会因为编译器的不同而不同。
RtlpCallVectoredHandlers(VEH or VCH)
将参数复制到栈上,保存非易失寄存器,提升栈,让rbp等于LdrpVectorHandlerList(LdrpVectorHandlerList在.mrdata段上,这个段每个进程是不共享的,如果写这个段会发生写拷贝,所以每个进程的VEH链表不共享)。
VEH的链表头是 LdrpVectorHandlerList+8 VCH的链表头是 LdrpVectorHandlerList+3×8。
如果函数第三个参数为0(VEH),则将_PEB.CrossProcessFlags.ProcessUsingVEH
位复制到CF,如果此位为1,则跳转到loc_180077A09处,如果此位为0则恢复栈和寄存器后返回。
如果函数第三个参数为1(VCH),则将_PEB.CrossProcessFlags.ProcessUsingVCH
位复制到CF,如果此位为1,则跳转到loc_180077A09处,如果此位为0则恢复栈和寄存器后返回。
当前线程获取SRW锁的所有权,LIST_ENTRY结构,判断链表是否是空的,如果是,释放锁后又进行一些与.mrdata段相关的操作(这里不详细讲解);如果不是,执行到loc_180077A2B处。
loc_180077A2B处,让r12等于r14+0x10,rsi等于r14,rax等于r12里的值,后面是处理多线程问题,原子锁,可以推断出 +0x10 位置是一个与计数相关的成员,正常的话会执行到loc_180077A4D处。
,则将cookie赋给变量CookieValue,随后执行到loc_180077A69处。
loc_180077A69处,主要是一个解码操作,可以推断出 +0x20 处为加密过的VehHandler。
结构分析为: 0x0 LIST_ENTRY 0x10 CountPtr 0x18 unknown 0x20 加密过的vehhandler。
后面的东西,一个与mrdata节相关的操作就不做过多解释,简单来说,如果VEH处理了,sil是1,赋值给al,函数返回值为1,
如果没有找到可以接管处理的VehHandler,则sil是0,赋值给al,函数返回值为0。
演示
(●’◡’●)
ps:.md文件直接导入的,可能有些格式或图片错误,可以私信我拿.md文件,