0608:补充CPU预读取知识
0609:补充段寄存器知识点
0610:补充跨段跳转知识点
0611:补充调用门知识点
0628:补充任务门、任务段、分页模式知识点
0630:补充CPU缓存及TLB知识点
0701:保护模式完结-补充控制寄存器知识点
https://zhuanlan.zhihu.com/p/32718424 可以看一下这个链接了解一下CPU知识和几个著名的漏洞。
文章中提到了 指令预测 技术 。 在执行长跳转(跨段跳转)时会将 缓存的指令 清空。 所以会造成执行速度减慢。这句话现在可能体会不到。 具个具体例子:
CPU分为实模式、保护模式、虚拟8086模式,大多数操作系统运行在保护模式下
保护模式具有两个特点:段、页 ,保护模式真正保护的是数据结构、寄存器、指令
实模式:16位汇编,访问的都是物理地址,非常危险。
保护模式:保护的是对内存的访问,相对实模式安全。段层面保护,页层面保护。
保护模式具体资料可以在 Intel白皮书第三卷 中查看。
CPU共有八个段寄存器 : ES CS SS DS FS GS LDTR TR ,OD可见前6个,但GS段寄存器windows并未使用(32位下)。
如果运行在实模式下,则只有前四个有用。
如果是64位,则使用GS而不是FS。
LDT:局部描述符表 LDTR寄存器指向LDT段描述符。段描述符具体是什么在后文说明。
当执行一条指令如mov dword ptr ds:[0x12345678],eax
时 , 真正访问的地址: ds.base + 0x12345678
当执行一条指令时,真正执行的是cs.base+EIP
处的指令
当访问堆栈时,真正访问的是ss.base+Addr
地址。
段寄存器结构: 共96位, 16位可见,80位不可见。
可见部分为Selector成员(选择子)。
具体结构如下图:
读段寄存器指令:mov ax,es 只能读16位(可见部分)
写段寄存器指令:mov ds,ax 写了96位的。
段寄存器可以用mov指令读写,但是LDTR和TR除外。
证明隐藏的80位的存在:(下表属性是根据实际分析时总结出来的。)
完成属性探测代码,感受段寄存器属性的存在。
上文说过段寄存器有96位,其中16位的可见部分称为段选择子。剩下的80位哪里来?从GDT中查出来的
GDT是一块内存,是CPU设计中要求操作系统提供的一款内存。这块内存是操作系统在启动时填充的。使用windbg的命令可以查看gdt表的地址:
与GDT作用一样,但是在windows中很少(也可能是没有)使用LDT。后文会介绍到什么时候查LDT。目前只做个了解。知道有这么个表即可。
当执行mov ds,ax这种对段寄存器的赋值时,不是简单的给可见部分赋值。也会为80位隐藏部分赋值。
因此cpu会查表来取那80位的数据,根据ax的值决定查找GDT还是LDT,查找表的什么位置,查多少数据
ax决定了gdt中的第几个段描述符,ax这种2字节的数据,称为段选择子
在学习段选择子之前,使用windbg查看GDT表的数据。 结合上图结构,拆分40个段描述符记录它们每个属性的值。不需要知道这些属性代表什么,该练习仅为了熟悉描述符结构。
段选择子是一个遵守特定结构的16位的数,这个数决定了去GDT/LDT表中查哪一个段描述符,段选择子结构如下图:(段选择子的RPL一定要<=对应段描述符的DPL,否则试图使用该选择子加载对应段描述符的行为将由于权限不足而失败)
打开OD随便加载个程序,找到CS DS SS ES GS FS对应的描述符。
加载段描述符至段寄存器的指令共有三种:
mov ss,ax 使用mov指令
les lss lds lfs lgs 修改对应的段寄存器
cs不能通过上述指令改变,否则会导致EIP的改变,必须保证cs与eip一起改。后文会讲解如何修改CS并在需要时提升权限。
段寄存器不可见的80位是由对应的段描述符的以下部分填充的:
WORD Selector //16位 就是段选择子的值
WORD Attribute //16位 从段描述符的G位到Type位 刚好16位
DWORD Base //32位 段描述符的3个Base,顺序在图里标注了高低位。
DWORD Limit //32位 段描述符的2个SegLimit,G位为0,单位为字节,高位补0。G位为1,低位补FFF。
打开OD随便加载个程序,找到CS DS SS ES GS FS的80位不可见部分。
有效位 1:描述符有效 0:描述符无效
当描述符无效时,任何尝试加载该描述符、访问该描述符对应的段间地址都会报错。
段对齐粒度。 也就是决定了Limit大小的一个位。
在上文填充段寄存器隐藏部分时,Limit在描述符中只有5个16进制位表示,剩下的3个16进制位就需要看G位。
当G为0时,整个段将以字节对齐,Limit大小单位为字节,所以精确到1。Limit直接就是段长。段寄存器中的Limit高位补0。
当G为1时,整个段将以4KB对齐,Limit大小单位为4KB,所以段的末尾处一定是以FFF结尾。段寄存器中的Limit低位补FFF。
描述符类型位。 为0时,是系统段描述符。 为1时,是代码或数据段描述符。具体类型需要搭配type属性来判断。
决定了具体是代码段还是数据段描述符
当S为1时,type针对代码或数据段,具体含义如下:
当S为0时,Type针对系统段描述符
大段或者小段,分为三种情况:
对CS段来说:
为1时,默认为32位寻址。 为0时,默认为16位寻址。
对SS段来说:
为1时,隐式堆栈访问指令(PUS H POP CALL RETN等)修改的是32位寄存器ESP
为0时,隐式堆栈访问指令(PUSH POP CALL RETN等)修改的是16位寄存器SP
对于向下扩展的数据段:
为1时,段上限大小为4GB。 为0时 段上限大小为64KB
分别更改ES SS DS几个段寄存器对应段描述符的各个属性,感受下当某个属性被更改时,会对代码执行发生什么影响?
0环 1环 2环 3环 特权指令只能运行在0环
我们常说的驱动就是运行在0环权限下。也是最高权限,所以驱动才会那么牛逼。
而常见的一些exe dll等我们直观感受的软件,代码都是运行在3环,权限最低。
当前特权级:CS和SS段选择子的后两位。之所以称为3环程序就是CPL为3
CS和SS段选择子的后两位永远相同!(X86规定的)
所以所谓的你程序是几环的就是看CS SS,而所谓的提权就是改CS SS,只要有一种方法能改掉CS SS,那就是提权。
描述符特权级别:访问该段所需要的特权级别。
请求特权级别:段选择子中的后两位,可以随意指定。
当使用选择子来加载段描述符时,会检查CPL DPL RPL三者。
假设上述代码运行在0环,也就是CPL=0,成功条件为:CPL<=DPL且RPL<=DPL,所以代码会执行失败。
修改DS CS对应的DPL,看看OD中内存访问、代码执行会有什么影响?
必须保证CS与EIP同时修改,因此没有lcs这种只修改cs的指令。
同时修改CS与EIP的指令:
案例:
JMP FAR 0x20:0x004183D7
使用jmp far跳转到自己的裸函数,裸函数内使用jmp far跳回来。要求不能崩溃报错。
CALL FAR较为复杂,因为对堆栈产生了影响。
常规call调用,调用后将下一条指令地址压入堆栈,并将ESP-4,然后修改EIP为指定地址。
RETN将栈顶数据赋给EIP,并将ESP+4
指令格式: CALL FAR CS:EIP
拆分CS段选择子,找到一个段描述符,这个段描述符是一个代码段。新EIP为该代码段的base+call指令后面的EIP
call时会将调用者的CS首先压入堆栈,然后压入返回地址。同时修改CS。(RPL若与CPL不同,会被CPU强制修正。)
指令格式: CALL FAR CS:EIP (EIP是废弃的,通过指定的段选择子来决定跳到哪)
拆分CS段选择子,找到一个段描述符,这个段描述符必须是个调用门。
跨段不提权的长调用与短调用不同的是,call时会将调用者的CS首先压入堆栈,长返回retf时在弹出EIP后再将CS弹出
指令格式: CALL FAR CS:EIP (EIP是废弃的,通过指定的段选择子来决定跳到哪)
拆分CS段选择子,找到一个段描述符,这个段描述符必须是个调用门。
涉及到权限变化后会发生堆栈的切换。从原调用者的堆栈A到目标堆栈B。
CALL后会将调用者的SS ESP CS 返回地址依次压入到堆栈B中。在retf时依次弹出回原调用者。
堆栈B和SS来源于TSS,每当发生提权调用时CPU就会从TSS中取出堆栈B和SS的值,而这两个值是Windows操作系统赋进去的。每个线程有两份堆栈,一份3环的一份0环的。
提权方式共有:调用门 任务门 中断门 陷阱门 快速调用 这几种。
其中快速调用需要CPU的MSR寄存器支持。
除了快速调用以外,其他的提权方式SS 和 ESP都是从TSS中获取。
快速调用的SS是通过CS+8计算得到,ESP在MSR寄存器里存着。
门描述符后面学,先练习call代码段。
使用call far调用自己的裸函数,裸函数内使用jmp far跳回来。要求不能崩溃报错、跳回后原寄存器的数值不能被改变。
指令格式: CALL FAR CS:EIP (EIP是废弃的,通过指定的段选择子来决定跳到哪)
根据CS的值查GDT得到门描述符,门描述符中的SegmentSelector指向一个代码段描述符。代码段描述符的base+门描述符的Offset才是要执行的代码的位置。
最终CS的值是调用门描述符内的选择子的值。
构造门描述符时,DPL一定要为3,否则无法访问门描述符,这样就连敲门的权利的没有了何谈进入门内提权。
调用门描述符内的选择子指向的代码段描述符的DPL为0时会自动提权。(代码3环,RPL任意,门3环,门内的代码段0环,则会自动提权)
参数个数写入门描述符中的ParamCount中,参数手动push,执行结束后通过retf 0xC之类的命令返回。注意堆栈平衡。
参数存储在CS和ESP之间,如 push 1 , push 2, call far xxxx, 堆栈内部情况为:返回地址、原CS、2、1、原ESP、原SS
call可以调用调用门。
对应代码段比CPL权限高时,自动提权,堆栈存储EIP CS 参数 ESP SS。
对应代码段比CPL权限低时,会触发异常,蓝屏。
权限相同时,执行正常,权限不变。
jmp可以跳调用门。
如果权限相同,则执行没问题,堆栈里没有值。
如果涉及提权,则会C005访问异常,代码仍在3环,不会蓝屏。
如果涉及降权,则会蓝屏。
Windows中的提权方式并未使用调用门,而是使用了中断门(旧CPU)。现在的新CPU全部使用快速调用。
中断门调用使用int+中断描述符索引指令来进入,返回时通过iretd返回。 中断门无法携带参数。
断电走2号中断(IDT第三个),双重错误走8号中断
中断描述符表。 r idtr r idtl 。 IDT表中全部存储系统段描述符。其中分为三类,任务门描述符,中断门描述符,陷阱门描述符。
1110为32位中断门 0110为16位中断门
相比较调用门,中断门的压栈数据多了一个EFLAGS寄存器。
当未发生权限转换(无堆栈切换)时,堆栈中的数据是: 返回地址,EFLAGS,原CS
当发生权限转换(堆栈转换)时,堆栈中的数据是:返回地址,原CS,EFLAGS,原ESP,原SS
在中断门中,不可以通过RETF返回,转而使用IRET/IRETD指令返回。
修改中断号为3的中断门的选择子,达到hook int3的效果。当int3指令执行时随便打印一句话。
提示:注意fs和cs。要保证hook代码执行完毕后的现场和刚进0环时的现场一模一样,否则KiTrap03回3环时会有问题。
体会CPU指令缓存的作用.
type为1111时是32位陷阱门,0111时是16位陷阱门。陷阱门执行指令为INT+IDA索引号。与中断门相同。返回指令也相同。不同点是中断门执行时会将EFLAG寄存器中的IF位清零,而陷阱门不会。
IF:中断允许标志位。当为1时,CPU会响应可屏蔽中断请求。为0时,CPU不会响应可屏蔽中断请求。
可屏蔽中断请求:如键盘输入,鼠标点击都是一次可屏蔽中断请求。
不可屏蔽中断请求:CPU必须立即无条件响应的请求,如电源断电。
TSS是一块内存,104个字节,存储着大量的寄存器的值,结构如下图
Intel的设计思想:想通过TSS来实现任务的切换,因为利用TSS可以一次性更换很多寄存器从而实现快速的任务切换。
操作系统的设计思想:觉得TSS不够好,所以windows自己实现了一套任务切换(线程切换)。
最终作用:可以一次性替换很多寄存器,由于TSS表中有CS SS,所以可以用于提权。
CPU中有一个段寄存器叫TR(TaskRegister),TR有96位,其中16位可见部分为选择子,可以找到GDT表中的一个段描述符,通过该描述符加载TR段寄存器中后80位。TR寄存器的Base指示了TSS表的位置。Limit指示了TSS表有多大。
Type:1001表示当前描述符是个TSS段描述符但没有加载到TR寄存器中。 1011表示已经加载到TR寄存器中。
通过LTR指定来装载TR寄存器(96位)。装载后TSS段描述符中的Type-3位会发生改变。LTR指令只能在0环权限中使用。并且仅修改TR寄存器,不修改其他寄存器。
通过STR指令来读取TR寄存器,但只能读取16位,也就是可见部分(段选择子)
准备TSS任务段(104个字节)并将对应成员赋值。CR3通过 !process 0 0 来查看
准备TSS段描述符。指向准备好的TSS任务段。并写入GDT表中合适的位置。(G位为0,AVL为0,因为TSS任务段是以字节为单位的。)
修改TR寄存器指向TSS段描述符。由于ltr指令为特权指令,因此若想在3环实现TR寄存器修改,需要使用JMP FAR或CALL FAR。
当JMP FAR后的段选择子指向一个TSS任务段描述符时,会首先将描述符装载到TR寄存器中,然后在根据TR.Base(TSS任务段)来修改当前寄存器的值。
JMP指令进行任务切换,TSS段中的PreviousLink不会被赋值。且NT位不变。
CALL指令进行任务切换,TSS段中的PreviousLink会被填入上一个TSS段所属TSS段描述符的段选择子。且NT位被置1.
NT位:任务嵌套位。 NT位为0时,IRET/IRETD会从堆栈中取值(中断返回)。 NT位为1时,IRET/IRETD不会再从堆栈中取值返回,而是从当前TSS的PreviousLink对应的上一个TSS中取数据进行返回。(调试模式,单步调试时,NT位会被清0,导致IRET无法从TSS中返回,造成蓝屏)
当我们主动JMP CALL任务段或任务门时,CPU会将ESP SS替换。
当我们通过调用门、中断门这种提权时,CPU会去找TSS,这种可以理解为被动。这时会根据新CS的权限来选择替换ESP0 SS0、ESP1 SS1还是ESP2 SS2。
使用jmp TSS任务段进入0环,使用JMP出来。
使用INT指令+IDT表索引的方式进入任务门。通过IDT表中对应的描述符中的TSS Segment Selector查GDT表找到TSS任务段描述符,使用IRETD返回。
使用任务门进1环。
每个进程的4GB是假的,并不是每个进程占4GB。CPU将线性地址转换为物理地址。在X86架构中转换的方式有两种:10-10-12分页方式和2-9-9-12分页方式(PAE模式)。
mov eax,dword ptr ds:[0x12345678]
0x12345678为有效地址,ds.base+0x12345678为线性地址,通过线性地址进行拆分得到的是物理地址。
默认为2-9-9-12分页,指定为10-10-12分页方式为:将noexecute中的no删除即可
默认为2-9-9-12分页模式(PAE模式),使用工具EasyBCD快捷修改启动引导属性。
CR3是一个寄存器,每个核只有一个。他是唯一一个存储着物理地址的寄存器。这个物理地址指向第一级目录(PDE,共4096字节)。
通过拆分后的第一个10, ×4后,可以找到PDE中存储的一个地址(PTE)。
通过拆分后的第二个10, ×4后, 可以找到PTE中存储的一个地址(物理页)
通过拆分后的第三个12(偏移)加上物理页首地址,得到真正的物理地址。
拆分得到的10为索引,32位下地址宽度4字节,所以需要×4,得到的12为偏移,无需×4
在记事本中随便写入一串字符串,通过CE搜索字符串在内存中的位置,记录内存地址。000AB318
将该内存地址转换为二进制形式,并以10-10-12的形式拆为3部分(高位补0)
获取记事本进程的CR3,通过!process 0 0遍历进程,可以看到CR3(DirBase,多次打开进程,选最后一个打印的。)18394000
在windbg查看CR3+第一个10×4后(0)的物理内存内容。使用!dd(查看物理内存)命令而不是dd(查看线性内存)。
将PTT首地址的后三位清0(这三位是属性,使用时需清0)后,加上第二个10×4(2AC)得到物理页首地址。
将物理页首地址的后三位清0(同上),加上第三个12得到真实的物理地址:184CC000+318 = 184CC318。使用!db命令以字节方式查看该地址,发现了HelloWorld字样。
(PDE:页目录表项 PDT:页目录表 PTE:页表项 PTT:页表)
页目录表(PDT)与页表(PTT)都是4KB(4096字节)。但是每个项是4个字节,所以一共是1024个项,是2的10次方,需要10位二进制来存储。
物理页是4096个字节,但是内存宽度最低1KB,所以一共有4096个地址,是2的12次方,需要12位二进制来存储。刚好为10-10-12分页。
一个页表项(PTE)可以不指向物理页。多个页表项(PTE)可以指向同一个物理页(不同权限)。一个页表项(PTE)只能指向一个物理页。
在正向开发中,我们知道线性地址0是无法读写数据的。我们需要通过查看0的物理地址来搞清楚为什么0无法读写。
可以看到,0地址的PTE为0,并没有具体的物理页,也就没有一个真实的物理内存。这是0地址无法读写的一个原因。那么如何使0地址可以读写呢?
编写代码,实现0地址读写。首先定义一个局部变量。这个变量的地址是可读写的,因此它一定有一个可读写的物理页。输出这个变量的地址。
将0012FF7C根据10-10-12分页方式拆分,查找物理地址。
将局部变量a的PTE 0x26861067 写入至0的PTE中,使用!ed命令来修改物理内存。
让程序执行,观察控制台输出,可以看到我们成功读0地址进行了一波读写。通过该实验可以得出一个线性地址的读写由PDE和PTE共同决定。线性地址仅用于寻找PDE PTE。
PDE PTE后12位(二进制位,3个16进制位)存储着属性,两者的属性字段和属性值很多都是一样的。重复属性是与的关系,必须两者都为1,对应物理页该属性才为1。
有效位。两个P位都为1时该页有效,只要其中一个P位为0时该页无效。
读写位,两者的P位都为1时代表该页内存可读可写,只要有一个为0代表只读不可写。(对于非VT而言,一块内存申请出来,它必然是可读的。)
实验验证:定义一个只读常量str,如下:
修改str的PDE和PTE,使R/W位为1,尝试对str的数据进行修改。
特权等级位,都为0时,特权用户可读写,都为1时,普通用户可读写。
特权用户: 0 1 2 环 普通用户:3环
实验验证:通过修改高2G地址的US位实现3环读写。
仅对PDE有意义,PaseSize位,页大小,当PS位为0时,为4KB小页。寻址如上述正常寻址。
当PS位为1时,对应物理页为大页,PDE直接指向物理页,无PTT参与。线性地址低22位为页内偏移。
Access 是否被访问过位,访问过就全部置1,否则为0。 哪怕是一个字节的地址访问也会将其对应的PDE PTE的A位置1.
Dirty 脏位,是否被写过位。逻辑与A位相同。
全局页,当CR3发生改变后CPU将切换TLB,若G位为1,则该页在CR3发生切换时,TLB中的数据将不被刷新。
TLB与缓存将在后续有专题讲解。
缓存相关,后续缓存章节会有说明,此处暂时不用了解。
缓存相关,后续缓存章节会有说明,此处暂时不用了解。
缓存相关,后续缓存章节会有说明,此处暂时不用了解。
Windows为了节省内存,当一个地址A对应的物理页访问频率很低时,会将其数据写入文件保存,将该物理页提供给哪些访问频率高的地址。在写入文件后,会将该线性地址的PTE的P位置0。9~11位为文件索引。
CPU在拆分线性地址寻找物理地址时,若发现P位为0,则会执行中断表中的0xE号中断。当地址A被访问时,发现P位为0,CPU触发E号中断。操作系统在E号中断处理函数中根据9~10位寻找对应的文件。将数据读出返回。具体的应用会在内存管理章节详细讲解。
思考:当我们申请一块内存用于读写,系统会为这块内存挂载相应的PDE 与PTE,挂载时必然会访问PDT与PTT。但我们在程序中出现的所有地址全部为线性地址,在保护模式下我们无法对任何物理地址进行操作。想要读写PDT PTT就必须将这两个表的物理地址映射成线性地址并挂载PDE PTE。这一步是我们及系统都无法做到的。 CPU为我们贴心的将PDT PTT的物理首地址映射成了线性地址(C0300000)供操作系统访问。这就是页目录表基址和页表基址。
我们已经知道了C0300000就是PDT的首地址线性地址。在拆分C0300000时,第一部分C00指向了PDT首地址。第二部分的C00再次指向了PDT。通过偏移为0找到了PDT。
得出: 第一个C00找到了PDT,第二个C00找到了PDT所在物理页,也就是PTT。第三个偏移0指向表头。而我们又知道PDT内存储着一个个的PDE(PTT表头)。那么也就说明第二个10位数字是PDE的索引。索引×4=C00处的偏移刚好为PDT。那么0 4 8 这种偏移就是正常的PTT表头
。通过这种方法,我们逆向C0300000重新组合,找到PTT基址公式。
PDT基址: C0300000 PTT基址:C0000000+1000×index 得出
每个PDE:C0300000+4×PDI 每个PTE:C0000000+1000×PDI+4×PTI
10-10-12分页模式下,内核文件为ntoskrnl.exe
29912分页模式下,内核文件为ntkrnlpa.exe
因为页大小依然是4KB,所以需要12个二进制位才可以找到整个4KB的每一个地址。
页大小固定,想要增大寻址范围,就要增加PTE的个数(一个PTE等于一个物理页)。在101012模式下,PTE最多FFFFF个。所以想要增加PTE数量就要增加PTE的长度。从4字节改为8字节,那么PTE最多就变成了FFFFFFFFFFFFF个。但PTT表的大小就会缩减一半。所以需要9个二进制位才能描述到每一个PTE。
PTT的大小不等于寻址能力大小,因为PTT内每一个PTE都可以更换。
PDE同理,从4字节变为8字节,数量减少一半,需要9个二进制位才能描述到每一个PDE
还剩2位,windows进行了扩展。扩展成了PDPT(PageDirectoryPointTable)。里面的成员叫PDPTE。成员数量为4。(2个二进制位)。每个成员占8字节。
在boot.ini中将execute改为noexecute后重启即可。
使用EasyBCD工具快捷修改分页模式。
如果PS位为1:大页,35~21位为物理页地址。低21位为页内偏移。每个页2MB大小。
PTE中35-12是物理页基址,24位,低12位补0。物理页基址+12位的页内偏移指向具体数据
在PAE分页模式下,PDE/PTE的最高位称为XD/NX位。不可执行位,为1时,该段内存不可执行。
就是我们常见的DEP数据执行保护
逆向29912分页下的(ntkrnlpa.exe)MmIsAddressValid函数,找出页表基址和windows的判断方法。
当我们想读取一个线性地址的数值时,CPU会先读这个线性地址对应的PDE再读PTE再读物理内存。 这样就相当于多读了8字节的内存,若这个线性地址前两个字节和后两个字节不在同一物理页上,那么多读的字节数会更大。这点在29912分页模式下更为明显。为了加快读取速度,提高效率,CPU内部做了一张表用来存储已读取过得线性地址和物理地址间的映射关系。这张表的读写速度与寄存器一样快速。这张表就是TLB(Translation Lookaside Buffer)。
TLB由于存储在CPU内部,因此一个核一套。当CR3发生了切换(任何对CR3赋值的指令都会触发TLB的切换,与最终值是否改变无关,如mov eax,cr3 mov cr3,eax),TLB就会被切换。操作系统中高2G(系统空间)的物理地址映射关系几乎不变。当CR3发生切换时,若将TLB中的高2G地址刷新会很浪费时间。因此PDE与PTE中的G位决定了TLB在切换时是否保留当前映射关系。1为保留,0为不保留。
G位(全局页位)为1时,整个页在TLB切换时都不会被刷新。
物理页分为普通页(4KB)、大页(2MB/4MB),物理页又分为指令和数据。因此分为4种TLB
INVLPG指令可以强制将某个地址从TLB中刷新掉。而不看G位。
TLB分为DataTLB和InstructionTLB,CRC校验开启时,目标代码由于会被执行,所以InstructionTLB会存一份,由于CRC线程会读取目标代码,所以DataTLB又会存一份。通过更改其中一个TLB内目标地址的映射缓存来实现读取和执行时指向的物理内存不是同一地址。达到欺骗CRC校验的效果。
但3环程序中使用的低2G地址在TLB中经常会被刷新,因此该技术显得很鸡肋。内核层高2G地址几乎不被刷新,此技术应用很强大。
通过层层分析,找出目标代码段上一层代码将要调用的系统API。将API后的目标代码段拷贝至自身内存。修复全局变量、IAT等数据以保证正常执行。在系统API中Hook,将EIP指向拷贝后的代码。由于CRC校验的是原始数据而我们跳回的是拷贝数据,因此可以过掉CRC。但要注意堆栈检测。
中断通常是由CPU外部的硬件设备所触发的,供外部硬件设备通知CPU有事情需要处理。因此又叫中断请求。中断请求的目的是希望CPU能暂停当前正在执行的程序转而处理中断请求所对应的处理程序。(IDT表决定)
X86 CPU有两条中断线,分别是: 可屏蔽中断线(NMI) 不可屏蔽中断线(INTR), 中断描述符表(IDT) 中索引为2的门为不可屏蔽中断处理程序。
当可屏蔽中断请求发生时,CPU会观察EFLAG寄存器中的IF位来决定要不要处理这条中断请求。为1处理,0不处理。
使用CLI指令将IF位置0 ,只用STI指令将IF位置1. clean-set
在CPU中 ,可屏蔽中断由一块专门的芯片来管理,称为中断控制器(APIC)。其管理着每个可屏蔽中断请求对应的中断处理程序的IDT表索引。在其内部也存在一个编号,称为IRQ。每个IRQ对应IDT中的一个索引号。这种对应关系不是一成不变的。
异常是在CPU执行指令时检测到了某种错误,如除0、访问无效物理页等。与中断不同,异常为CPU内部产生的。而中断是外部硬件设备通过中断线传入的。
INT X中断指令称为软件中断。 其本质为异常。 与中断请求无任何关系。中断请求为硬件层面的请求。异常为软件层面的产生。因此,IF位不对异常产生任何影响。
页错误走E号门, 段错误走D号门 ,除0错误走0号门, 双重错误走8号门。
由于中断走IDT表,因此我们需要先找到2号中断在IDT表内的描述符。
00008500`0058113e根据之前学过的描述符结构拆分,发现type为0101,是个任务门,低4字节的高16位是TSS任务段的段选择子
将段选择子拆分,得到一个TSS任务段描述符的索引,这个索引是在GDT表中的
查找GDT表,找一下TSS段描述符
根据段描述符结构拆分得到offset为0x8054af68,数据段base为0,在windbg中查看0x8054af68的数据,这是一个TSS任务段,根据上文TSS任务段结构可以知道0x20处的数据为EIP(4字节)。
使用u addr LXX命令查看8053f3fc处的汇编代码,由于我们已经导入了符号,所以windbg为我们解析出来了这个函数为nt!KiTrap02。
这样就是当不可屏蔽中断请求发生时,CPU的执行流程,具体KiTrap02函数就不分析了,头大,百度上一些帖子是对照WRK或ReactOS源码来分析。只看汇编的话个人实力还没强到那种地步,以后有时间的话再详细分析吧。
8号中断的流程与2号中断完全一致。
CPU缓存是介于CPU和物理内存之间的临时存储器。它容量比内存小得多,但读写速度比内存要快得多。越强大的CPU,CPU缓存大小越大。
与TLB不同,TLB是线性地址和物理地址间的映射缓存。 而CPU缓存是物理地址和地址内的数值(内容)间的映射缓存。TLB+CPU缓存搭配可以大大加快读取速度。
CPU读写内存时,会先去CPU缓存中寻找该物理地址,如果缓存中存在该物理地址,则读写全部对CPU缓存操作。
CPU缓存分为3级,L1 L2 L3。
L1缓存速度最快,容量最小,一个核一个。
L2缓存比L1容量大,速度略慢。一个核一个。
L3缓存比L2容量大,速度最慢。所有核共享一个。
在读写内存时,会依次从 L1 L2 L3进行查找。若在L2中找到了,会将缓存更新到L1(提升下次访问速度)。若在L3中找到了,会将缓存更新到L1 L2。所以L1一直都在变。
Intel白皮书3卷11章(Cache Control)详细讲解了CPU缓存。
UC:无缓存 WC:合并写(组合写) WT:直写 WP:写保护 WB:回写
例子:向一个地址写数据时,会先搜索L1,若没有则搜索L2,假设L2中发现了这个地址。在向L2写入数据的同时,会把这份映射关系同时写入L1中。
但WC一次只能写4个地址。如果超过4个地址就不采用WC的形式。
当向一个地址写数据时,会先将数据写入缓存(标记缓存)。而不写入物理内存,什么时候写入物理内存由CPU缓存控制器决定。
因为回写无需第一时间写入物理内存,因此效率较高。常用的缓存类型就是回写类型。
当向一个地址写数据时,在写入缓存的同时也要写入物理内存。
因为直写需要同时写入缓存与物理内存,因此时间花费的比回写多。但直写可以保证数据的同步性,对于一些多媒体功能(需要实时刷新内存)这种需求,直写则是最好的选择。否则可能会出现所谓的“掉帧、卡顿”的情况。
该页不能写。即使RW位置1也不可以。
PageWriteThrough : 页直写位。 当为1时,数据写入CPU缓存的同时也要写入内存。 为0时,数据写入CPU缓存时无需写入内存,具体什么时候更新到内存,由CPU缓存控制器决定。
PageCacheDisable : 页缓存禁用位。 为1时,该页的数据在写入时不会写入到CPU缓存中,读写时直接操作物理内存。 0则相反。
PageAttributeTable:缓存页标志位。 当此位为1时,该页用于存储缓存相关数据。既不可作为数据段使用,也不可作为代码段执行。因此在IsAddrValid函数中,当判断PAT为1才会返回false。
注意: 控制寄存器中有些位一旦置1,则代表对应功能直接启用。 有些位只有置1了,对应功能才可以被启用,具体启不启用看细化到PTE之类上面的控制位。 所以学习控制寄存器属性时需要留意。
Protection Enabled:保护启用位,为1时是保护模式 ,为0时是实模式。 1时仅启用段保护机制。
Paging:页保护启用位(分页机制位),为1时代表启用分页保护机制。 为0时不启用分页保护机制(线性地址=物理地址)。
Write Protect:写保护位,当WP为1时,超级特权用户(0环)不可以向用户层只读地址写入数据。
与数学运算相关,不用了解。
Task Switched: 任务切换位。当call入任务门时,TS位置1。当从任务门中返回时,TS位置0。
Cache Disable : 缓存禁用位。 当置1时,所有缓存全部禁用。相当于缓存的总开关。
Alignment Mask:对齐位。为1时,启用对齐检查。为0时,关闭对齐检查。
Cr1寄存器在X86架构中为保留状态,并未使用。
当程序执行发生缺页异常时(E号中断),CPU会将触发了缺页异常的线性地址写入Cr2寄存器。供异常处理函数(E号中断)使用。
如 00401000:mov eax,[12345678],若12345678地址无效,则CR2中存12345678. 若401000地址无效,则CR2存00401000.
页目录表基址。
PageLevelCacheDisabled:缓存禁用位。 为1时,禁用页表缓存。该位仅在CR0.PG=1且CR0.CD=0时才有效果。
Intel手册原文:
PageLevelWriteThrough:页直写位。 为1时,页表使用直写缓存,为0时页表使用回写缓存。
Intel手册原文:
当访问一个32位分页模式(101012)下的PDE时,PCD与PWT取自CR3寄存器。
当访问一个PAE模式(29912)下的PDE时,PCD与PWT取自PDPTE相关寄存器
当访问一个PTE时,PCD与PWT取自对应的PDE。
当访问一个从线性地址翻译过来的物理地址时,PCD与PWT取自与PTE或PDE。
Virtual-8086 Mode Extensions:虚拟8086模式扩展位。置1时,启用虚拟8086模式的中断和异常处理。置0时,不启用。
Protected-mode Virtual Interrupts:虚拟8086中断位。置1时,启用VIF(virtual interrupt flag)位。置0时,VIF位无效。
Time Stamp Disable:时间戳禁用位。置1时,只有特权级用户才可以执行RDTSC指令。置0时,所有用户都可以执行RDTSC指令。 该指令用于获取Tick值。
Debugging Extensions:调试扩展位。置1时,调试寄存器DR4 DR5启用。置0时,DR4 DR5保留。DR4 DR5启用时作为DR6 DR7使用。
Page Size Extensions:页尺寸扩展位。置1时,PDE的PS位才有效果。置0时,PDE的PS位作废。
Physical Address Extensions:物理地址扩展位。 为1时,29912分页。为0时,101012分页。
Machine-Check Enable:机器检查启用位。置1时,会检查硬件连接。置0时,不会检查硬件连接。
Page Global Enable:全局页启用位。置1时,PDE PTE的G位才有效果。否则无效果。
Performance-Monitoring Counter Enable:性能监控计数器启用位。置1时,3环可以执行RDPMC指令。否则只能在特权级执行。
VMX-Enable:VT标志位。为1时,代表处于VT模式下。为0时,未处于VT模式。特权级为-1
VT所有资料参考Intel手册-卷3B
SMX-Enable:更安全模式位(Safer-mode)。为1时,处于SM模式下。否则未处于。特权级为-2
SuperModeExecuteProtect:特权执行保护。为1时,特权级不能执行US=1的代码。
SuperModeAccessProtect:特权访问保护。为1时,特权级不能访问US=1的数据。
在64位中,CR0.AM不再作为扩展位存在,而是控制SMEP与SMAP。当AM=0时,SMEP和SMAP失效。
其他位的作用可以参考Intel白皮书第三卷中的2.5章节:Control Registers
中文版链接:https://pan.baidu.com/s/1Z_G9H_oerIV5aA-GslaO6g 密码:gtg7
英文版链接:https://pan.baidu.com/s/1OksH-A2ezKAe6mYC7MdTFw 密码:4ju9
具体代码等到第二章驱动学习结束后再回头完成这个保护模式的测试。
HOOK绘制做方框透视。 为什么你的框刷新速度慢跟不上人物模型的位置?
因为你的HOOK代码用到了JMP,而且多数情况是个跨段跳转。 这种JMP会将缓存的指令清空。造成执行速度减缓。
而对于 绘制 这种频率极高的行为,这种速度减缓会被放大,也就造成了框子跟不上模型的现象。
而call指令无论跳多远都不会清空缓存,因此hook时使用call而不是jmp会优化绘制速度。让你的框子不会刷新过慢。
HOOK绘制做方框透视。 为什么你的框刷新速度慢跟不上人物模型的位置?
因为你的HOOK代码用到了JMP,而且多数情况是个跨段跳转。 这种JMP会将缓存的指令清空。造成执行速度减缓。
而对于 绘制 这种频率极高的行为,这种速度减缓会被放大,也就造成了框子跟不上模型的现象。
而call指令无论跳多远都不会清空缓存,因此hook时使用call而不是jmp会优化绘制速度。让你的框子不会刷新过慢。
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,ss
/
/
ss可读可写
mov ds,ax
/
/
ds可读可写
mov dword ptr ds:[var],eax
/
/
ds此时为ss,不报错,说明两个段寄存器权限相同
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,ss
/
/
ss可读可写
mov ds,ax
/
/
ds可读可写
mov dword ptr ds:[var],eax
/
/
ds此时为ss,不报错,说明两个段寄存器权限相同
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,cs
/
/
cs可读可执行不可写
mov ds,ax
/
/
ds可读可写
mov dword ptr ds:[var],eax
/
/
ds此时为cs,写入时报错,说明Attribute属性存在
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,cs
/
/
cs可读可执行不可写
mov ds,ax
/
/
ds可读可写
mov dword ptr ds:[var],eax
/
/
ds此时为cs,写入时报错,说明Attribute属性存在
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,fs
/
/
fs 的 base为TEB 用ds编译不过去
mov gs,ax
/
/
gs 的 base为
0
mov eax,gs:[
0
]
/
/
gs此时为fs,写入不出错,说明Base属性存在 fs.base
+
0
mov dword ptr gs:[var],eax
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,fs
/
/
fs 的 base为TEB 用ds编译不过去
mov gs,ax
/
/
gs 的 base为
0
mov eax,gs:[
0
]
/
/
gs此时为fs,写入不出错,说明Base属性存在 fs.base
+
0
mov dword ptr gs:[var],eax
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,fs
/
/
fs 的 base为TEB 用ds编译不过去
mov gs,ax
/
/
gs 的 base为
0
mov eax,gs:[
0x1000
]
/
/
写入出错,超过了fs的limit,说明Limit属性存在 fs.base
+
0x1000
/
/
mov eax,ds:[
0x7FFDF000
+
0x1000
] 不报错
mov dword ptr gs:[var],eax
}
}
int
main(
int
argc,char
*
argv[]){
int
var
=
0
;
__asm{
mov ax,fs
/
/
fs 的 base为TEB 用ds编译不过去
mov gs,ax
/
/
gs 的 base为
0
mov eax,gs:[
0x1000
]
/
/
写入出错,超过了fs的limit,说明Limit属性存在 fs.base
+
0x1000
/
/
mov eax,ds:[
0x7FFDF000
+
0x1000
] 不报错
mov dword ptr gs:[var],eax
}
}
gdtr寄存器(windbg伪寄存器,是windbg通过sgdt lgdt指令获取的,为了方便用户,才模拟了一个寄存器叫gdtr,实际是没有这个寄存器的) :
存两个值,一个是GDT表在哪里,一个是GDT表有多大
48
位 有
32
位存储在哪里,
16
位存储大小
r gdtr r查看gdtr寄存器的前
32
位也就是位置
r gdtl r查看gdtr寄存器的后
16
位也就是大小 都查gdtr
dd xxxx
4
字节查看内存
dq XXXX
8
字节查看内存
dq xxxx Lnum 查看固定数量元素的内存
GDT表中存储着段描述符,每一个段描述符
8
个字节
gdtr寄存器(windbg伪寄存器,是windbg通过sgdt lgdt指令获取的,为了方便用户,才模拟了一个寄存器叫gdtr,实际是没有这个寄存器的) :
存两个值,一个是GDT表在哪里,一个是GDT表有多大
48
位 有
32
位存储在哪里,
16
位存储大小
r gdtr r查看gdtr寄存器的前
32
位也就是位置
r gdtl r查看gdtr寄存器的后
16
位也就是大小 都查gdtr
dd xxxx
4
字节查看内存
dq XXXX
8
字节查看内存
dq xxxx Lnum 查看固定数量元素的内存
GDT表中存储着段描述符,每一个段描述符
8
个字节
数据段:
A位:数据段是否被访问过位,访问过为
1
,未访问过为
0
段描述符是否被加载过
W位:数据段是否可写位,可写为
1
,不可写为
0
E位:向下扩展位,
0
向上扩展:段寄存器.base
+
limit区域可访问。
1
向下扩展:除了base
+
limit以外的部分可访问。
代码段:
A位:代码段是否被访问过位,访问过为
1
,未访问过为
0
段描述符是否被加载过
R位:代码段是否可读位,可读为
1
,不可读为
0
C位:一致位。
1
:一致代码段。
0
:非一致代码段 具体后文解释
数据段:
A位:数据段是否被访问过位,访问过为
1
,未访问过为
0
段描述符是否被加载过
W位:数据段是否可写位,可写为
1
,不可写为
0
E位:向下扩展位,
0
向上扩展:段寄存器.base
+
limit区域可访问。
1
向下扩展:除了base
+
limit以外的部分可访问。
代码段:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2021-7-1 13:13
被SSH山水画编辑
,原因: 补充控制寄存器知识点