[原创] 经典重现:永恒之蓝内核态Shellcode分析
2022-9-23 04:9:21 Author: bbs.pediy.com(查看原文) 阅读量:37 收藏

目录

在现有公开的永恒之蓝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 =\

        b"\x60\xE8\x00\x00\x00\x00\x5B\xE8\x23\x00\x00\x00\xB9\x76\x01\x00"\

        b"\x00\x0F\x32\x8D\x7B\x39\x39\xF8\x74\x11\x39\x45\x00\x74\x06\x89"\

        b"\x45\x00\x89\x55\x08\x89\xF8\x31\xD2\x61\xC2\x24\x00\x8D\xAB\x00" \

        b"\x10\x00\x00\xC1\xED\x0C\xC1\xE5\x0C\x83\xED\x50\xC3"

可以判定导致不稳定重启的原因存在于shellcode中,针对shellcode进行优化处理,有可能可以解决该问题。

可以搜集到的关于exp中Ring0 shellcode的分析资料很少,想解决永恒之蓝exp的概率性重启问题,要针对shellcode进行优化,就必须了解exp中的Ring0 shellcode中的执行细节,所以就有了这篇文章的产生。

根据执行过程中所作事情的不同,将整个执行流程分为了四个阶段

  1. 一阶段shellcode:操作MSR模式寄存器将二阶段的shellcode作为钩子函数来对sysenter或syscall下钩子。
    • X86 覆盖IA32_SYSENTER_EIP来hook SYSENTER。
    • X64 覆盖IA32_LSTAR MSR 来hook SYSCALL。
  2. 二阶段shellcode:触发系统调用后还原syscall(sysenter),遍历进程寻找lsass.exe或者spoolsv.exe进程,使用异步过程调用 (APC)将用户级 shellcode 注入进程中。

  3. 三阶段shellcode:执行KernelApcRoutine(该回调函数为KeInitializeApc的KernelRoutiune参数,原本用于销毁KAPC的函数地址,而现在用于执行自定义功能)。

    • KernelApcRoutine:分配内存将存在于内核态的shellcode拷贝到用户态,遍历kernel32.dll中的CreateThread中的api地址用于存储。
  4. 四阶段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用于将二阶段的shellcode安装为sysenter或者syscall的钩子。

在x86下是sysenter,在x64下是syscall。

MSR

MSR是模式指定寄存器,用于设置CPU 的工作环境和标示CPU 的工作状态,包括温度控制,性能监控等。

rdmsr:读取ECX中的MSR寄存器地址的值,低32位存储到EAX中,高32位存储到EDX中。

1644456731132

wrmsr:向ECX中存储的MSR寄存器地址写入EAX:ECX的值,其中低32位存储在EAX中,高32位存储在EDX中。

1644456930667

从inter CPU手册中可以看到,X86架构下 MSR寄存器 在0x176的地址存储了 我们关注的SYSENTER_EIP_MSR值,通过对其修改可以达到hook sysenter的目的,在X64下其值为0xC0000082。

1644456184254

Ring0 shellcode通过这种方式将X86下的sysenter的地址替换为了自定义的函数地址,实现了钩子的安装。

1644634732915

其中的set_ebp_data_address_fn函数,用于调整ebp栈底空间,在HAL heap中寻找了一块可用的空间来存储临时变量和作用后续的KAPC结构体的空间使用。

1644464548444

X86 sysenter hook:

1644458796043

x64 syscall hook:

1644463104830

二阶段shellcode

在发生系统调用时,syscall_hook 函数的代码会被执行。

所以对syscall_hook函数下断点,等待一阶段shellcode执行完成后,发生系统调用时该函数就会被执行到。

1644910049384

syscall_hook

syscall_hook函数用于执行原有sysenter/syscall中的初始化代码,限制APC排队数量为1,恢复原有系统调用,函数内部调用了r3_to_r0_start进行了后续的APC注入的操作。

系统调用初始化

在二阶段shellcode开头的部分使用了sysenter/syscall中的前几行指令,用于执行原始系统调用。

X86架构下:

1644474741005

1644475250722

X64架构下:

1644474872533

1644474317223

限制APC排队数量

1644634472286

cmpxchg为比较交换指令,操作数1与eax做比较:

1

