系统调用
用户层的API调用最终都需要进入到0环,其功能才能够实现:
上图描述的就是整个windows操作系统的体系结构. Kernel32.dll
, user32.dll
等所有用户层DLL它们在调用API时,最终都会调用到ntdll.dll
中. ntdll.dll
是系统内核代码的存根部分,这个存根部分的代码非常的简单:
函数内部的逻辑都是一致的:
将一个数值保存到
eax
调用一个相同的函数:
edx
,实际这个函数是KiFastSystemCall
:
此函数内部也只做了两件事情, 将栈顶esp
保存到了edx
中. 然后执行sysenter
指令. 然后就进入到0环执行代码, 如果使用OD调试,那么调试就到此为止了, 内核代码的执行过程在OD中就跟不进去了. SYSENTER
这条指令执行后发生了什么? 为什么它当这条简单的指令执行完毕之后, API的功能就被实现了(创建文件,创建进程等等功能).
SYSENTER指令
无论windows系统上面有多少个用户层API , 最终都需要进入到内核层, 进入内核层的方法只有一个:SYSENTER
那么在进入0环之后, 内核该如何区分用户层到底要执行什么功能呢? 是创建进程呢,还是创建文件呢? 进入到0环之后, 如果要执行的功能是创建文件, 那么文件路径在哪里? 也就是函数的参数在哪里?
对于第一个问题, Windows使用调用号来解决. 在用户层调用SYSENTER
指令切换到0环之前, 每个函数都会将一个数值保存到eax
寄存器中, 这个数值就是一个调用号:
对于第二个问题, 由于每个线程都有一套线程上下文,都有一个独立的栈. 进入到内核后, 内核也会使用自己的内核栈. 因此, 在进入内核之前. 使用edx
寄存器保存了esp
的值
还有一个最重要的问题, SYSENTER
执行之后就进入到内核层, 那到底进入到内核层的哪个地址???
首先, SYSENTER
执行的时候, 会读取三个特殊寄存器,从这三个特殊寄存器中取出内核栈的栈顶(esp
) , 内核代码段段选择子(cs
) ,以及代码的首地址(eip
), 保存这三个值得寄存器是MSR(模组特殊寄存器)寄存器组. 这组寄存器没有名字, 只有编号, 由于没有名字, 无法通过正常的汇编指令来存取值, Intel提供了两条指令来读写这些寄存器:
rdmsr 读取MSR寄存器 其中高32位存放在EDX 低32位存放在EAX(64位和32位是一样,只是64位时rdx和rcx的高32位会被清零),使用ECX传递寄存器编号
wrmsr 写入MSR寄存器 和读取一样写入时是用EDX表示高32位,EAX表示低32位. 使用ECX传递寄存器编号 而上述三个值分别保存在MSR寄存器中的下面三个编号的寄存器中:
MSR编号
宏
作用描述
0x174
SYSENTER_CS_MSR
保存内核CS段选择子
0x175
SYSENTER_ESP_MSR
保存内核栈栈顶
0x176
SYSENTER_EIP_MSR
保存进入到内核之后要执行的函数地址,函数名实际是:KiFastCallEntry
也就是说, Windows在启动,进行初始化的时候会将内核栈栈顶,内核CS段选择子,以及KiFastCallEntry
函数的地址一一存放到MSR寄存器组的这几个编号的寄存器中. 当SYSENTER
被执行, CPU就直接使用这些寄存器的值来初始化真正的CS
,EIP
,ESP
寄存器. 因此, SYSENTER
执行之后, 就跑到内核的KiFastCallEntry
函数中执行代码了.
KiFastCallEntry分析
本函数的调用, 是因为用户层执行了SYSENTER
函数, 在执行这个指令时, 用户层将要执行的功能保存到了eax
寄存器中, 将函数的参数保存到了用户层的栈里, 栈顶地址被保存到了edx
寄存器中 , 进入到内核层后, ESP寄存器的值已经被替换成内核栈的栈顶, 和用户层的栈已经不再是同一个了.
如今, 这个函数要做的事情是这样的:
根据
eax
保存的调用号来调用相对应的函数, 来完成功能.由于权限有别, 在内核层中不能直接使用用户栈的内容 , 函数需要将保存在用户栈的参数拷贝到内核栈
要完成上述功能的时候, 函数需要几个信息:
哪个调用号对应哪个函数
例如在用户层中,调用CreateFileW时,使用了0x42的调用号,进入到内核层后,也需要根据0x42这个调用号找到内核版的创建文件的API.
用户层的API参数个数是不一样的, 在把用户栈中的参数拷贝到内核栈的时候,拷贝多少个字节?
为解决这两个问题, Windows设计了两张表, 一张被称之为函数表, 一张被称之为参数个数表. 这样一来, 通过调用号作为序号, 就能找到函数的地址, 同样, 通过调用号作为序号, 也能找到函数的参数个数, 使用参数个数乘以4(字节)就可以得到参数的总字节数. Windows内核把这张函数地址表称为 : 系统服务描述表(System Service Descriptor Table ) 简称SSDT.
但是, Windows内核实际设计了两张表, 一张专门用于保存和用户界面相关的服务(例如像创建窗口这种服务就保存在这张表里面),这张表被称为ShadowSSDT
, 而另一张则只保存非用户界面相关的系统服务,例如创建文件,创建进程等. 这两张表在内核中都使用了下面的结构体来表示:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; //函数地址表的首地址
PULONG ServiceCounterTableBase;// 函数表中每个函数被调用的次数
ULONG NumberOfService;// 服务函数的个数, NumberOfService * 4 就是整个地址表的大小
UCHAR* ParamTableBase; // 参数个数表首地址
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
而内核又使用一个结构体把两种表保存了起来:
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl;// ntoskrnl.exe的服务函数,即SSDT
KSYSTEM_SERVICE_TABLE win32k; // win32k.sys的服务函数(GDI32.dll/User32.dll 的内核支持),即ShadowSSDT
KSYSTEM_SERVICE_TABLE notUsed1; // 不使用
KSYSTEM_SERVICE_TABLE notUsed2; // 不使用
}KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
虽然在内核中服务函数的地址被放在了两个表中查询, 但是用户层的调用号只有一套, 为了区分调用号到底是哪张表的调用号, Windows把一个4字节的调用号设计成了下面的结构:
服务表号
表号等于0 时, 表示使用SSDT表. 表号等于1 时, 表示使用ShadowSSDT表.
索引号
表中的下标.
KPCR
内核处理器控制区(Kernel Processor Control Region) 0环的ETHREAD结构体是记录线程的相关信息,EPROCESS结构体是记录进程相关的信息,同样我们每个CPU也有一个结构体来记录每个CPU的状态这个结构体就是KPCR
结构体 这个结构体保存了内核的大量信息, 例如,GDT,IDT表的地址,当前线程对象(ETHREAD). 而Windows把这个结构体的基地址保存在一个段中,通过0x30
段选择子就可以找到此段描述符, 段描述中的基地址就是其首地址:
在这个结构体中, 还有一个更大的结构体:_KPRCB
, 这个结构体也保存了很多的信息,其中一个是当前线程内核对象的首地址. 而在线程内核对象(KTHREAD
)结构体中, 就有一个字段保存这SSDT
表的地址
KPCR
结构体如下下面该结构体中几个主要的成员,
<pre>kd> dt _KPCR
ntdll!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD // 内核层SHE链头节点的地址.
+0x004 Used_StackBase : Ptr32 Void
+0x008 Spare2 : Ptr32 Void
+0x00c TssCopy : Ptr32 Void
+0x010 ContextSwitches : Uint4B
+0x014 SetMemberCopy : Uint4B
+0x018 Used_Self : Ptr32 Void
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB // PRCB首地址
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY // IDT表首地址
+0x03c GDT : Ptr32 _KGDTENTRY // GDT表首地址
+0x040 TSS : Ptr32 _KTSS // TSS任务段首地址
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 SpareUnused : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB // 此结构体字段内部还保存了大量的和CPU有关的信息.
KiFastCallEntery
分析
8404a750 mov ecx,23h ; 数据段段选择子
8404a755 push 30h ; 内核FS段选择子,保存的是_KPCR结构
8404a757 pop fs ;切换FS段寄存器.
8404a759 mov ds,cx
8404a75b mov es,cx
8404a75d mov ecx,dword ptr fs:[40h] ; PcTss
8404a764 mov esp,dword ptr [ecx+4] ;PcTss.TssEsp0 , 内核的ESP寄存器
8404a767 push 23h
8404a769 push edx
8404a76a pushfd ; 标记寄存器入栈
8404a76b push 2 ; 将标志寄存器设置为2(清空了DF标志位)
8404a76d add edx,8 ; edx保存的是内核栈顶地址, 加8之后得到的是第一个参数的地址.
; edx = 用户层第一个参数的首地址(用户栈地址)
;
; 用户层栈顶布局:
; edx + 0x0 [返回地址1]
; edx + 0x4 [返回地址2]
; edx + 0x8 [ 形参1 ]
; edx + 0xc [ 形参2 ]
;
; edx += 8之后, edx保存的就是第一个形参的首地址.
;
8404a770 popfd ;
8404a771 or byte ptr [esp+1],2 ; 将中断允许标志位置1
8404a776 push 1Bh ; 将用户的CS段选择子入栈
8404a778 push dword ptr ds:[0FFDF0304h]; 将返回地址入栈
8404a77e push 0
8404a780 push ebp
8404a781 push ebx
8404a782 push esi
8404a783 push edi
8404a784 mov ebx,dword ptr fs:[1Ch]; PRCB Address
8404a78b push 3Bh; 用户层的FS
8404a78d mov esi,dword ptr [ebx+124h]; get current thread address
8404a793 push dword ptr [ebx]
8404a795 mov dword ptr [ebx],0FFFFFFFFh
8404a79b mov ebp,dword ptr [esi+28h]
8404a79e push 1
8404a7a0 sub esp,48h
8404a7a3 sub ebp,29Ch
8404a7a9 mov byte ptr [esi+13Ah],1
8404a7b0 cmp ebp,esp
8404a7b2 jne nt!KiFastCallEntry2+0x49 (8404a74b)
8404a7b4 and dword ptr [ebp+2Ch],0
8404a7b8 test byte ptr [esi+3],0DFh
8404a7bc mov dword ptr [esi+128h],ebp
8404a7c2 jne nt!Dr_FastCallDrSave (8404a600)
8404a7c8 mov ebx,dword ptr [ebp+60h]
8404a7cb mov edi,dword ptr [ebp+68h]
8404a7ce mov dword ptr [ebp+0Ch],edx
8404a7d1 mov dword ptr [ebp+8],0BADB0D00h
8404a7d8 mov dword ptr [ebp],ebx
8404a7db mov dword ptr [ebp+4],edi
8404a7de sti ; 屏蔽中断
8404a7df mov edi,eax ; eax调用号,格式:19-1(服务表)-12(索引号)
8404a7e1 shr edi,8 ; eax>>8 实际是为了获取服务表的值.
8404a7e4 and edi,10h ; 和0x10按位与之后,如果服务表的数值为0.则edi为0,否则edi还是0x10.
8404a7e7 mov ecx,edi ; ecx 保存的是服务表号
8404a7e9 add edi,dword ptr [esi+0BCh];esi保存的是线程内核对象的
;首地址(KTHREAD),0BC偏移是字段:ServiceTable
; edi是服务表偏移, 在ServiceTable中,第0个是SSDT
; 一个SSDT字段正好是0x10个字节.
; 因此,edi = 服务表结构体的首地址.
; 服务表结构体为:_KSYSTEM_SERVICE_TABLE
8404a7ef mov ebx,eax ; eax是调用号
8404a7f1 and eax,0FFFh ; 取调用号的低12位, 得到的就是服务表的序号了.
; eax = 服务表序号
8404a7f6 cmp eax,dword ptr [edi+8];edi是服务表首地址,+8就是NumberOfService,服务个数
8404a7f9 jae nt!KiBBTUnexpectedRange (8404a532); 如果序号大于等于个数了,就跳转
8404a7ff cmp ecx,10h ; ecx保存的是服务表号,0是SSDT,0x10是ShadowSSDT
8404a802 jne nt!KiFastCallEntry+0xce (8404a81e); 如果不是ShadowSSDT就跳转,下面是针对
; ShadowSSDT的处理
8404a804 mov ecx,dword ptr [esi+88h] ; esi保存的是线程内核对象的首地址(KTHREAD)
; +0x088 是 Teb首地址
; ecx = Teb首地址
8404a80a xor esi,esi
8404a80c or esi,dword ptr [ecx+0F70h]; ecx+0F70 ==> Teb.GdiBatchCount
8404a812 je nt!KiFastCallEntry+0xce (8404a81e)
8404a814 push edx
8404a815 push eax
8404a816 call dword ptr [nt!KeGdiFlushUserBatch (8417582c)]; 调用函数
8404a81c pop eax
8404a81d pop edx
8404a81e inc dword ptr fs:[6B0h] ; fs保存的是KPCR,+6B0是_GDI_TEB_BATCH结构体中的一个字段.
8404a825 mov esi,edx ; edx是在用户层传入进来的用户层栈顶.现在 esi=用户层栈顶.
8404a827 xor ecx,ecx
8404a829 mov edx,dword ptr [edi+0Ch] ; edi=服务表结构体的首地址, +0C是ParamTableBase,
; 也就是函数的参数个数表
; edx = 函数的参数个数表首地址
8404a82c mov edi,dword ptr [edi] ; edi=服务表结构体的首地址, +0是ServiceTableBase
; 也就是服务函数地址表
; edi = 服务函数地址表首地址
8404a82e mov cl,byte ptr [eax+edx] ; eax 是调用号, edx是函数参数个数表
; 取出函数参数个数表中调用号对应的函数的参数个数
; cl = 函数参数个数
8404a831 mov edx,dword ptr [edi+eax*4];edi=服务表结构体的首地址,eax=是调用号
; 取出调用号对应的函数的地址
; edx = 服务函数地址
8404a834 sub esp,ecx ; 开辟栈空间, 开辟的大小就是ecx
; ecx= 函数参数个数
8404a836 shr ecx,2 ; ecx /= 2;
8404a839 mov edi,esp ; 把当前栈顶赋值给edi
8404a83b cmp esi,dword ptr [nt!MmUserProbeAddress (84175704)] ; 将esi执行的地址设置为可写
; esi=用户层栈顶
8404a841 jae nt!KiSystemCallExit2+0xa5 (8404aa75)
8404a847 rep movs dword ptr es:[edi],dword ptr [esi]; 将esi指向的数据拷贝到edi,字节数为ecx
; 实际就是将用户栈的数据拷贝到内核栈, 拷贝的字节
; 数就是: 参数个数/2 * 4;
8404a849 test byte ptr [ebp+6Ch],1 ;
8404a84d je nt!KiFastCallEntry+0x115 (8404a865)
8404a84f mov ecx,dword ptr fs:[124h] ; fs[124h]==>当前线程对象,
; ecx = 当前线程对象地址
8404a856 mov edi,dword ptr [esp] ; edi=第一个参数的值
8404a859 mov dword ptr [ecx+13Ch],ebx ; ecx+13c=>KTHREAD.SystemCallNumber, 保存调用号
8404a85f mov dword ptr [ecx+12Ch],edi ; ecx+12c=>KTHREAD.FirstArgument , 保存第一个参数
8404a865 mov ebx,edx ; ebx= edx = 服务函数地址
8404a867 test byte ptr [nt!PerfGlobalGroupMask+0x8 (841430c8)],40h
8404a86e setne byte ptr [ebp+12h]
8404a872 jne nt!KiServiceExit2+0x17b (8404ac04)
8404a878 call ebx ; 调用服务函数 终于调用了!!!!!!!!!!!!!!!
8404a87a test byte ptr [ebp+6Ch],1
8404a87e je nt!KiFastCallEntry+0x164 (8404a8b4)
8404a880 mov esi,eax
8404a882 call dword ptr [nt!_imp__KeGetCurrentIrql (84016168)]
8404a888 or al,al
8404a88a jne nt!KiServiceExit2+0x142 (8404abcb)
8404a890 mov eax,esi
8404a892 mov ecx,dword ptr fs:[124h]
8404a899 test byte ptr [ecx+134h],0FFh
8404a8a0 jne nt!KiServiceExit2+0x160 (8404abe9)
8404a8a6 mov edx,dword ptr [ecx+84h]
8404a8ac or edx,edx
8404a8ae jne nt!KiServiceExit2+0x160 (8404abe9)
8404a8b4 mov esp,ebp
8404a8b6 cmp byte ptr [ebp+12h],0
8404a8ba jne nt!KiServiceExit2+0x187 (8404ac10)
8404a8c0 mov ecx,dword ptr fs:[124h]
8404a8c7 mov edx,dword ptr [ebp+3Ch]
8404a8ca mov dword ptr [ecx+128h],edx
nt!KiServiceExit:
8404a8d0 cli
8404a8d1 test byte ptr [ebp+72h],2
8404a8d5 jne nt!KiServiceExit+0xd (8404a8dd)
8404a8d7 test byte ptr [ebp+6Ch],1
8404a8db je nt!KiServiceExit+0x74 (8404a944)
8404a8dd mov ebx,dword ptr fs:[124h]
8404a8e4 test byte ptr [ebx+2],2
8404a8e8 je nt!KiServiceExit+0x22 (8404a8f2)
8404a8ea push eax
8404a8eb push ebx
8404a8ec call nt!KiCopyCounters (840eb811)
8404a8f1 pop eax
8404a8f2 mov byte ptr [ebx+3Ah],0
8404a8f6 cmp byte ptr [ebx+56h],0
8404a8fa je nt!KiServiceExit+0x74 (8404a944)
8404a8fc mov ebx,ebp
8404a8fe mov dword ptr [ebx+44h],eax
8404a901 mov dword ptr [ebx+50h],3Bh
8404a908 mov dword ptr [ebx+38h],23h
8404a90f mov dword ptr [ebx+34h],23h
8404a916 mov dword ptr [ebx+30h],0
8404a91d mov ecx,1
8404a922 call dword ptr [nt!_imp_KfRaiseIrql (8401615c)]
8404a928 push eax
8404a929 sti
8404a92a push ebx
8404a92b push 0
8404a92d push 1
8404a92f call nt!KiDeliverApc (840833fe)
8404a934 pop ecx
8404a935 call dword ptr [nt!_imp_KfLowerIrql (84016158)]
8404a93b mov eax,dword ptr [ebx+44h]
8404a93e cli
8404a93f jmp nt!KiServiceExit+0xd (8404a8dd)
8404a941 lea ecx,[ecx]
8404a944 mov edx,dword ptr [esp+4Ch]
8404a948 mov dword ptr fs:[0],edx
8404a94f mov ecx,dword ptr [esp+48h]
8404a953 mov esi,dword ptr fs:[124h]
8404a95a mov byte ptr [esi+13Ah],cl
8404a960 test dword ptr [esp+2Ch],0FFFF23FFh
8404a968 jne nt!KiSystemCallExit2+0x1c (8404a9ec)
8404a96e test dword ptr [esp+70h],20000h
8404a976 jne nt!Kei386EoiHelper+0x134 (8404b3bc)
8404a97c test word ptr [esp+6Ch],0FFF8h
8404a983 je nt!KiSystemCallExit2+0x72 (8404aa42)
8404a989 cmp word ptr [esp+6Ch],1Bh
8404a98f bt word ptr [esp+6Ch],0
8404a996 cmc
8404a997 ja nt!KiSystemCallExit2+0x60 (8404aa30)
8404a99d cmp word ptr [ebp+6Ch],8
8404a9a2 je nt!KiServiceExit+0xd9 (8404a9a9)
8404a9a4 lea esp,[ebp+50h]
8404a9a7 pop fs
8404a9a9 lea esp,[ebp+54h]
8404a9ac pop edi
8404a9ad pop esi
8404a9ae pop ebx
8404a9af pop ebp
8404a9b0 cmp word ptr [esp+8],80h
8404a9b7 ja nt!Kei386EoiHelper+0x150 (8404b3d8)
8404a9bd add esp,4
8404a9c0 test dword ptr [esp+4],1
nt!KiSystemCallExitBranch:
8404a9c8 jne nt!KiSystemCallExit2 (8404a9d0)
8404a9ca pop edx
8404a9cb pop ecx
8404a9cc popfd
8404a9cd jmp edx
nt!KiSystemCallExit:
8404a9cf iretd
nt!KiSystemCallExit2:
8404a9d0 test dword ptr [esp+8],100h
8404a9d8 jne nt!KiSystemCallExit (8404a9cf)
8404a9da pop edx
8404a9db add esp,4
8404a9de and dword ptr [esp],0FFFFFDFFh
8404a9e5 popfd
8404a9e6 pop ecx
8404a9e7 sti
8404a9e8 sysexit ; 退出系统调用,回到用户层
HOOK
HOOK的方式有几种:
将MSR寄存器组中的
SYSENTER_EIP_MSR
寄存器(编号0x176
)改成自己的函数地址,这样一来, CPU每次执行SYSENTER
时调用的就是保存在这个寄存器中的函数.使用内联HOOK, 将
KiFastCallEntry
头改成jmp XXXX
, 跳转到自己的函数. 自己的函数执行完之后再跳转回去.
SYSENTER HOOK : 修改MSR寄存器组进行HOOK
#include <Ntifs.h>
#include <ntddk.h>
CHAR* PsGetProcessImageFileName(PEPROCESS*);
VOID UnLoad(DRIVER_OBJECT* object);
void installSysenterHook();
void uninstallSysenterHook();
void MyKiFastCallEntry();
ULONG_PTR g_oldKiFastCallEntery;
ULONG g_uPid = 2940; // 需要保护的进程ID, 这个PID可以通过内核通讯来修改.
NTSTATUS DriverEntry(DRIVER_OBJECT* driver, UNICODE_STRING* path)
{
DbgBreakPoint();
driver->DriverUnload = UnLoad;
// 安装HOOK
installSysenterHook();
return STATUS_SUCCESS;
}
VOID UnLoad(DRIVER_OBJECT* object)
{
// 卸载HOOK
uninstallSysenterHook();
}
// 安装HOOK
void _declspec(naked) installSysenterHook()
{
_asm
{
push edx;
push eax;
push ecx;
;// 备份原始函数
mov ecx, 0x176 ;//SYSENTER_EIP_MSR寄存器的编号.保存着KiFastCallEntry的地址
rdmsr; // // 指令使用ecx寄存器的值作为MSR寄存器组的编号,将这个编号的寄存器中的值读取到edx:eax
mov[g_oldKiFastCallEntery], eax;// 将地址保存到全局变量中.
;// 将新的函数设置进去.
mov eax, MyKiFastCallEntry;
xor edx, edx;
wrmsr; // 指令使用ecx寄存器的值作为MSR寄存器组的编号,将edx:eax写入到这个编号的寄存器中.
pop ecx;
pop eax;
pop edx;
ret;
}
}
// 卸载HOOK
void uninstallSysenterHook()
{
_asm
{
push edx;
push eax;
push ecx;
;// 将新的函数设置进去.
mov eax, [g_oldKiFastCallEntery];
xor edx, edx;
mov ecx, 0x176;
wrmsr; // 指令使用ecx寄存器的值作为MSR寄存器组的编号,将edx:eax写入到这个编号的寄存器中.
pop ecx;
pop eax;
pop edx;
}
}
void _declspec(naked) MyKiFastCallEntry()
{
/**
* 本函数是从用户层直接切换进来的.
* 在本函数中,有以下信息可以使用:
* 1. eax保存的是调用号
* 2. edx保存着用户层的栈顶,且用户层的栈顶布局为:
* edx+0 [ 返回地址1 ]
* edx+4 [ 返回地址2 ]
* edx+8 [ 形 参1 ]
* edx+C [ 形 参2 ]
* 3. 要HOOK的API是 OpenProcess,其调用号和参数信息为:
* 调用号 - 0xBE
* 函数参数 -
* NtOpenProcess(
* [edx+08h] PHANDLE ProcessHandle,// 输出参数,进程句柄
* [edx+0Ch] ACCESS_MASK DesiredAccess,// 打开的权限
* [edx+10h] POBJECT_ATTRIBUTES ObjectAttributes,// 对象属性,无用
* [edx+14h] PCLIENT_ID ClientId // 进程ID和线程ID的结构体
* 最后一个参数的结构体原型为:
* typedef struct _CLIENT_ID
* {
* PVOID UniqueProcess;// 进程ID
* PVOID UniqueThread; // 线程ID(在这个函数中用不到)
* } CLIENT_ID, *PCLIENT_ID;
*
* HOOK 步骤:
* 1. 检查调用号是不是0xBE(ZwOpenProcess)
* 2. 检查进程ID是不是要保护的进程的ID
* 3. 如果是,则将进程ID改为0,再调用原来的函数,这样一来,即使功能被执行,
* 也无法打开进程, 或者将访问权限设置为0,同样也能让进程无法被打开.
* 4. 如果不是,则调用原来的KiFastCallEntry函数
*/
_asm
{
;// 1. 检查调用号
cmp eax, 0xBE ;
jne _DONE ; // 调用号不是0xBE,执行第4步
;// 2. 检查进程ID是不是要保护的进程的ID
push eax; // 备份寄存器
;// 2. 获取参数(进程ID)
mov eax, [edx + 0x14];// eax保存的是PCLIENT_ID
mov eax, [eax] ;// eax保存的是PCLIENT_ID->UniqueProcess
;// 3. 判断是不是要保护的进程ID
cmp eax, [g_uPid];
pop eax ;// 恢复寄存器
jne _DONE ;// 不是要保护的进程就跳转
;// 3.1 是的话就该调用参数,让后续函数调用失败.
mov[edx + 0xC], 0; // 将访问权限设置为0
_DONE:
; // 4. 调用原来的KiFastCallEntry
jmp g_oldKiFastCallEntery;
}
}