在现有公开的永恒之蓝RCE的exp中并没有直接使用doublepulsar后门,而是采用了Ring0 shellcode和Ring3 shellcode结合的方式,通过Ring0 shellcode来执行用户自定义的Ring3 shellcode。
在公开的永恒之蓝RCE的exp中几乎都使用了同一个Ring0 shellcode,包括Metasploit中的ms17_010_eternalblue.rb
https://github.com/worawit/MS17-010/blob/master/shellcode/eternalblue_kshellcode_x86.asm
同时公开的永恒之蓝exp中,除了zzz_exploit.py这种需要有权限的命名管道或者低权限的smb用户以外的远程RCE利用的exp都是不稳定的,会导致概率性的重启问题。
经过测试发现只保留exp中的部分Ring0 shellcode进行攻击,该exp是可以稳定执行的。
保留部分如下:
1 2 3 4 5 |
|
可以判定导致不稳定重启的原因存在于shellcode中,针对shellcode进行优化处理,有可能可以解决该问题。
可以搜集到的关于exp中Ring0 shellcode的分析资料很少,想解决永恒之蓝exp的概率性重启问题,要针对shellcode进行优化,就必须了解exp中的Ring0 shellcode中的执行细节,所以就有了这篇文章的产生。
根据执行过程中所作事情的不同,将整个执行流程分为了四个阶段
二阶段shellcode:触发系统调用后还原syscall(sysenter),遍历进程寻找lsass.exe或者spoolsv.exe进程,使用异步过程调用 (APC)将用户级 shellcode 注入进程中。
三阶段shellcode:执行KernelApcRoutine(该回调函数为KeInitializeApc的KernelRoutiune参数,原本用于销毁KAPC的函数地址,而现在用于执行自定义功能)。
四阶段shellcode:四阶段shellcode为三阶段中被从内核态拷贝到用户态的shellcode,在三阶段shellcode执行后会紧接着执行,用于执行之前存储的CreateThread创建线程,执行用户自定义的Ring3 shellcode。
Ring0 shellcode相当于一个中转,通过APC注入的方式从漏洞利用成功后的内核态来执行用户态的shellcode。
这里存在一个问题,在漏洞利用成功后,明明已经可以控制eip了,为什么还要进行syscall的hook呢?
原因在于,shellcode 代码在Ring0下执行时,IRQL是DISPATCH_LEVEL,在该级别下无法使用分页内存。
内核态下执行ZwAllocateVirtualMemory函数可以在指定进程中保留一系列应用程序可以访问的虚拟地址,用于将存在于内核态下的shellcode copy到用户态中,该函数需要使用到PASSIVE_LEVEL中断请求级别,通过劫持syscall获取到的IRQL是PASSIVE_LEVEL。
一阶段shellcode用于将二阶段的shellcode安装为sysenter或者syscall的钩子。
在x86下是sysenter,在x64下是syscall。
MSR
MSR是模式指定寄存器,用于设置CPU 的工作环境和标示CPU 的工作状态,包括温度控制,性能监控等。
rdmsr:读取ECX中的MSR寄存器地址的值,低32位存储到EAX中,高32位存储到EDX中。
wrmsr:向ECX中存储的MSR寄存器地址写入EAX:ECX的值,其中低32位存储在EAX中,高32位存储在EDX中。
从inter CPU手册中可以看到,X86架构下 MSR寄存器 在0x176的地址存储了 我们关注的SYSENTER_EIP_MSR值,通过对其修改可以达到hook sysenter的目的,在X64下其值为0xC0000082。
Ring0 shellcode通过这种方式将X86下的sysenter的地址替换为了自定义的函数地址,实现了钩子的安装。
其中的set_ebp_data_address_fn函数,用于调整ebp栈底空间,在HAL heap中寻找了一块可用的空间来存储临时变量和作用后续的KAPC结构体的空间使用。
X86 sysenter hook:
x64 syscall hook:
在发生系统调用时,syscall_hook 函数的代码会被执行。
所以对syscall_hook函数下断点,等待一阶段shellcode执行完成后,发生系统调用时该函数就会被执行到。
syscall_hook函数用于执行原有sysenter/syscall中的初始化代码,限制APC排队数量为1,恢复原有系统调用,函数内部调用了r3_to_r0_start进行了后续的APC注入的操作。
在二阶段shellcode开头的部分使用了sysenter/syscall中的前几行指令,用于执行原始系统调用。
X86架构下:
X64架构下:
cmpxchg
为比较交换指令,操作数1与eax做比较:
1 2 |
|
如果相等,则 操作数1 = 操作数2
,ZF = 1。
如果不相等,则 eax = 操作数1
,ZF = 0。
shellcode中的cmpxchg运算完成后,将ebp + 8,即ffdfffb8的值置1
去掉hook,还原回原地址,原有的sysenter跳转到的eip为nt!KiFastCallEntry
。
在执行完成syscall_hook函数后会自动跳转到KiFastCallEntry/KiSystemCall64 还未执行代码的位置。
KiFastCallEnter函数属于内核中的导出函数,通过其可以定位到内核的首地址。
shellcode中从KiFastCallEnter所属的页为起始位置,按页为单位向前遍历,直到遍历到页开头为MZ标记为止。
在遍历到内核首地址后,执行PsGetCurrentProcess获取进程的EPROCESS结构体,通过使用PsGetProcessImageFileName获取ImageFileName在EPROCESS中的偏移地址,来判断操作系统版本从而推算出EPROCESS.ThreadListHead的位置。
win_api_direct函数用于根据eax寄存器转入的API hash来寻找api的地址并执行。
动态分析执行流程
执行PsGetCurrentProcess获取进程的EPROCESS结构体。
执行PsGetProcessImageFileName获取EPROCESS.ImageFilename的地址,用它减去EPROCESS的首地址,得到偏移。
通过ImageFilename在EPROCESS中的偏移判断操作系统版本,从而推算出EPROCESS.ThreadListHead的偏移地址。
验证一下,win7系统下ThreadListHead在EPROCESS中的偏移确实是0x188
通过遍历EPROCESS.ThreadListHead的成员减去KPRC.KPRCB.CurrentThread(当前线程首地址)的方式来获取ThreadListEntry相对于ETHREAD结构体的偏移。
最后得到ThreadListEntry相对于ETHREAD结构体在Win7系统下的偏移是0x268。
从windbg中验证确为上述值。
在Windows内核中有一个活动进程链表AcvtivePeorecssList。它是一个双向链表,保存着系统中所有进程的EPROCESS结构,记录了当前进程正在哪些处理器上运行,shellcode通过其来进行进程遍历。
获取lsass.exe
进程或者spoolsv.exe
。
经过测试发现如果注入lsass.exe
进程,在断开bind_tcp的shellcode连接时会导致系统重启,而在msf的ms17_010_eternalblue中只定向注入spoolsv.exe
进程退出后不会导致该问题。
在EPROCESS结构体中,UniqueProcessId与ActiveProcessLinks字段是相邻的,shellcode使用PsGetProcessId获取UniqueProcessId字段,在其字段后0xA的位置存储着它的偏移,获取UniqueProcessId的偏移后就能够得到ActiveProcessLinks字段的偏移。
进行动态调试,观察执行过程。
获取到EPROCESS.ActiveProcessLinks的偏移值是0xb8。
根据
获取EPROCESS.ActiveProcessLinks的偏移,通过其来遍历进程匹配进程名称来找到lsass.exe
或者spoolsv.exe
,因为注入lsass.exe
进程存在断开bindtcp shellcode连接后操作系统重启的问题,所以在调试过程中将lsass.exe
的hash改为了spoolsv.exe
的,不再遍历lsass.exe
进程。
最终遍历到进程spoolsv.exe
通过注释可以看到,作者描述说
寻找可以用于APC注入的线程,通常判断线程中是否可以被APC注入的方法是检查ETHREAD中的alertable字段。
因为每个Windows版本的ETHREAD中的alertable偏移量不同,所以这种方式不太可靠。
尝试逐个线程插入APC队列然后检查KAPC成员的方式更加可靠。
继续阅读shellcode中的注释,
在Ring0 shellcode中要用到CreateThread来启动Ring0的用户自定义的shellcode。
CreateThread函数需要使用非空的TEB.ActivationContextStackPointer,
如果TEB.ActivationContextStackPointer是NULL,被注入的进程将因为访问违规而崩溃。
(APC程序中不需要非空的TEB.ActivationContextStackPointer)
当TEB.ActivationContextStackPointer为NULL时,KTRHEAD.Queue总是为NULL。
精简掉注释后shellcode代码实现如下:
Win7下在KTHREAD结构体中Queue和Teb的位置。
查看执行结果:
可以看到其执行过后,遍历得到的线程的KTHREAD.queue不为0
在遍历得到可以被注入的线程后,使用KeInitializeApc
初始化KAPC结构体,使用KeInsertQueueApc
注入到目标线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
调试查看KeInitializeApc
这里context在通常的apc注入中这里是要执行的函数,而在这个shellcode里面作者使用了kernel_apc_routine这个参数放入了要执行的函数进行空间的分配和copy Ring3 shellcode,该函数一般用于销毁KAPC
可以看到context只是使用了栈底ebp的值,是无效的函数地址。
KernelApcRoutine的地址为ffdff3d2。
调用前的函数参数。
查看执行初始化前后KAPC值的变化。
初始化前:
经过初始化后:
调试查看KeInitializeApc
查看要注入的线程执行前的APC队列,这里我们主要查看线程的_KTHREAD.ApcState,通过其KAPC_STATE结构体就可以看到APC队列执行的情况。
在注入前,正在等待执行的用户APC为0。
查看执行后的APC队列,可以看到,正在等待的用户APC数量为1,并且在用户APC队列中看到了我们插入的KAPC结构体的地址。
在shellcode中也对KAPC.UserApcPending进行了判断,如果其值不为1,则清除线程的KAPC队列重新选择线程进行插入。
对于文中该处的shellcode,笔者有不同的想法,UserApcPending应该是KAPC_STATE的成员。
按照shellcode里写的逻辑,只能获取到KAPC.ApcListEntry中第一个成员 + 0xe偏移的值。
如果想要获取到当前线程中的KTHREAD.ApcState.UserApcPending
的值,要做如下修改(只针对于windows 7 x86)。
1 2 3 4 |
|
在二阶段shellcode执行KeInitializeApc之前,对KeInitializeApc函数中的kernel_apc_routine参数下断点。
在执行完二阶段shellcode后,系统调用被执行,触发KiServiceExit函数,该函数执行了KiDeliveApc函数(负责执行APC函数),通过KiDeliveApc我们的三阶段shellcode 被执行。
1 2 3 4 5 6 |
|
在函数开头,调整堆栈位置,保存当前eip到SystemArgument2函数中,去除掉栈中无用的PKAPC参数,保存寄存器环境,将取出的SystemArgument1和NormalRoutine参数压入栈中,其中SystemArgument1的位置用于存储寻找到的CreateThread API地址,最后调整ebp的位置。
存储调用前的返回地址到参数2的位置。
调整栈底指针ebp
调整完堆栈以后,调用ZwAllocateVirtualMemory分配一块Ring3的堆空间来将Ring0中存储的Ring3 用户自定义的shellcode拷贝到该空间中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
由于ZwAllocateVirtualMemory要求IRQL级别为PASSIVE_LEVEL,所以在调用函数前要对IRQL进行调整。
https://docs.microsoft.com/en-us/previous-versions/ff566416(v=vs.85)
在内核模式下FS[0]表示的KPCR结构体。
- 每个CPU都有一个KPCR结构体(一个内核一个)
- KPCR中存储了CPU本身要用的一些重要数据:GDT、IDT以及线程相关的一些信息
调整完后,接着构造ZwAllocateVirtualMemory函数的参数。
根据Protext参数的值可以看到,分配了一段可读可写可执行的空间。
可以看到BaseAddress中存储了分配的空间地址310000,成功分配地址。
接下来开始copy shellcode。
在windbg中调试 copy的过程。
在将存储在内核态的四阶段shellcode和用户自定义的shellcode拷贝到分配的空间中以后,开始获取CreateThread的API地址。
采取传统的通过获取PEB表,遍历InMemoryOrderModuleList获取kernel32.dll后遍历导出表的方式来获取。
使用PsGetProcessPeb获取PEB结构体。
根据PEB遍历Kernel32.dll
遍历导出表获取CreateThread API的地址。
储存API地址到参数SystemArgument1中。
对310000下执行断点,在三阶段shellcode执行完成后,g命令运行程序,就可以成功断下。
调用链如下:
在四阶段shellcode中,构造调用参数,使用CreateThread API来创建线程执行Ring3 用户自定义的shellcode。
获取压入前的返回地址
获取CreateThread API的地址。
获取用户自定义shellcode的地址。
查看四阶段shellcode构造的CreateThread调用参数。
可以看到lpStartAddr参数中记录了要执行的用户自定义的shellcode地址。
成功执行用户自定义的Ring3 shellcode代码。
至此,已经完成了整个漏洞利用成功后的Ring0 到Ring3 shellcode的引导过程。
https://www.cnblogs.com/onetrainee/p/11675225.html
https://www.microsoft.com/security/blog/2017/06/30/exploring-the-crypt-analysis-of-the-wannacrypt-ransomware-smb-exploit-propagation/
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-zwallocatevirtualmemory
滴水中级 APC注入
看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~
最后于 4天前 被flag0编辑 ,原因: