Windows内核基础知识——32位
这次真不会鸽了,64位Windows内核那个API逆向我基础太差,写出来怕笑话,所以重新过一下32位,再接着去学64位
Typora图片无法直接加载出来,只能一个一个的上传图床了。
所以先传一点简单的逆向把,基础知识目前还没时间上传图床,只能先上传下文本了。
emm,API分析的差不多了,后续的学习打算参考Reactor OS、Windows 情景分析、Windows内核原理实现、Windows驱动开发、寒江独钓这些资料先看看,这篇博客等有心情写的时候在写写吧。
可惜的是 内存、调试、异常这一块还没有去逆向过。
异常得抽个时间好好逆一下,毕竟涉及到某个杀软的查杀
系统调用
_KUSER_SHARED_DATA
ring0层和ring3层的共享数据
User层地址为:0x7ffe0000
Kernnel层地址为:0xffdf000
但User层是只读的,在Kernel层是可写的
_Trap_Frame
ring3进ring0需要切换_Trap_Frame
结构如下
而在Windows xp下进入ring0有两种方式,一种是KiSystemService,一种是KiFastCallEntry
KiSystemService
.text:004067C1 _KiSystemService proc near ; CODE XREF: ZwAcceptConnectPort(x,x,x,x,x,x)+C↓p
.text:004067C1 ; ZwAccessCheck(x,x,x,x,x,x,x,x)+C↓p ...
.text:004067C1
.text:004067C1 arg_0 = dword ptr 4
.text:004067C1
.text:004067C1 push 0
.text:004067C3 push ebp ; push 0 和下面的push都是填充Trame_Frame
.text:004067C4 push ebx
.text:004067C5 push esi
.text:004067C6 push edi
.text:004067C7 push fs
.text:004067C9 mov ebx, 30h ; '0' ; 切换fs寄存器
.text:004067C9 ; ring0的fs寄存器指向KPCR
.text:004067CE mov fs, ebx
.text:004067D0 push large dword ptr fs:0 ; 将异常Exception链压入栈中
.text:004067D7 mov large dword ptr fs:0, 0FFFFFFFFh ; 赋值Exception链为-1
.text:004067E2 mov esi, large fs:124h ; 传入KThread结构体
.text:004067E9 push dword ptr [esi+140h] ; KThread->PreviousMode 保存调用该函数的调用者的模式是ring0还是ring3
.text:004067EF sub esp, 48h ; 提升0x48个字节,就是剩下的Trap_Frame的成员
.text:004067F2 mov ebx, [esp+68h+arg_0]
.text:004067F6 and ebx, 1
.text:004067F9 mov [esi+140h], bl ; 将当前的模式压入KThread->PreviousMode
.text:004067FF mov ebp, esp
.text:00406801 mov ebx, [esi+134h]
.text:00406807 mov [ebp+3Ch], ebx ; 将当前KThread的TrapFrame放入TrapFrame->edx中
.text:0040680A mov [esi+134h], ebp ; KThread->TrapFrame=Cur_TrapFrame
.text:0040680A ; 此时的TrapFrame利用链表串了起来
.text:00406810 cld ; Flag->DF=0
.text:00406811 mov ebx, [ebp+60h] ; ebp=TrapFrame->ebp
.text:00406814 mov edi, [ebp+68h] ; edi=TrapFrame->eip
.text:00406817 mov [ebp+0Ch], edx ; TrapFrame->DbgArgPointer=edx(调用者API的第一个参数)
.text:0040681A mov dword ptr [ebp+8], 0BADB0D00h ; TrapFrame->DbgArgMark=0xBADB0D00
.text:00406821 mov [ebp+0], ebx ; [ebp]=TrapFrame->ebp
.text:00406824 mov [ebp+4], edi ; ebp+4(返回地址)=TrapFrame->eip
.text:00406827 test byte ptr [esi+2Ch], 0FFh ; 是否为调试模式
.text:0040682B jnz Dr_kss_a ; 为调试模式则跳出
.text:00406831
.text:00406831 loc_406831: ; CODE XREF: Dr_kss_a+10↑j
.text:00406831 ; Dr_kss_a+7C↑j
.text:00406831 sti
.text:00406832 jmp loc_406922 ; 进入KiFastEntry
.text:00406832 _KiSystemService endp
KiFastCallEntry
.text:0040688F _KiFastCallEntry proc near ; DATA XREF: _KiTrap01+71↓o
.text:0040688F ; KiLoadFastSyscallMachineSpecificRegisters(x)+24↓o
.text:0040688F
.text:0040688F var_B = byte ptr -0Bh
.text:0040688F
.text:0040688F ; FUNCTION CHUNK AT .text:0040685C SIZE 00000024 BYTES
.text:0040688F ; FUNCTION CHUNK AT .text:00406B33 SIZE 00000014 BYTES
.text:0040688F
.text:0040688F mov ecx, 23h ; '#'
.text:00406894 push 30h ; '0'
.text:00406896 pop fs ; fs=0x30
.text:00406898 mov ds, ecx ; ds=0x23
.text:0040689A mov es, ecx ; es=0x23
.text:0040689C mov ecx, large fs:40h ; ecx=TSS
.text:004068A3 mov esp, [ecx+4] ; esp=TSS->esp0(将ring0的esp传给esp)
.text:004068A6 push 23h ; '#' ; HardWareSS
.text:004068A8 push edx ; HardWareEsp
.text:004068A9 pushf ; TrapFrame->eflag
.text:004068AA
.text:004068AA loc_4068AA: ; CODE XREF: _KiFastCallEntry2+23↑j
.text:004068AA push 2
.text:004068AC add edx, 8 ; edx=esp+8(ring3 args)
.text:004068AF popf
.text:004068B0 or [esp+0Ch+var_B], 2
.text:004068B5 push 1Bh ; Trap->SegCs
.text:004068B7 push dword ptr ds:0FFDF0304h ; _KTRAP_FRAME.Eip = _KUSER_SHARED_DATA.SystemCallReturn
.text:004068BD push 0
.text:004068BF push ebp
.text:004068C0 push ebx
.text:004068C1 push esi
.text:004068C2 push edi
.text:004068C3 mov ebx, large fs:1Ch
.text:004068CA push 3Bh ; ';'
.text:004068CC mov esi, [ebx+124h]
.text:004068D2 push dword ptr [ebx]
.text:004068D4 mov dword ptr [ebx], 0FFFFFFFFh
.text:004068DA mov ebp, [esi+18h]
.text:004068DD push 1
.text:004068DF sub esp, 48h
.text:004068E2 sub ebp, 29Ch ; stack-=0x29C
.text:004068E8 mov byte ptr [esi+140h], 1 ; 从3环调用过来
.text:004068EF cmp ebp, esp
.text:004068F1 jnz loc_40685C
.text:004068F7 and dword ptr [ebp+2Ch], 0
.text:004068FB test byte ptr [esi+2Ch], 0FFh
.text:004068FF mov [esi+134h], ebp
.text:00406905 jnz Dr_FastCallDrSave
.text:0040690B
.text:0040690B loc_40690B: ; CODE XREF: Dr_FastCallDrSave+10↑j
.text:0040690B ; Dr_FastCallDrSave+7C↑j
.text:0040690B mov ebx, [ebp+60h]
.text:0040690E mov edi, [ebp+68h]
.text:00406911 mov [ebp+0Ch], edx
.text:00406914 mov dword ptr [ebp+8], 0BADB0D00h
.text:0040691B mov [ebp+0], ebx
.text:0040691E mov [ebp+4], edi
.text:00406921 sti
.text:00406922
.text:00406922 loc_406922: ; CODE XREF: _KiBBTUnexpectedRange+18↑j
.text:00406922 ; _KiSystemService+71↑j
.text:00406922 mov edi, eax
.text:00406924 shr edi, 8 ; 此时的eax是系统调用号
.text:00406924 ; edi=(eax>>8)&0x30
.text:00406927 and edi, 30h ; 查看是Ntoskrl.exe还是Win32k.sys
.text:0040692A mov ecx, edi
.text:0040692C add edi, [esi+0E0h] ; 系统调用号+系统服务表
.text:00406932 mov ebx, eax
.text:00406934 and eax, 0FFFh
.text:00406939 cmp eax, [edi+8] ; 比较系统调用号是否超过了系统服务表
.text:0040693C jnb _KiBBTUnexpectedRange
.text:00406942 cmp ecx, 10h ; 比较第12位是否为1
.text:00406942 ; 决定着是否为Ntoskrl.exe或Win32k
.text:00406945 jnz short loc_406962 ; KeSystemCalls
.text:00406947 mov ecx, large fs:18h ; KPCR->Self
.text:0040694E xor ebx, ebx
.text:00406950
.text:00406950 loc_406950: ; DATA XREF: _KiTrap0E+114↓o
.text:00406950 or ebx, [ecx+0F70h]
.text:00406956 jz short loc_406962 ; KeSystemCalls
.text:00406958 push edx
.text:00406959 push eax
.text:0040695A call ds:_KeGdiFlushUserBatch
.text:00406960 pop eax
.text:00406961 pop edx
.text:00406962
.text:00406962 loc_406962: ; CODE XREF: _KiFastCallEntry+B6↑j
.text:00406962 ; _KiFastCallEntry+C7↑j
.text:00406962 inc large dword ptr fs:638h ; KeSystemCalls
.text:00406969 mov esi, edx ; fs[638h]=KeSystemCalls
.text:0040696B mov ebx, [edi+0Ch] ; ebx=ArgmentTable
.text:0040696E xor ecx, ecx
.text:00406970 mov cl, [eax+ebx] ; cl=SystemServiceTable.ArgmentTable[eax]
.text:00406970 ; 获取参数字节数
.text:00406973 mov edi, [edi]
.text:00406975 mov ebx, [edi+eax*4] ; 获取函数地址
.text:00406978 sub esp, ecx ; 为参数 拉伸栈空间
.text:0040697A shr ecx, 2 ; 获取参数个数
.text:0040697D mov edi, esp
.text:0040697F cmp esi, ds:_MmUserProbeAddress ; 验证目标地址是否可用
.text:00406985 jnb loc_406B33 ; 查看cs是否为01
.text:0040698B
.text:0040698B loc_40698B: ; CODE XREF: _KiFastCallEntry+2A8↓j
.text:0040698B ; DATA XREF: _KiTrap0E+10A↓o
.text:0040698B rep movsd
.text:0040698D call ebx ; 调用系统函数
.text:0040698F
.text:0040698F loc_40698F: ; CODE XREF: _KiFastCallEntry+2B3↓j
.text:0040698F ; DATA XREF: _KiTrap0E+12A↓o ...
.text:0040698F mov esp, ebp
.text:00406991
.text:00406991 loc_406991: ; CODE XREF: _KiBBTUnexpectedRange+38↑j
.text:00406991 ; _KiBBTUnexpectedRange+43↑j
.text:00406991 mov ecx, large fs:124h ; ecx=KThread
.text:00406998 mov edx, [ebp+3Ch] ; edx=TrapFrame->edx
.text:0040699B mov [ecx+134h], edx ; TrapFrame=Previous_TrapFrame
.text:0040699B _KiFastCallEntry endp
线程切换
本来想逆向一下KiSwapThread的,然后逆着逆着看到了自旋锁,索性把自旋锁也给逆了。
在逆的过程中,还好一年前已经看过这个课程了,所以简单的介绍一些成员
为什么没放逆向的过程,因为我想直接放idb好一些,下面的过程可能还会有些错误。。
过程
线程切换的主要API是KiSwapThread,该API首先会通过KPRCB,找到下一个线程的KTHREAD结构体,然后获取当前CPU编号,随后找到就绪的线程。
接着判断是否查找到,如果查找到了则直接进入KiSwapContext,进行上下文切换,如果没有找到,则需要对空闲线程进行处理,具体操作,目前实力还不行。
然后进入KiSwapContext进入线程上下文切换,其主要操作会切换当前KPRCB离的KTHREAD,接着进入SwapContext
进入后会对KThread->State状态进行赋值为2,切换线程当前的状态为(),紧接着会对DebugActive赋值给KPCR中,在把栈空间进行切换和提升0x210,并查看是否开启了保护模式,接着切换TSS指针,和KTHREAD->TEB和把新的KPCR赋值进去再开启中断。再填充GDT和IDT和TSS里面的cr3,最后把原来的ExceptionList传入新的KPCR中。
逆向分析KiSwapContext和SwapContext函数,解决如下问题:
1、SwapContxt有几个参数,分别是什么?你是如何判断出来参数的?
3
2、SwapContxt在哪里实现了线程切换?
从切换KPCR中的栈空间开始
3、线程切换的时候,会切换Cr3吗?切换Cr3的条件是什么?
会,有3个地方会判断(这个成员代表是否需要切换块意思是否切换cr3)
4、中断门提权时,CPU会从TSS得到ESP0和SS0,TSS中存储的一定是当前线程的ESP0和SS0吗?如何做到的?
是的,因为线程切换回修改KPCR中的TSS指针值,而KPCR就是由当前线程切换的,所以会去TSS中取的值
5、FS:[0]在3环时指向TEB 但是 线程有很多 FS:[0]指向的是哪个线程的TEB 如何做到的?
fs段选择子虽然没有变化,但会改变fs段选择子里的值
6、0环的ExceptionList在哪里备份的?
开头有push ecx
7、IdleThread是什么?什么时候执行?找到这个函数?
8、如何找到下一个就绪线程?
通过KiFindReadyThread来找到就绪线程
APC执行
在逆向TerminateThread/SuspendThread/ResumeThread这三个API中,发现3个API都将会调用KiInsertQueueApc,而该API在ring0中主要是直接对线程的APC中插入一个_KAPC结构体,在介绍过程之前先简单的介绍一下相关的结构体
_KAPC
nt!_KAPC
+0x000 Type
+0x002 Size
+0x004 Spare0 找到你提供的APC函数,并不完全等于
+0x008 Thread APC函数的地址,后面会讲。
+0x00c ApcListEntry
+0x014 KernelRoutine
+0x018 RundownRoutine
+0x01c NormalRoutine
+0x020 NormalContext
+0x024 SystemArgument1
+0x028 SystemArgument2
+0x02c ApcStateIndex
+0x02d ApcMode
+0x02e Inserted
_KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead //2个APC队列 用户APC和内核APC
+0x010 Process //线程所属或者所挂靠的进程
+0x014 KernelApcInProgress //内核APC是否正在执行
+0x015 KernelApcPending //是否有正在等待执行的内核APC
+0x016 UserApcPending //是否有正在等待执行的用户APC
TerminateThread过程
从ring3开始逆的话,其实也差不了太多,因为我还没逆APC执行这一块,主要还是逆的APC插入到执行的一个过程,这样可以真正明白APC。其次是API的逆向,可能只会对重要的逆向,如果流程基本差不多,便不会在详说
1.进入NtTerminateThread后,通过_KPCR.PrcbData.CurrentThread来获取当前ETHREAD,然后通过ObReferenceObjectByHandle来获取目标ETHREAD对象,然后跳入PspTerminateThreadByPointer。
2.进入PspTerminateThreadByPointer会判断当前的fs的ETHREAD和目标线程的ETHREAD是否为同一个,如果为同一个则直接退出,如果不为同一个,判断目标线程是否为系统线程。
3.如果是系统线程则分配一个_KAPC结构体,然后利用API KeInitializeApc进行APC的初始化,KeInsertQueueApc插入APC。
4.接着调用KeForceResumeThread恢复线程执行,该API里面又会调用KiUnlockDispatcherDatabase来执行APC。
SuspendThread
API流程:KiInsertQueueApc
当线程挂起,会被插入APC,但不会调用
ResumeThread
当线程恢复,会调用APC
KiInsertQueueApc
该函数主要是插入APC
过程
1.检查_KAPC.Inserted是否为0,为0代表还未插入,不为0代表插入成功
2.将创建的APC先赋值,这样方便要插入的目标线程,然后赋值了ApcStateIndex,然后通过ApcStatePointer来找到APC_STATE队列。
3.然后判断当前KAPC是用户态APC还是内核态的APC,紧接着通过ApcStateIndex来找到对应的(用户态或者是内核态)APC_STATE,并且插入,并且将_KAPC_Inseted置为1,如果插入的是用户态的APC则还会将UserApcPending为1,这说明当前有未处理完的APC操作
4.然后回判断内核_KAPC.NormalRoutine是否为0,判断是用户APC还是内核APC,如果是内核APC,则判断当前线程是否为running状态,并且把KernelApcPending为1,代表有需要执行的APC,如果是running状态,则APC中断,如果不是running状态则判断其是不是等待状态,判断中断请求等级是不是0,,然后查看NormalRoutine是否为0,如果以上条件都不满足则退出,否则会继续查看当前APC是否可用,是否有内核APC正在执行,如果没有内核APC执行,设置线程优先级和状态。如果是用户态APC,判断当前线程是否为等待,然后查看等待模式是否不为用户导致的阻塞,检查当前线程是否可以被APC唤醒和APC是否还有执行,如果都满足,则跳入设置线程优先级和状态。
5.如果APC在运行并且,则进入APC中断
6.如果NormalRoutine为0,则进入循环,该循环会遍历当前线程的APC队列,直到找到下一个 NormalRoutine 为空的APC。
KiDeliverApc
该函数主要是执行APC和构造ring3APC环境的
过程
该API主要是执行APC的,APC分为用户APC和内核APC,内核APC相对简单,判断玩是否被禁止执行后,再直接执行就可以,而用户APC需要先执行完内核APC再去执行用户APC,这当中会有ring0和ring3的切换,所以该API在末尾会调用KiInitializeUserApc来构造ring3的环境,用户后门的环境切换,并且会把ntdll里的函数KiUserApcDispatcher来执行用户APC。
执行内核APC的代码段
xor cl, cl ; NewIrql
mov byte ptr [esi+48h], 1
call ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
push [ebp+temp_kacp_systemArgument2]
push [ebp+temp_kapc_systemArgument1]
push [ebp+temp_kapc_normalContext]
call [ebp+temp_kapc_normalRoutine] ; 执行真正的APC
mov cl, 1 ; NewIrql
call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x)
mov [ebp+LockHandle.OldIrql], al
NtReadVirtulMemoery
会调用KeStackAttachProcess
KeStackAttachProcess
该API跟挂靠相关,会通过当前线程里APC_STATE里的ApcStateIndex来判断当前线程是否挂靠了,然后调用KiAttachProcess,如果挂靠了会传入一个还未填充完的APC_STATE,否则传入备份的SavedApcState。KiAttachProcess首先会调用KiMoveApcState拷贝一份APC_STATE,然后将当前线程的APC_STATE进行清理,并在挂靠情况下会有不一样的赋值
mov [esi+_ETHREAD.Tcb.ApcStatePointer], eax
mov [esi+13Ch], ebx
mov [esi+_ETHREAD.Tcb.ApcStateIndex], 1
接着会调用KiSwapProcess,这个API主要就是切换进程的cr3也就是切换KPCR里的TSS里的cr3部分。同时该函数还会修改GDT和IDT表里面的部分值。
KeUnstackDetachProcess
异常
Windows在处理异常时,主要会经过如下步骤
异常记录、异常分发、异常处理
而在进行这些步骤时有几个很重要的结构体
type struct _EXCEPTION_RECORD
{
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags; //异常状态
struct _EXCEPTION_RECORD* ExceptionRecord; //下一个异常
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters; //附加参数个数
ULONG_PTR ExceptionInformation
[EXCEPTION_MAXIMUM_PARAMETERS]; //附加参数指针
}
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
int 0
在逆之前,我们需要知道0号异常是除0异常
1.0号异常对应的函数是_KiTrap00,首先会保存触发异常时的环境Trap_Frame
2.判断RPL是否为0,如果为0则直接进入CommonDispatchException,如果是3则判断是否为CS段寄存器是否为0x1B,如果是则进入CommonDispatchException,如果不是则跳入Vdm那一块地方,并将eax设置为0xc000094(未处理异常)进入CommonDispatchException.
CommonDispatchException
1.先初始化结构体_Exception_Record
2.判断调用者是不是ring3来的,如果是ring3过来的,则eax=0xffff
3.调用KiDispatchException
KiDispatchException
内核模式派发
1.先将KPCR里的KeExceptionDispatchCount派发数量加一,并判断是否为用户模式进入的
2.如果是内核模式则会判断是否为调试状态,接着调用KeContextFromKFrames将Trap_Frame,也就是将调用者之前的环境拷贝一份放入Context,然后再判断一次调用者是否为用户模式,再Context里的Eip减一
3.接着检查内核调试器是否存在,如果不存在,就调用RtlDispatchException
4.调用完后查看异常是否被正确处理,并再检查一遍内核调试器是否存在,如果被正确处理,则将Context拷贝一份到Trap_Frame里面,否则调试器不存在蓝屏。
用户模式派发
1.构造了一份用户模式下的TrapFrame后,调用Kei386EoiHelper进入KeUserExceptionDispatcher
2.KeUserExceptionDispatcher会调用RtlDispatchException处理完用户模式下的异常后,如果没有问题,就会调用ZwContinue来恢复Trap_Frame里的eip,然后再回到原来的TrapFrame中
3.如果处理时出错了,此时调用RtlRaiseException,再一次触发异常。
RtlDispatchException
这个函数有个循环,对异常链表的循环,然后通过遍历链表先执行RtlpExceptionLogCount来拷贝异常日志,然后执行异常函数,执行完异常处理后,再把异常处理的结构也记录到最后一个异常日志中。如果异常函数再执行完后,成功返回1,遍历下一个,失败返回其他值,又会进入新的异常会调用RtlRaiseException。