目录
Windows程序3环进0环追踪记录... 2
一、 实验环境... 2
二、 关于3环和0环... 2
三、 函数ReadProcessMemary追踪... 4
1、 函数说明... 4
2、 函数追踪... 4
3、 结构体_KUSER_SHARED_DATA.. 5
4、 地址0x7FFE0300到底存储的是什么?... 6
5、 如何判断自己的cpu是否支持快速调用?... 6
6、 追踪函数!KiIntSystemCall(). 7
7、 快速调用... 8
8、 为什么叫快速调用... 9
9、 函数KiSystemService分析... 9
10、 系统服务表... 15
11、 函数nt!KiFastCallEntry+0x8f (8054276f)分析... 16
12、 函数nt!KiSystemServiceAccessTeb+0x12分析... 17
四、 总结... 18
五、 附件... 18
1、 结构体_KPCR. 18
2、 结构体_NT_TIB. 19
3、 结构体_KPRCB. 19
4、 结构体_ETHREAD.. 21
5、 结构体_KTHREAD.. 23
Windows程序3环进0环追踪记录
1. 系统:32位 Windows xp sp3
2. 虚拟机:VMware workstation
3. 调试器
Ollybdg 1.1
IDA pro 6.8
Win dbg
1. 0环,ring0,运行内核程序,kernel层。
2. 3环,ring3,运行应用程序,user层。
3. 程序由3环进入0环主要问题
1) 程序进入0环后,原来3环的寄存器会保存到什么地方?
2) 程序进入0环后,程序要去哪里执行,也就是新的eip的值从哪里取呢?
3) 程序进入0环后,权限发生切换,必然需要新的堆栈,那么新的esp的值来自哪里呢?
4) 权限切换后,需要新的cs和ss,那么cs和ss段寄存器的值哪里获得?
带着上面的问题,我们利用调试工具来追踪一下函数ReadProcessMemary
如下图所示
ReadProcessMemory归属为编程中的内存操作函数, 其作用为根据进程句柄读入该进程的某个内存空间; 函数原型为
BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
由布尔声明可以看出, 当函数读取成功时返回1, 失败则返回0, 具体参数含义将在下文中指出。
1) hProcess 句柄号
2) lpBaseAddress 要读取的地址
3) lpBuffer out 类型 数据缓冲区,用于返回数据
4) nSize in 类型 读取多少数据
5) lpNumberOfBytesRead out类型
利用IDA PRO 追踪ReadProcessMemory函数
打开IDA Pro ,载入kernel32.dll,搜索函数ReadProcessMemory获得函数汇编代码如下
通过分析上述代码:ReadProcessMemory函数一共完成如下2件事
1) 将5个参数push进入堆栈
2) 调用函数NtReadVirtualMemory
函数NtReadVirtualMemory位于ntdll.dll,利用IDA PRO打开ntdll.dll,继续追踪函数NtReadVirtualMemory
分析上述的汇编代码共完成如下两件事:
1) MOV EAX,0Bah 将函数调用好存入eax中
2) MOV EDX,7FFE0300h 将参数指针放到地址为:7ffe0300h 的地方
注意这个地址0x7FFE0300,这里就是user层进入kernel层的关键,在详细说明这个地址之前我们要先介绍一个结构体。
在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于 User 层和 Kernel 层共享某些数据
它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:
User 层地址为:0x7ffe0000
Kernnel 层地址为:0xffdf0000
通过winbdg查看,可以发现user层地址0x7ffe0000 和kernel层地址0xffdf0000的内容是一样的,虽然指向的是同一个物理页,但在User 层是只读的,在Kernnel层是可写的.
地址0x7ffe000 指向了结构体_KUSER_SHARED_DATA,下图为结构体成员
注意:地址为0x7ffe000 + 0x300 ,指向的结构体_KUSER_SHARED_DATA的成员SystemCall, 这里就是user层进入kernel层的关键。
如果cpu支持快速调用,那么系统会将ntdll.dll!KiFastSystemCall() 的函数地址写到位置0x7FFE0300
如果cpu不支持快速调用,那么会将ntdll.dll!KiIntSystemCall()的函数地址写到位置0x7FFE0300
也就是说0x7FFE0300,会根据CPU是否支持快速调用,从而选择不同的函数进入kernel层。
注意:上述两个函数都不是内核函数,是ntdll.dll中的应用层的函数。
打开ollybdg
打开一个exe文件,
清空edx
修改当前程序的指令为cpuid
执行cpuid 查看edx的值,拆解edx,如果第11位等于1,说明当前的CPU支持快速调用。
非快速调用
如果cpu不支持快速调用,那么user层进入kernel层会调用函数ntdll.dll!KiIntSystemCall()
打开IDA pro 载入ntdll.dll
通过上述汇编代码,int 2eh 发现,这里是通过中断门2e进入kernel层
我们知道中断门位于IDT表中,利用winbdg查看idt表,十六进制2e等于十进制46,就是
上图中蓝色部分所对应的中断门。通过拆分中断门8054ee00·00082611
获得eip = 80542611
Cs (代码段选择子)= 0008
Ss (堆栈段选择子)和esp 由tss 段提供。
也就是说程序下一步会跳转到地址为80542611的位置上,这里指向了正真的内核函数KiSystemService
由于ss和esp的是从tss段中获取到的,所以这里涉及到了内存查找,所以说不是快速调用。
如果cpu支持快速调用,那么user层进入kernel层会调用函数ntdll.dll!KiFastSystemCall()
打开IDA pro 载入ntdll.dll 追踪函数KiFastSystemCall()
可以发现,这里使用的是指令sysenter,进入Kernel
说明:
在执行sysenter指令之前,操作系统必须指定0环的CS段、SS段、EIP以及ESP.
这些值在MSR寄存器中。
查看MSR寄存器
kd> rdmsr 174 //查看CS
kd> rdmsr 175 //查看ESP
kd> rdmsr 176 //查看EIP
SS = cs+8
可以发现这里的eip = 0x805426e0 指向KiFastSystemCall()
中断门进0环,需要的CS、EIP在IDT表中,需要查内存获取SS和ESP,(SS与ESP由TSS提供)
而CPU如果支持sysenter指令时,操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,本质都是获得CS,SS,ESP,EIP的值,只是一个查内存,慢一点,一个查cpu寄存器,快一点,本质是一样的!
在分析函数KiSystemService之前需要先说明几个结构体。
1) _KPCR CPU相关结构体
2) _ETHREAD 线程相关的结构体
3) _TrapFrame 用于三环进0环时,保存3环寄存器的值。下图就是_TrapFrame结构体
kd> u 80542611 l40 保存三环的寄存器到_Trap_Frame结构体中
nt!KiSystemService:
80542611 6a00 push 0 ;保存到_Trap_Frame+0x064的位置
80542613 55 push ebp ;保存到_Trap_Frame+0x060的位置
80542614 53 push ebx ;保存到_Trap_Frame+0x05c的位置
80542615 56 push esi ;保存到_Trap_Frame+0x058的位置
80542616 57 push edi ;保存到_Trap_Frame+0x054的位置
80542617 0fa0 push fs ;保存到_Trap_Frame+0x050的位置
80542619 bb30000000 mov ebx,30h
8054261e 668ee3 mov fs,bx ;修改段寄存器fs
通过段选择子:0x30获得index = 6
查找GDT表获得段描述符
获得段描述符为:ffc093df`f0000001
拆分段描述符:获得base:ffdff000
地址ffdff000指向结构体_KPRC
fs:[0]指向_KPRC
80542621 64ff3500000000 push dword ptr fs:[0] ;保存老的异常列表
80542628 64c70500000000ffffffff mov dword ptr fs:[0],0FFFFFFFFh ;设置ExceptionList为-1
80542633 648b3524010000 mov esi,dword ptr fs:[124h] ;esi 指向_KTHREAD 线程结构体的地址
8054263a ffb640010000 push dword ptr [esi+140h] ;保存先前模式,位置_KTHREAD + 140H
80542640 83ec48 sub esp,48h ;提升堆栈,esp指向_Trap_Frame+0的位置
80542643 8b5c246c mov ebx,dword ptr [esp+6Ch] ;获得原来三环的cs
80542647 83e301 and ebx,1 ;如果cpl = 0x0,ebx =0
;如果cpl = 0x11,ebx =1
8054264a 889e40010000 mov byte ptr [esi+140h],bl ;设置先前模式
80542650 8bec mov ebp,esp ;提升堆栈 ebp指向_Trap_Frame+0x00
80542652 8b9e34010000 mov ebx,dword ptr [esi+134h] 取出原来的TrapFrame结构体地址
80542658 895d3c mov dword ptr [ebp+3Ch],ebx 保存原来的TrapFrame结构体地址
8054265b 89ae34010000 mov dword ptr [esi+134h],ebp 将现在的TrapFrame的结构体地址写入到_KTHREAD+0X134的位置
80542661 fc cld
80542662 8b5d60 mov ebx,dword ptr [ebp+60h] ;取出原来3环的ebp
80542665 8b7d68 mov edi,dword ptr [ebp+68h] ;取出原来3环的eip
80542668 89550c mov dword ptr [ebp+0Ch],edx ;写入三环的参数指针
8054266b c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h
80542672 895d00 mov dword ptr [ebp],ebx ;写入原来3环的ebp
80542675 897d04 mov dword ptr [ebp+4],edi ;写入原来3环的eip
80542678 f6462cff test byte ptr [esi+2Ch],0FFh ;判断是否是调试模式
8054267c 0f858afeffff jne nt!Dr_kss_a (8054250c)
80542682 fb sti
80542683 e9e7000000 jmp nt!KiFastCallEntry+0x8f (8054276f)
分析上述代码:
1) 地址80542611 –80542617
80542611 6a00 push 0
80542613 55 push ebp
80542614 53 push ebx
80542615 56 push esi
80542616 57 push edi
80542617 0fa0 push fs
是将三环寄存器的值保存到结构体_TrapFrame 中,就是保存现场。
2) fs:[0]指向_KPRC
80542619 bb30000000 mov ebx,30h
8054261e 668ee3 mov fs,bx ;修改段寄存器fs
a) 通过段选择子:0x30
根据段选择子结构,拆分段选择子:0x30 = 0b 0000 0000 0011 0000
获得index = 0b0110 = 6
RPL=0
GDT=0
b) 根据index = 6 查找GDT表获得段描述符
c) 获得段描述符为:ffc093df`f0000001
d) 拆分段描述符:获得base:ffdff000
e) 地址ffdff000指向结构体_KPRC
所以:fs:[0]指向_KPRC
3) 地址:80542621 – 80542628
80542621 push dword ptr fs:[0]
80542628 mov dword ptr fs:[0],0FFFFFFFFh ;设置ExceptionList为-1
因为fs:[0] 指向结构体_KPCR,他的第一个成员就是结构体_NT_TIB,结构体_NT_TIB的第一个成员是ExceptionList,所以:push dword ptr fs:[0] 的功能就是保存老的异常链表。
mov dword ptr fs:[0],0FFFFFFFFh ,就是设置新的异常列表为-1。
4) 80542633 mov esi,dword ptr fs:[124h]
fs:[0]指向了结构体_KPCR, fs:[124h] 指向了_KTHREAD
也就是所esi 指向结构体_KTHREAD
5) 地址80542650-8054265
80542650 mov ebp,esp //提升堆栈,此时esp = ebp指向结构体_TrapFrame的首地址
80542652 mov ebx,dword ptr [esi+134h] //取出原来的_TrapFrame结构体地址
80542658 mov dword ptr [ebp+3Ch],ebx //保存原来的_TrapFrame结构体地址
8054265b mov dword ptr [esi+134h],ebp //将新_TrapFrame结构体地址写入到结构体_KTHREAD +0x134的位置
6) 代码跳转到 nt!KiFastCallEntry+0x8f (8054276f)
在分析函数nt!KiFastCallEntry+0x8f (8054276f)之前要先简绍一下系统服务表
Windows共计两张系统服务表
结构体SystemServiceDescriptorTable
typedef struct SystemServiceDescriptorTable
{
UINT *ServiceTableBase; //指向函数地址表
UINT *ServiceCounterTableBase; //记录被访问次数
UINT NumberOfService; //函数地址表共计多少个函数
UCHAR *ParameterTableBase; //指向函数参数表
}SystemServiceDescriptorTable,*PSystemServiceDescriptorTable;
成员* ServiceTableBase指向函数地址表,函数地址表是UINT类型的数组,储存内核函数的地址。
成员*ParameterTableBase指向函数参数表,UCHAR类型数组,存储函数参数地址。
我们可以通过eax传递进来的内核函数号,查找函数地址表和函数参数表,获得内核函数的地址和参数。
内核函数号的使用方法:第12位决定查那张系统服务表,0-11位决定函数地址,和参数地址。
以ReadProcessMemary 的内核函数号0x000000BA为列
他的第12位等于0 ,说明查找一张内核服务表
后12位等于0BA,说明函数地址位于函数地址表的0x0BA项,函数参数地址为函数参数表的第0X0BA项。
8054276f 8bf8 mov edi,eax ;获得内核函数编号 值为0x00ba
80542771 c1ef08 shr edi,8 ;edi = 0
80542774 83e730 and edi,30h ;edi = 0
80542777 8bcf mov ecx,edi ;ecx = 0
80542779 03bee0000000 add edi,dword ptr [esi+0E0h] ;esi指向KTHREAD [esi + 0e0h]获得系统服务表的地址 ;edi指向系统服务表
8054277f 8bd8 mov ebx,eax ;ebx = 00ba
80542781 25ff0f0000 and eax,0FFFh ;取后12位
80542786 3b4708 cmp eax,dword ptr [edi+8] ;[edi+8]就是ServiceLimit,判断函数编号是否超出范围
80542789 0f8333fdffff jae nt!KiBBTUnexpectedRange (805424c2) ;异常处理程序
8054278f 83f910 cmp ecx,10h ;判断是否是第一张系统服务表
80542792 751b jne nt!KiSystemServiceAccessTeb+0x12 (805427af)
80542794 648b0d18000000 mov ecx,dword ptr fs:[18h]
8054279b 33db xor ebx,ebx 地址8054276f
mov edi,eax
获得内核调用号
1) 地址 80542771 - 80542779
80542771 c1ef08 shr edi,8
80542774 83e730 and edi,30h
80542777 8bcf mov ecx,edi
80542779 03bee0000000 add edi,dword ptr [esi+0E0h]
esi指向KTHREAD [esi + 0e0h]获得系统服务表的地址
edi指向系统服务表
2) 地址:8054277f - 80542781
8054277f mov ebx,eax
80542781 and eax,0FFFh
获取内核调用号的后12位,用于在函数地址表查找函数地址和函数参数表中查找函数参数的地址。
3) 地址80542792
jne nt!KiSystemServiceAccessTeb+0x12 (805427af)
跳转到系统服务表处理函数!KiSystemServiceAccessTeb
805427af 64ff0538060000 inc dword ptr fs:[638h] fs:[0]指向_KPRC,fs:[638h]指向结构体_KPRCB +0x518 KeSystemCalls
805427b6 8bf2 mov esi,edx edx:三环参数的指针
说明:edx为三环参数的指针,esi = edx,esi指向三环程序的参数。
805427b8 8b5f0c mov ebx,dword ptr [edi+0Ch]
说明:因为edi指向系统服务表,[edi+0ch]就是系统服务表的成员ArgmentTable,ebx =函数参数表的地址
805427bb 33c9 xor ecx,ecx
805427bd 8a0c18 mov cl,byte ptr [eax+ebx]
说明:Eax + Ebx函数参数表的地址 + 偏移 ,取值后获得函数参数的个数
805427c0 8b3f mov edi,dword ptr [edi] 取出系统服务表的地址 ServiceTableBase的值
805427c2 8b1c87 mov ebx,dword ptr [edi+eax*4]
说明:通过系统调用号,查找系统服务表,找到函数地址,将地址存储到ebx中
805427c5 2be1 sub esp,ecx 提升堆栈,提升高度为cl,就是参数的个数
805427c7 c1e902 shr ecx,2 参数的总长度/4 == 参数的个数
805427ca 8bfc mov edi,esp 设置要copy的目的地址。就是刚才提上堆栈esp的位置
805427cc f6457202 test byte ptr [ebp+72h],2
805427d0 7506 jne nt!KiSystemServiceAccessTeb+0x3b (805427d8)
805427d2 f6456c01 test byte ptr [ebp+6Ch],1
805427d6 740c je nt!KiSystemServiceCopyArguments (805427e4) ;copy参数
805427d8 3b3534315680 cmp esi,dword ptr [nt!MmUserProbeAddress (80563134)] ;esi:三环参数指针;判断三环地址是否越界
805427de 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (8054298c) ;越界就退出
nt!KiSystemServiceCopyArguments:
805427e4 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
说明:将三环的参数copy到0环的堆栈
805427e6 ffd3 call ebx
说明:调用0环对应的函数
到此我们完成了3环进0环整个过程的追踪
Ntdll.dll 中的 API 都只不过是一个简单的包装函数而已,当 Kernel32.dll 中的 API 通过 Ntdll.dll 时,会完成参数的检查,再调用一个中断(int 2Eh 或者 SysEnter 指令),从而实现从 Ring3 进入 Ring0 层,并且将所要调用的服务号(也就是在 SSDT 数组中的索引值)存放到寄存器 EAX 中,并且将参数地址放到指定的寄存器(EDX)中,再将参数复制到内核地址空间中,再根据存放在 EAX 中的索引值来在 SSDT 数组中索引对应的内核函数,然后调用他。
kd> dt _KPCR
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课