X86处理器通过RING( 环 )来进行访问控制的。共有4层,从0,到3。0层拥有最高的权限,3层拥有最低的权限。按照intel原有的设计,应用程序在3层,只能访问3层的数据,操作系统在0层,可以访问所有层的数据,而其他驱动程序位于1,2层,可以访问除0层以外的数据,应该是很好的设计,这样操作系统工作在最核心没有人可以修改他,其他驱动程序在1,2层,有要求则向0层调用,可以做到操作系统的完全安全性.
但现在的OS,包括WINDOWS和LINUX都没有采用,而只是使用2层,RING0和RING3,分别来存放操作系统数据和,应用程序数据.据说是为了兼容其他处理器,从而导致了一旦驱动加载了,就运行在0层,从而就拥有了和操作系统同样的权限,可以做任何事情,而ROOTKIT也就随之而生了.
虽然大多数指令可以同时使用于0层和3层,但有些和系统设置相关的指令,却只能在0层被使用
? LGDT — Load GDT register.
? LLDT — Load LDT register.
? LTR — Load task register.
? LIDT — Load IDT register.
? MOV (control registers) — Load and store control registers.
? LMSW — Load machine status word.
? CLTS — Clear task-switched flag in register CR0.
? MOV (debug registers) — Load and store debug registers.
? INVD — Invalidate cache, without writeback.
? WBINVD — Invalidate cache, with writeback.
? INVLPG —Invalidate TLB entry.
? HLT— Halt processor.
? RDMSR — Read Model-Specific Registers.
? WRMSR —Write Model-Specific Registers.
? RDPMC — Read Performance-Monitoring Counter.
? RDTSC — Read Time-Stamp Counter.
最后2条指令 RDPMC和RDTSC在CR4的位4(PCE)和位2(TSD)被设置的情况下可以同时被0层和3层调用.任何违反上面规定的操作,WINDOWS下,可能会产生通用保护故障的异常.
另外还有些所谓的IO敏感指令,包括
· cli — stop interrupt processing (on the current CPU)
· sti — start interrupt processing (on the current CPU)
· in — read data from a hardware port
· out — write data to a hardware port
这些指令在0层可以直接被使用,在3层被使用的时候还要检查IO许可位图综合判断是否允许调用,详细的可参考INTEL或AMD的软件开发手册
2.2 保护模式的内存保护
X86处理器的保护,分为段式保护和页式保护.
首先介绍下CPU的运行模式,本文介绍的CPU默认为X86架构。
目前的CPU的运行模式主要有以下几种:
1,实模式:CPU在启动后所处的模式,大家熟悉的DOS就是运行在这种模式下,缺点是默认只能访问1MB内存,单任务.优点就是所有程序都运行在0层,应用程序和系统程序具有同样的权限,可以说是病毒编写者的最爱.
2,保护模式:建议的CPU运行模式,支持分段,分页,可以访问更大的内存,可以运行多任务,大家熟悉的WINDOWS,LINUX均运行在该模式下,提供了丰富的系统管理特性.
3,系统管理模式:(SMM)这个模式提供给操作系统或执行环境一个透明的机制来处理电源管理和OEM独有的特性
4,虚拟8086模式:为了能在保护模式中运行原来能在实模式下运行的程序,所特意提供的一种虚拟的8086模式,在这种模式下允许在保护模式的多任务环境下执行实模式程序.所以我们可以在WIN9X,WIN2K下运行大多数的DOS程序.
5.64位扩展模式:INTEL和AMD均推出了支持64位的CPU,支持64位的线性寻址,其又分位个子模式;
a::64位模式支持64位线性寻址,支持超过64GB的物理地址,所有64位程序均运行在此模式下.
b: 兼容模式:原有的32位程序运行于此模式下,从而可以使32位程序不用修改就直接运行于64位操作系统下,保护了用户的投资.
X86处理器的分段模式包括以下几种形式:
1平坦模式:
在这种模式下,系统能访问一个连续的,不分段的地址空间,所有的段被映射到同一个地址空间,所有的段都与相同的基地址0,界限为4GB(IA32).我们所用的操作系统,无论是WINDOWS,还是LINUX都运行在这种模式下.
2,多段模式:
不同的段,代码段,数据段,堆栈段等位于不同的段中,由于现在OS大多不采用,所以也就不详细介绍了.
页式保护:
由于现在操作系统大多采用页式保护来进行操作系统的保护,所以稍微详细介绍下.
首先要打开分页模式必须使CPU处于保护模式下,实模式下不能使用分页。
只有在CR0寄存器的位0为1(启用保护模式)位31为1(启用分页)后才能使用分页模式.
我们在访问系统的时候一般会通过虚拟地址来访问,后者通过分段机制转换为线性地址,(份段是不能被禁止的),如果禁止分页,那么直接将线性地址当作物理处理而发到地址线上.如果启用了分页,那么再通过分页机制,将线性性地址转为物理地址,再发到地址线上进行访问.
只要进入保护模式,那么自动启用段级保护,只要启用分页,那么自动启用页级保护.虽然在保护模式下我们不能关掉段级保护,但却能关掉页级保护,只需要做如下操作:
1,清除CR0的WP位
2,对每一级的页目录项和页项设置R/W位为W,U/S位为U即可以把页设置为可写的用户页,也就相当于关闭了页级保护.
我们在访问内存的时候,系统会做以下操作:
通过段选择子,在GDT中找到段基地址,进行各种特权检查,确保目标段可以使用,然后将段基地址加上我们的偏移行成线性地址,然后通过分页形成物理地址,放到地址线上进行访问.
实际上系统为了性能考虑,增加了所谓的段描述符映射寄存器,位于CPU内部,除了第一次访问内存需要做以上事情外,对以后的所有访问,如果不改变段的话,均通过映射寄存器来访问,以增加效率,以前有人实现在DOS下访问1M以上空间就是利用INTEL的缺陷,将CPU切入保护模式,设置4GB界限,然后切换回实模式.INTEL CPU在切换回来后并没有重新设置映射寄存器,导致界限还是4GB,从而访问额外的内存.
系统分页可以包括以下几种情况:
1 非PAE模式下4K分页,线性地址被分为3个部分,最高10位为页目录表偏移,中间10位为页表偏移,最低12位为4KB页内偏移,从而可以寻址2^10*2^10*2^12=4GB内存
2, 1非PAE模式下4M分页,线性地址被分为2个部分,最高10位为页目录表偏移, ,低22位为4MB页内偏移,从而可以寻址2^10*2^22=4GB内存.
其中页目录表中存放的是页目录项,页表中存放的是页表项,4M分页只有页目录没有页表.
注意我们前面所说R/W位位于位1,代表了该页是否可写(0,只读,1可写),U/S位位于位2,代表了该页是否为系统页(0,系统页,只能被RING0程序访问,1,用户页,可以同时为0,3程序访问).
还有就是PAE模式,本来这种模式是为了寻址超过4GB内存才被使用的,,它可以让CPU支持36位物理寻址,支持物理地址达64GB,但实际意义不大,超过4GB的话建议大家还是用64位CPU来寻址,而PAE模式有一定性能上的损失.不过之所以在这里介绍他是因为,AMD首先在其AMD64 CPU中率先推出的NX执行保护技术,该技术对于系统来说还是有很大的革新的意义.
众所周知,很多攻击技术都是利用缓冲区溢出,来实现的,就是说利用程序的漏洞,攻击者可以设定一段精心构造的代码,然后通过溢出,将堆栈中的返回地址替换为攻击者设定好的地址从而执行相应的代码,一般包括堆溢出和栈溢出,他们的特点是都是位于数据段中,不同于我们通常程序代码所处的代码段.而以前的X86处理器并没有执行段的概念,只要该页是可读的,那么他就可以被执行,虽然有很多方法可以从软件上来进行补救,但都无法从根本上解决缓冲区溢出的问题,基于这种情况下AMD首先推出了NX执行保护的概念,INTEL也随后跟进了,也就是说在硬件上增加了执行保护.所有的程序和代码在执行前都会进行页保护检查,凡是违反保护的,将一律不被执行,所以尽管你还是可以溢出,但你已经无法获得执行了,所以可以从根本上很好的解决缓冲区溢出的问题.
简单介绍下64位CPU的一些东西:
AMD64是一个架构。在这个架构下,支持64位操作数,虚拟地址空间也被扩展到了64位。AMD64为64位的操作增加一个名为Long Mode的CPU运作模式。在Long Mode下有2种执行模式:一种被称为64-Bit Mode。一种被称为Compatibility Mode。原来32BIT的CPU支持的32位、16位保护模式和实模式则被称为Legacy Mode. 从AMD64 Architecture Programming Manual中有下表描述AMD64支持的各种模式。
分页机制在Long Mode下有2种可能性。 1种是1个PAGE为4K,另外1种为1个PAGE为2M。4K PAGE的地址转换由4级页表转换得来。而2M PAGE的地址转换由3级页表转换得来。
4K PAGE的地址转换由4级页表转换,是把地址划分为6组。Bit48-63是Bit 47的符号扩展,不在转换中起作用。为什么要这么设计要问AMD。Bit39-47是Page Map Level 4(PML4) Index,Bit30-38是Page Directory Pointer(PDP) Index,Bit21-29是Page Directory(PD) Index,Bit12-20是Page Table (PT) Index。转换过程与32位下的一样,就不多介绍了,要介绍的是各表的Entry因为他们和”NX执行保护”大大相关。下面是各表的Entry格式
2M的PAGE则是3级转换得出物理地址位置。
把地址划分为5组。Bit48-63是Bit 47的符号扩展,不在转换中起作用。Bit39-47是Page Map Level 4(PML4),Bit30-38是Page Directory Pointer(PDP),Bit21-29是Page Directory(PD)。省略了Page Table。
各表项如下:
Page Map Level 4(PML4),Page Directory Pointer(PDP) Index,Page Directory(PD) Index, Page Table (PT) Index都是以表的形式存在。每个表容纳一定数量的Entry。我们可以看到在各个表的Entry都会有1个NX Bit,位于Bit 63。其他的Bit我们暂不关心,这个NX bit则是这次”数据执行保护”的关键所在。
No Execute (NX) Bit.,在各Entry的中的Bit63决定当对应的项映射到虚拟内存中,对应的地址是否可以是代码并且被执行。当NX位被设定为1,代码不可以从对应的虚拟地址执行。如果NT位被设定为0,代码可以从对应的地址执行。
NX是扩展功能,通常在某个特殊寄存器的某个Bit会有一个标志,来起用NX功能。这个Bit就是No-Execute Enable (NXE) Bit。在Extended Feature Enable Register(EFER)寄存器的Bit11。EFER寄存器是Model Specific Register(MSR)的其中一个。EFER位于MSR的C0000080H处。可以通过特权指令RDMSR/WMSR来进行读写。当NXE为1时,NX功能就启动了。所谓”数据执行保护”也就被启动出来。在启动NX功能前,必须通过CPUID指令来确定NX功能确实存在。CPUID指令的Function 80000001H用于检查CPU的Signature和AMD扩展功能。可用来检查NX功能是否存在。
当NXE,也就是EFER的Bit11被置为1前,NX Bit被认为是保留位。如果将NX Bit置1则会导致一个Page Fault Exception。只有将NXE置为1后,NX位才开始发挥作用。当NX发生作用时,如果每次执行指令时都去检查NX Bit,恐怕CPU的效率不会很好。因此,NX Bit只在指令TLB(TLB是虚拟地址和物理地址转换用的CACHE)加载的时候才会被检查。CPU在指令预取时,如果没有命中指令TLB,就需要尝试将虚拟地址和物理地址转换加载到TLB。此时,如果NX Bit被检查到为1,就引发一个Page Fault exception。而且无论是否是RING0,这项检查都无法逃过。整个过程和违反其他保护的检查的结果一样都会触发页中断处理. 由于每个表Entry都有NX位。因此不同表Entry的能控制的NX范围也不同,比如PTE中的NX只能控制对应的PAGE是否NX。而PDE的NX位则作用于PDE所指向的整个PT。当PDE的NX位为1,无论对应的PTE是否NX被置位,对应的页面就是NX,不可执行.
上面讲到的都是Long Mode的NX。AMD在设计CPU时,同样将NX设计到了Legacy Mode中。但是Legacy Mode的情况有些特殊,32BIT的PTE,PDE需要兼容INTEL的IA32的PTE,PDE。而且IA32的PTE,PDE并没有什么空闲的地方可以插入NX位。为了兼容INTEL,所以只能从INTEL原有的模式上下手.从PENTIUM PRO开始,INTEL定义了一个名为Physical Address Extension(PAE)的扩展模式。在这个模式下,能够使用到36Bit的物理地址。能动脑筋的恐怕就是Physical Address Extension(PAE)起用后的PDPE,PDE,PTE了。在PAE起用后,PDPE,PDE,PTE被扩展为64Bit. 由于PAE只用36BIT的地址空间,Bit63到可以占用一下。INTEL在PAE中规定了4K和2M这2种页面格式,那AMD看来也必须兼容4K和2M这2种格式的页面。AMD在设计Legacy Mode的NX功能时,不象Long Mode那么全面。并没有在所有的级别都设定了NX Bit。
首先看看Legacy Mode情况下的4K页面。
虚拟地址分成4部分。Bit30-31是Page Directory Pointer(PDP),Bit21-29是Page Directory Index,Bit 12-20是Page Table(PT) Index。最低12位为页内偏移
Legacy mode PAE 4K PDPE
Legacy mode PAE 4K PDE
Legacy mode PAE 4K PTE
我们可以看到在Legacy PAE Mode下,4K的页面模式,只有PTE和PDE能控制NX。而PDPE则无法控制NX。
再看看Legacy Mode情况下的2M页面。
虚拟地址分成3部分。Bit30-31是Page Directory Pointer(PDP),Bit21-29是Page Directory Index。
Legacy mode PAE 2M PDPE
Legacy mode PAE 2M PDP
在Legacy PAE Mode下,2M的页面模式,只有PDE能控制NX。而PDPE同样无法控制NX。
自从AMD64推出这样的规格后。INTEL也开始走“兼容”路线。INTEL称为EM64T,NX规格的兼容是EM64T的一部分。
NX规格可以使得某片区域的RAM无法被执行。但是又不影响读写,这样的区域很符合堆和栈的特征。通常堆和栈是只能被读写,不被用来生成可执行代码的。这个很不错的特性被WINDOWS XP SP2所利用。就是所谓的”数据执行保护”了。但是”数据执行保护”有其局限性,因为”数据执行保护”归根结底是个CPU提供的硬件保护措施,如果CPU不支持NX特性,那么”数据执行保护”就无从谈起。不过现在的CPU也大多都支持NX”数据执行保护”的这个功能了.
数据执行保护”对一些应用提出的新的要求。比如可执行文件的壳程序以及大家都很习惯用的API HOOK,远线程等等。启动了”数据执行保护”后,正常情况下通过malloc/HeapAlloc获得的RAM,则是不可执行的。意味着当壳解码真正执行内容时,如果是解码到malloc/HeapAlloc中获得的空间,那代码会无法执行起来。
必须在执行目标代码前通过VirtualProtectEx,来改变页面的属性为PAGE_EXECUTE/PAGE_EXECUTE_WRITE_COPY/PAGE_EXECUTE_READ,或者PAGE_EXECUTE_READWRITE。这对可执行文件的壳作者提出了新的挑战。如果壳代码不顾一切,先将所有的的页面全部设定为PAGE_EXECUTE_READWRITE,那么就彻底破坏了NX的原有保护功能。在壳代码所在的进程范围内无法阻止病毒蠕虫和恶意入侵。通常HEAP和STACK是只能被读写,不被用来生成可执行代码的。这个很不错的特性被WINDOWS XP SP2所利用。就是所谓的”数据执行保护”了。但是”数据执行保护”有其局限性,因为”数据执行保护”归根结底是个CPU提供的硬件保护措施,如果CPU不支持NX特性,那么”数据执行保护”就无从谈起。
在WINDOWS XP SP2以及其他能支持NX的WINDOWS OS下,有时候由于NX的存在导致的问题会严重影响系统。原先在没有NX的情况下勉强是跑起来了。但是一旦起用了NX功能,应用就崩溃了,所以MS为我们还保留了一手, Boot.ini为NX功能新增了2个开关。/NOEXECUTE和/EXECUTE。/NOEXECUTE用来起用NX功能。而/EXECUTE则关闭了NX功能。当然是否关闭他就看你自己的要求了.
虽然有了NX执行保护,但并不能说就高枕无忧了,网上已经有方法号称可以避开32位XP Serivce Pack 2的NX保护来溢出.
因为WINDOWS中提供了关闭NX的机制,只要溢出程序能够以ProcessExecuteFlags为MEM_EXECUTE_OPTION_ENABLE而不是MEM_EXECUTE_OPTION_DISABLE来调用NtSetInformationProcess就可以关闭当前进程的NX执行保护
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
NtSetInformationProcess(
NtCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x2
sizeof(ExecuteFlags)); // 0x4
而NTDLL中已经为我们构造好了整个调用过程;首先把代码执行导向执行MOV AL,1
RET的地方,比如xor eax,eax/inc eax/ret; mov eax, 1/ret;等等,NTDLL中已经有了
ntdll!NtdllOkayToLockRoutine:
7c952080 b001 mov al,0x1
7c952082 c20400 ret 0x4
然后执行导向
ntdll!LdrpCheckNXCompatibility+0x13:
7c91d3f8 3c01 cmp al,0x1
7c91d3fa 6a02 push 0x2
7c91d3fc 5e pop esi
7c91d3fd 0f84b72a0200 je ntdll!LdrpCheckNXCompatibility+0x1a (7c93feba)
ntdll!LdrpCheckNXCompatibility+0x1a:
7c93feba 8975fc mov [ebp-0x4],esi
7c93febd e941d5fdff jmp ntdll!LdrpCheckNXCompatibility+0x1d (7c91d403)
ntdll!LdrpCheckNXCompatibility+0x1d:
7c91d403 837dfc00 cmp dword ptr [ebp-0x4],0x0
7c91d407 0f8560890100 jne ntdll!LdrpCheckNXCompatibility+0x4d (7c935d6d)
ntdll!LdrpCheckNXCompatibility+0x4d:
7c935d6d 6a04 push 0x4
7c935d6f 8d45fc lea eax,[ebp-0x4]
7c935d72 50 push eax
7c935d73 6a22 push 0x22
7c935d75 6aff push 0xff
7c935d77 e8b188fdff call ntdll!ZwSetInformationProcess (7c90e62d)
7c935d7c e9c076feff jmp ntdll!LdrpCheckNXCompatibility+0x5c (7c91d441)
ntdll!LdrpCheckNXCompatibility+0x5c:
7c91d441 5e pop esi
7c91d442 c9 leave
7c91d443 c20400 ret 0x4
返回回来后原来进程的NX就已经被关闭了,然后就可以做想做的事了,至于如何对付我想就不用多说了吧..