2

xor eax,eax

cmpxchg 操作数1,操作数2

如果相等,则 操作数1 = 操作数2,ZF = 1。

如果不相等,则 eax = 操作数1,ZF = 0。

1644479255926

shellcode中的cmpxchg运算完成后,将ebp + 8,即ffdfffb8的值置1

恢复原有系统调用

1644480043793

去掉hook,还原回原地址,原有的sysenter跳转到的eip为nt!KiFastCallEntry

1644480270173

在执行完成syscall_hook函数后会自动跳转到KiFastCallEntry/KiSystemCall64 还未执行代码的位置。

1644475883516

r3_to_r0_start

遍历内核首地址

KiFastCallEnter函数属于内核中的导出函数,通过其可以定位到内核的首地址。

1644480917769

shellcode中从KiFastCallEnter所属的页为起始位置,按页为单位向前遍历,直到遍历到页开头为MZ标记为止。

1644481410753

遍历指定进程

在遍历到内核首地址后,执行PsGetCurrentProcess获取进程的EPROCESS结构体,通过使用PsGetProcessImageFileName获取ImageFileName在EPROCESS中的偏移地址,来判断操作系统版本从而推算出EPROCESS.ThreadListHead的位置。

1644484690301

win_api_direct函数用于根据eax寄存器转入的API hash来寻找api的地址并执行。

1644485579352

动态分析执行流程

执行PsGetCurrentProcess获取进程的EPROCESS结构体。

1644486028292

执行PsGetProcessImageFileName获取EPROCESS.ImageFilename的地址,用它减去EPROCESS的首地址,得到偏移。

1644486658257

通过ImageFilename在EPROCESS中的偏移判断操作系统版本,从而推算出EPROCESS.ThreadListHead的偏移地址。

1644543829518

验证一下,win7系统下ThreadListHead在EPROCESS中的偏移确实是0x188

1644544085100

通过遍历EPROCESS.ThreadListHead的成员减去KPRC.KPRCB.CurrentThread(当前线程首地址)的方式来获取ThreadListEntry相对于ETHREAD结构体的偏移。

1644546234613

最后得到ThreadListEntry相对于ETHREAD结构体在Win7系统下的偏移是0x268。

1644546959762

从windbg中验证确为上述值。

1644547055457

在Windows内核中有一个活动进程链表AcvtivePeorecssList。它是一个双向链表,保存着系统中所有进程的EPROCESS结构,记录了当前进程正在哪些处理器上运行,shellcode通过其来进行进程遍历。

获取lsass.exe进程或者spoolsv.exe

经过测试发现如果注入lsass.exe进程,在断开bind_tcp的shellcode连接时会导致系统重启,而在msf的ms17_010_eternalblue中只定向注入spoolsv.exe进程退出后不会导致该问题。

1644551620111

在EPROCESS结构体中,UniqueProcessId与ActiveProcessLinks字段是相邻的,shellcode使用PsGetProcessId获取UniqueProcessId字段,在其字段后0xA的位置存储着它的偏移,获取UniqueProcessId的偏移后就能够得到ActiveProcessLinks字段的偏移。

1644550070105

进行动态调试,观察执行过程。

1644550786213

获取到EPROCESS.ActiveProcessLinks的偏移值是0xb8。

1644550798022

根据

获取EPROCESS.ActiveProcessLinks的偏移,通过其来遍历进程匹配进程名称来找到lsass.exe或者spoolsv.exe,因为注入lsass.exe进程存在断开bindtcp shellcode连接后操作系统重启的问题,所以在调试过程中将lsass.exe的hash改为了spoolsv.exe的,不再遍历lsass.exe进程。

1644560467682

1644560533824

最终遍历到进程spoolsv.exe

1644560560080

遍历可用于注入的线程

通过注释可以看到,作者描述说

寻找可以用于APC注入的线程,通常判断线程中是否可以被APC注入的方法是检查ETHREAD中的alertable字段。

因为每个Windows版本的ETHREAD中的alertable偏移量不同,所以这种方式不太可靠。

尝试逐个线程插入APC队列然后检查KAPC成员的方式更加可靠。

1644567631775

继续阅读shellcode中的注释,

在Ring0 shellcode中要用到CreateThread来启动Ring0的用户自定义的shellcode。

CreateThread函数需要使用非空的TEB.ActivationContextStackPointer,

如果TEB.ActivationContextStackPointer是NULL,被注入的进程将因为访问违规而崩溃。

(APC程序中不需要非空的TEB.ActivationContextStackPointer)

当TEB.ActivationContextStackPointer为NULL时,KTRHEAD.Queue总是为NULL。

精简掉注释后shellcode代码实现如下:

1644569666448

Win7下在KTHREAD结构体中Queue和Teb的位置。

1644568718181

查看执行结果:

1644570560440

可以看到其执行过后,遍历得到的线程的KTHREAD.queue不为0

1644570709410

1644570740436

APC注入

在遍历得到可以被注入的线程后,使用KeInitializeApc初始化KAPC结构体,使用KeInsertQueueApc注入到目标线程。

1

2

3

4

5

6

7

8

9

10

11

12

13

KeInitializeApc(PKAPC,//KAPC指针

                PKTHREAD,//目标线程

                KAPC_ENVIRONMENT = OriginalApcEnvironment (0),//0 1 2 3 四种状态

                PKKERNEL_ROUTINE = kernel_apc_routine,//销毁KAPC的函数地址

                PKRUNDOWN_ROUTINE = NULL,

                PKNORMAL_ROUTINE = userland_shellcode,//用户APC总入口或者内核APC函数

                KPROCESSOR_MODE = UserMode (1),//要查看的用户apc队列还是内核APC队列

                PVOID Context);//内核APC队列;NULL 用户APC:真正的APC函数

BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);

  //SystemArgument1 is second argument in usermode code

  //SystemArgument1是usermode代码中的第二个参数

  //SystemArgument2 是用户模式代码的第三个参数

1644572900451

调试查看KeInitializeApc

1644632102731

这里context在通常的apc注入中这里是要执行的函数,而在这个shellcode里面作者使用了kernel_apc_routine这个参数放入了要执行的函数进行空间的分配和copy Ring3 shellcode,该函数一般用于销毁KAPC

可以看到context只是使用了栈底ebp的值,是无效的函数地址。

1644573650863

KernelApcRoutine的地址为ffdff3d2。

1644632561116

调用前的函数参数。

1644827351349

查看执行初始化前后KAPC值的变化。

初始化前:

1644632773006

经过初始化后:

1644632811001

调试查看KeInitializeApc

查看要注入的线程执行前的APC队列,这里我们主要查看线程的_KTHREAD.ApcState,通过其KAPC_STATE结构体就可以看到APC队列执行的情况。

1644633646113

1644633322984

1644633344263

在注入前,正在等待执行的用户APC为0。

1644633440179

查看执行后的APC队列,可以看到,正在等待的用户APC数量为1,并且在用户APC队列中看到了我们插入的KAPC结构体的地址。

1644633541132

在shellcode中也对KAPC.UserApcPending进行了判断,如果其值不为1,则清除线程的KAPC队列重新选择线程进行插入。

1644807819959

对于文中该处的shellcode,笔者有不同的想法,UserApcPending应该是KAPC_STATE的成员。

1644807929252

按照shellcode里写的逻辑,只能获取到KAPC.ApcListEntry中第一个成员 + 0xe偏移的值。

1644808240902

1644808298404

如果想要获取到当前线程中的KTHREAD.ApcState.UserApcPending的值,要做如下修改(只针对于windows 7 x86)。

1

2

3

4

lea eax,dword ptr ds:[ebp + 0x10];get KAPC

mov eax, [eax + 0x8];get KAPC.Thread

mov eax, [eax + 0x40];get KTHREAD.ApcState

mov eax, byte ptr [eax + 0x016];get KTHREAD.ApcState.UserApcPending

三阶段shellcode

在二阶段shellcode执行KeInitializeApc之前,对KeInitializeApc函数中的kernel_apc_routine参数下断点。

1644910877846

在执行完二阶段shellcode后,系统调用被执行,触发KiServiceExit函数,该函数执行了KiDeliveApc函数(负责执行APC函数),通过KiDeliveApc我们的三阶段shellcode 被执行。

1644825780608

1

2

3

4

5

6

; VOID KernelApcRoutine(

;           IN PKAPC Apc,

;           IN PKNORMAL_ROUTINE *NormalRoutine,

;           IN PVOID *NormalContext,

;           IN PVOID *SystemArgument1,

;           IN PVOID *SystemArgument2)

调整堆栈

在函数开头,调整堆栈位置,保存当前eip到SystemArgument2函数中,去除掉栈中无用的PKAPC参数,保存寄存器环境,将取出的SystemArgument1和NormalRoutine参数压入栈中,其中SystemArgument1的位置用于存储寻找到的CreateThread API地址,最后调整ebp的位置。

1644823039132

存储调用前的返回地址到参数2的位置。

1644826261945

调整栈底指针ebp

1644827922806

分配内存拷贝Ring3 shellcode

调整完堆栈以后,调用ZwAllocateVirtualMemory分配一块Ring3的堆空间来将Ring0中存储的Ring3 用户自定义的shellcode拷贝到该空间中。

1

2

3

4

5

6

7

8

9

10

11

12

13

NTSTATUS ZwAllocateVirtualMemory(

  _In_    HANDLE    ProcessHandle,//当前进程的句柄

  _Inout_ PVOID     *BaseAddress,

    //该变量将接收分配的页面区域的基地址。如果此参数的初始值为非NULL,则从指定的虚拟地址开始分配区域,

    //向下舍入到下一个主机页面大小地址边界。如果此参数的初始值为NULL,操作系统将确定分配区域的位置。

  _In_    ULONG_PTR ZeroBits,//

    //此值必须小于 21,并且仅在操作系统确定分配区域的位置时使用,例如BaseAddress为NULL时。

  _Inout_ PSIZE_T   RegionSize,//

    //指向变量的指针,该变量将接收分配的页面区域的实际大小(以字节为单位)。

    //此参数的初始值指定区域的大小(以字节为单位),并向上舍入到下一个主机页面大小边界。

  _In_    ULONG     AllocationType,//指定要执行的分配类型的标志。

  _In_    ULONG     Protect//指定权限

);

1644828418022

由于ZwAllocateVirtualMemory要求IRQL级别为PASSIVE_LEVEL,所以在调用函数前要对IRQL进行调整。

1644828915196

https://docs.microsoft.com/en-us/previous-versions/ff566416(v=vs.85)

1644829212066

在内核模式下FS[0]表示的KPCR结构体。

  1. 每个CPU都有一个KPCR结构体(一个内核一个)
  2. KPCR中存储了CPU本身要用的一些重要数据:GDT、IDT以及线程相关的一些信息

1644829416491

调整完后,接着构造ZwAllocateVirtualMemory函数的参数。

1644831414097

根据Protext参数的值可以看到,分配了一段可读可写可执行的空间。

1644832196872

可以看到BaseAddress中存储了分配的空间地址310000,成功分配地址。

1644831663296

接下来开始copy shellcode。

1644832646216

在windbg中调试 copy的过程。

1644889062769

获取CreateThread API地址

在将存储在内核态的四阶段shellcode和用户自定义的shellcode拷贝到分配的空间中以后,开始获取CreateThread的API地址。

采取传统的通过获取PEB表,遍历InMemoryOrderModuleList获取kernel32.dll后遍历导出表的方式来获取。

1644890566995

使用PsGetProcessPeb获取PEB结构体。

1644891068738

根据PEB遍历Kernel32.dll

1644892269007

1644892321475

遍历导出表获取CreateThread API的地址。

1644894155185

储存API地址到参数SystemArgument1中。

1644894474295

四阶段shellcode

对310000下执行断点,在三阶段shellcode执行完成后,g命令运行程序,就可以成功断下。

1644894555636

调用链如下:

1644895528227

在四阶段shellcode中,构造调用参数,使用CreateThread API来创建线程执行Ring3 用户自定义的shellcode。

1644894947486

获取压入前的返回地址

1644896061792

获取CreateThread API的地址。

1644896200699

获取用户自定义shellcode的地址。

1644896337515

查看四阶段shellcode构造的CreateThread调用参数。

1644905539547

可以看到lpStartAddr参数中记录了要执行的用户自定义的shellcode地址。

1644905726329

成功执行用户自定义的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编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-274500.htm
如有侵权请联系:admin#unsafe.sh