传送门
【漏洞分析】MS 17-010:NSA Eternalblue SMB 漏洞分析
1 环境
EXPLOIT:
Eternalromance-1.3.0
TARGET:
windows xp sp3
FILE:
srv.sys 5.1.2600.5512
2 Exploit使用
我们可以发现工具包中有两个Eternalromance, 一个1.4.0, 另外一个是1.3.0。经过我一翻折腾也没有把1.4.0跑起来。无奈试了下1.3.0发现竟然能成功运行。因此便有了这篇分析。大家可能都会用fuzzbunch这个命令行了。但是我们调试的时候总不能调一次都要重新输入TargetIp等那些配置吧。告诉大家一个省劲的方法。首先fuzzbunch按正常流程走一遍。在最后跑起exp的时候,在Log目录下会生成一个xml的配置文件。然后大家就可以用Eternalromance.1.3.0 –inconfig “xml路径” 来调用了。还有就是你也可以改exploit下的那个配置文件不过你得填非常多的参数。
3 基础知识
不想看的同学可以直接跳到 3.5 漏洞相关的重点那里
3.1 SMB Message structure
SMB Messages are divisible into three parts:
* fixed-length header
* variable length parameter block
* variable length data block
header的结构如下:
SMB_Header
{
UCHAR Protocol[4];
UCHAR Command;
SMB_ERROR Status;
UCHAR Flags;
USHORT Flags2;
USHORT PIDHigh;
UCHAR SecurityFeatures[8];
USHORT Reserved;
USHORT TID;
USHORT PIDLow;
USHORT UID;
USHORT MID;
}
更详细见 (https://msdn.microsoft.com/en-us/library/ee441702.aspx )
3.2 SMB_COM_TRANSACTION (0x25)
为事务处理协议的传输子协议服务。这些命令可以用于CIFS文件系统内部通信的邮箱和命名管道。如果出书的数据超过了会话建立时规定的MaxBufferSize,必须使用SMB_COM_TRANSACTION_SECONDARY命令来传输超出的部分:SMB_Data.Trans_Data和SMB_Data.Trans_Parameter。这两部分在初始化消息中没有固定。
如果客户端没有发送完所有的SMB_Data.Trans_Data,会将DataCount设置为小于TotalDataCount的一个值。同样的,如果SMB_Data.Trans_Parameters没有发送完,会设置ParameterCount为一个小于TotalParameterCount的值。参数部分优先级高于数据部分,客户端在每个消息中应该尽量多的发送数据。服务器应该可以接收无序到达的SMB_Data.Trans_Parameters 和 SMB_Data.Trans_Data,不论是大量还是少量的数据。
在请求和响应消息中,SMB_Data.Trans_Parameters和SMB_Data.Trans_Data的位置和长度都是由SMB_Parameters.ParameterOffset、SMB_Parameters.ParameterCount,SMB_Parameters.DataOffset和SMB_Parameters.DataCount决定。另外需要说明的是,SMB_Parameters.ParameterDisplacement和SMB_Parameters.DataDisplacement可以用来改变发送数据段的序号。服务器应该优先发送SMB_Data.Trans_Parameters。客户端应该准备好组装收到的SMB_Data.Trans_Parameters和SMB_Data.Trans_Data,即使它们是乱序到达的。
The PID, MID, TID, and UID MUST be the same for all requests and responses that are part of the same transaction.
更详细看 (https://msdn.microsoft.com/en-us/library/ee441489.aspx )
3.3 SMB_COM_TRANSACTION_SECONDARY(0x26)
此命令用来完成SMB_COM_TRANSACTION中未传输完毕数据的传输。在请求和响应消息中,SMB_Data.Trans_Parameters和SMB_Data.Trans_Data的位置和长度都是由SMB_Parameters.ParameterOffset、SMB_Parameters.ParameterCount,SMB_Parameters.DataOffset和SMB_Parameters.DataCount决定。另外需要说明的是,SMB_Parameters.ParameterDisplacement和SMB_Parameters.DataDisplacement可以用来改变发送数据段的序号。服务器应该优先发送SMB_Data.Trans_Parameters。客户端应该准备好组装收到的SMB_Data.Trans_Parameters和SMB_Data.Trans_Data,即使它们是乱序到达的。
更详细看 (https://msdn.microsoft.com/en-us/library/ee441949.aspx )
3.4 SMB_COM_WRITE_ANDX (0x2F)
结构如图:
更详细看 (https://msdn.microsoft.com/en-us/library/ee441848.aspx )
3.5 总结下漏洞相关的重点
客户端处理SMB_COM_TRANSACTION命令的时候如果数据大小超过MaxBufferSize,则需要使用SMB_COM_TRANSACTION_SECONDARY传输剩下的数据。
对于作为同一Transcation部分的所有请求和响应,PID,MID,TID和UID必须相同。
4 漏洞分析
4.1 SrvSmbTransactionSecondary
结合上一节的重点中提到的。
如果我们先发送了一个数据大小大于MaxBufferSize的SMB_COM_TRANSACTION数据包。那么接下来肯定是要发送SMB_COM_TRANSACTION_SECONDARY来传输剩余的数据,那么服务器在收到处理SMB_COM_TRANSACTION_SECONDARY这个命令的时候肯定会找他对应的那个Transcation。同时服务器也可能同时收到很多个包含SMB_COM_TRANSACTION_SECONDARY命令的数据包。怎么定位某个MB_COM_TRANSACTION_SECONDARY数据包对应的SMB_COM_TRANSACTION数据包呢?重点里也提到了。如果MB_COM_TRANSACTION_SECONDARY数据包中的PID,MID,TID和UID和SMB_COM_TRANSACTION数据包中的相同,那么就认为是同一部分的请求。
代码是这么实现的。
int __thiscall SrvSmbTransactionSecondary(int this)
{
int v1; // ebx@1
int pSmbParamter; // edi@1
TRANSCATION *pTransation; // eax@1 MAPDST
unsigned int ParameterDisplayment; // ecx@10 MAPDST
size_t ParameterSize; // eax@10 MAPDST
size_t DataSize; // edx@10 MAPDST
int pTransList; // [sp+Ch] [bp-24h]@1
PERESOURCE Resource; // [sp+18h] [bp-18h]@26
struct _ERESOURCE *Resourcea; // [sp+18h] [bp-18h]@30
int pSmbHeader; // [sp+1Ch] [bp-14h]@1
int ParameterOffset; // [sp+20h] [bp-10h]@10
int DataOffset; // [sp+28h] [bp-8h]@10
v1 = this;
pSmbParamter = *(_DWORD *)(this + 0x6C);
pSmbHeader = *(_DWORD *)(this + 0x68);
pTransList = *(_DWORD *)(this + 0x4C);
//
// 首先查找SMB_COM_TRANSACTION_SECONDARY对应的SMB_COM_TRANSACTION
//
pTransation = (TRANSCATION *)SrvFindTransaction(pTransList, pSmbHeader, 0);
if ( !pTransation )
{
return 2;
}
if ( !*(_BYTE *)(pTransation->field_10 + 0x98) )
{
ParameterDisplayment = *(_WORD *)(pSmbParamter + 9);
ParameterOffset = *(_WORD *)(pSmbParamter + 7);
ParameterSize = *(_WORD *)(pSmbParamter + 5);
DataOffset = *(_WORD *)(pSmbParamter + 0xD);
DataSize = *(_WORD *)(pSmbParamter + 0xB);
DataDisplayment = *(_WORD *)(pSmbParamter + 0xF);
if ( pTransation->field_93 )
{
//...
}
else
{
//Check
Resource = *(PERESOURCE *)(*(_DWORD *)(v1 + 0x60) + 0x10);
if ( ParameterSize + ParameterOffset > *(_DWORD *)(*(_DWORD *)(v1 + 0x60) + 0x10)
|| DataOffset + DataSize > (unsigned int)Resource
|| ParameterSize + ParameterDisplayment > pTransation->TotalParameterCount
|| DataSize + DataDisplayment > pTransation->TotalDataCount )
{
//CheckFaild
}
else
{
if ( pTransation->field_94 != 1 )
{
//
// 这里将SMB_COM_TRANSACTION_SECONDARY传过来的Parameter和Data都保存
// 到Transaction中
//
//拷贝Parameter Buffer
if ( ParameterSize )
_memmove(
(void *)(ParameterDisplayment + pTransation->ParameterBuffer),
(const void *)(pSmbHeader + ParameterOffset),
ParameterSize); // parameter
//复制Data Buffer,这里注意下,下面会提到!!
if ( DataSize )
_memmove(
(void *)(DataDisplayment + pTransation->DataBuffer),
(const void *)(pSmbHeader + DataOffset),
DataSize); // data
return 2;
}
}
}
}
return 1;
}
这个SMB_COM_TRANSACTION_SECONDARY命令的处理函数的大体流程就是首先查找SMB_COM_TRANSACTION_SECONDARY对应的SMB_COM_TRANSACTION如果找到就将SMB_COM_TRANSACTION_SECONDARY中的Parameter和Data都复制到SMB_COM_TRANSACTION中去。
4.2 SrvFindTransaction
下面来看下查找的逻辑SrvFindTransaction:
int __stdcall SrvFindTransaction(int a1, int pSmbHeaderOrMid, int a3)
{
SMB_HEADER *pSmbHeader_; // edi@1
struct _ERESOURCE *v4; // ebx@4
_DWORD **pTransList; // edx@4
_DWORD *i; // ecx@4
TRANSCATION *v7; // eax@5
int v9; // esi@14
pSmbHeader_ = (SMB_HEADER *)pSmbHeaderOrMid;
//
// command 0x2f is SUM_CMD_WRITE_ANDX
// a3 = SUM_CMD_WRITE_ANDX->Fid
//
if ( *(_BYTE *)(pSmbHeaderOrMid + 4) == 0x2F )
pSmbHeaderOrMid = a3;
else
LOWORD(pSmbHeaderOrMid) = *(_WORD *)(pSmbHeaderOrMid + 0x1E);
v4 = (struct _ERESOURCE *)(a1 + 0x130);
ExAcquireResourceExclusiveLite((PERESOURCE)(a1 + 0x130), 1u);
pTransList = (_DWORD **)(*(_DWORD *)(a1 + 0xF4) + 8);
for ( i = *pTransList; ; i = (_DWORD *)*i )
{
if ( i == pTransList )
{
//
// 查到最后了退出
//
ExReleaseResourceLite(v4);
return 0;
}
//
// 这里是对比TID,PID,UID,MID
// 这里注意这个MID,如果命令是SUM_CMD_WRITE_ANDX MID就是SUM_CMD_WRITE_ANDX MID数据包中的Fid
//
v7 = (TRANSCATION *)(i - 6);
if ( *((_WORD *)i + 47) == pSmbHeader_->TID
&& v7->ProcessId == pSmbHeader_->PIDLow
&& v7->UserId == pSmbHeader_->UID
&& v7->MutiplexId == (_WORD)pSmbHeaderOrMid )// MutilplexId如果名令时SMB_CMD_WRITE_ANDX那么这里是Fid
{
break;
}
}
if ( BYTE1(v7->field_0) == 2 )
{
_InterlockedExchangeAdd((volatile signed __int32 *)(v7->field_8 + 4), 1u);
v9 = (int)(i - 6);
}
else
{
v9 = 0;
}
ExReleaseResourceLite(v4);
return v9;
}
大家可以看下逻辑。重点在这里如果命令是SMB_CMD_WRITE_ANDX(0x2f)话那么MID的对比就不一样了,这里是用Transaction->MID和SMB_CMD_WRITE_ANDX->Fid比较。如果一样返回pTransaction。那么问题来了。如果服务器端正好有一个其他的Transaction->MID恰好和SMB_CMD_WRITE_ANDX->Fid的相等那么将会返回一个错误的pTransaction。很经典的类型混淆。
处理SUM_CMD_WRITE_ANDX的函数SrvSmbWriteAndX代码如下:
signed int __thiscall SrvSmbWriteAndX(int this)
{
...略
pTrans = (TRANSCATION *)SrvFindTransaction(pTransList, pSmbTranscationBuffer, Fid);
pTrans_ = pTrans;
if ( !pTrans )
{
if ( (unsigned int)SrvWmiEnableLevel >= 2 && SrvWmiEnableFlags & 1 && KeGetCurrentIrql() < 2u )
WPP_SF_(64, &unk_1E1CC);
v40 = "d:\xpsp\base\fs\srv\smbrdwrt.c";
v37 = 3216;
goto LABEL_69;
}
if ( pTrans->TotalDataCount - pTrans->nTransDataCounts < v11 )
{
SrvCloseTransaction(pTrans);
SrvDereferenceTransaction(pTrans_);
_SrvSetSmbError2(v1, 0x80000005, 0, 3238, (int)"d:\xpsp\base\fs\srv\smbrdwrt.c");
StatusCode = 0x80000005;
goto LABEL_100;
}
v31 = Size;
qmemcpy((void *)pTrans->DataBuffer, VirtualAddress, Size);
//
// !!!!这里将DataBuffer指针给增加了!!!!
//
pTrans_->DataBuffer += Size;
pTrans_->nTransDataCounts += v31;
...略
}
看到pTrans_->DataBuffer += Size这句相信大家就能明白了。这里将DataBuffer的指针增大了。再处理此Transcation的SMB_COM_TRANSACTION_SECONDARY命令的时候也就是SrvSmbTransactionSecondary中复制Data的memcpy可就越界了!!!!!
所以此漏洞可以总结成类型混淆造成的越界写。
4.3 Exploit数据包
通过对Exploit抓包我们可以看到其漏洞触发过程。
首先发送SMB_COM_TRANSACTION命令创建一个TID=2049 PID=0 UID=2049 MID=16385(0x4001)的Transcation:
然后发送SMB_CMD_WRITE_ANDX命令还增加pTrans_->DataBuffer这个指针:
5 漏洞利用
从上面描述可以看出,该漏洞为类型混淆导致的越界写漏洞。前期通过spray 使多个TRANSACTION相邻,然后让其中一个TRANSACTION触发该漏洞,再通过该TRANSACTION越界写其相邻的TRANSACTION
spary 最终 memory view:
spray的目的是构造出下出三个相邻的transaction, 其中write_transaction 主要用于写操作,leak_transaction 主要用于信息泄露,而control_transaction为触发漏洞的transaction,触发之后其他InData字段会增加0x200, 以致于可写的范围可以向后延伸0x200.利用于这点可以写与其相依的write_transaction的InData字段。从面达到任意地址写的效果。
注: 本次调试中 control_transaction地址为:0x58b90, write_transaction地址为: 0x59c38, leak_transaction地址为:0x5ace0
其中TRANSACTION 结构部份如下:
typedef struct _TRANSACTION {
//...
/*+0xc*/ LPVOID Connection;
//...
/*+0x34*/ LPWORD InSetup;
//...
/*+0x40*/ LPBYTE OutParameters;
/*+0x44*/ LPBYTE InData; /*写的目的地址*/
/*+0x48*/ LPBYTE OutData; /*读的目的地址*/
//...
/*+0x60*/ DWORD DataCount; /*控制可读的长度*/
/*+0x64*/ DWORD TotalDataCount
}TRANSACTION
写操作:
SMB_PROCESSOR_RETURN_TYPE SrvSmbTransactionSecondary (
SMB_PROCESSOR_PARAMETERS
)
{
//...
request = (PREQ_TRANSACTION_SECONDARY)WorkContext->RequestParameters;
header = WorkContext->RequestHeader;
transaction = SrvFindTransaction( connection, header, 0 );
//...
dataOffset = SmbGetUshort( &request->DataOffset );
dataCount = SmbGetUshort( &request->DataCount );
dataDisplacement = SmbGetUshort( &request->DataDisplacement );
//...
// Copy the parameter and data bytes that arrived in this SMB.
//...
if ( dataCount != 0 ) {
RtlMoveMemory(
transaction->InData + dataDisplacement,
(PCHAR)header + dataOffset,
dataCount);
}
//...
}
读操作:
VOID SrvCompleteExecuteTransaction (
IN OUT PWORK_CONTEXT WorkContext,
IN SMB_TRANS_STATUS ResultStatus)
{
//...
transaction = WorkContext->Parameters.Transaction;
header = WorkContext->ResponseHeader;
transactionCommand = (UCHAR)SmbGetUshort( &transaction->InSetup[0] );
//...
// Copy the appropriate parameter and data bytes into the message.
if ( paramLength != 0 ) {
RtlMoveMemory( paramPtr, transaction->OutParameters, paramLength );
}
if ( dataLength != 0 ) {
RtlMoveMemory( dataPtr, transaction->OutData, dataLength );
}
//...
}
从exp运行的log可以看出该漏洞利用分为两部份:信息泄露 与 代码执行
5.1 信息泄露
需要泄露的信息包括 Transaction2Dispatch Table基址 与 一块NonePagedPool Buffer地址. 通过修改Transaction2Dispatch Table中的函数指针来执行 shellcode, 其中NonePagedPool Buffer就是用于存放shellcode.
5.1.1 泄露Transaction2Dispatch Table基址
从exp运行的log可以看出首先泄露CONNECTION结构基址:CONNECTION地址存放于TRANSACTION->Connection字段。看到这,你可能已经想到该怎么做了:直接利用constrol transaction的0x200字节的越界能力修改write_transaction的DataCount字段让其可以越界读leak_transaction上的内容,从而读出TRANSACTION->Connection。 但exp作者却并没有这么做,这里并不打算深究其原因,或许是有其他限制,或许不是。
作者这里利用了另一种复杂不少方法,通过另一种方法修改 write_transaction的DataCount字段。
5.1.1.1 write_transaction初始状态如下:
可以看出CONNECTION地址为:89a29c18,OutData为0x5acd4 (== 59c38+0x109c)已经是write_transaction的末尾,所以其DataCount为0,表示不可读。
5.1.1.2 修改write_transaction->DataCount
首先 修改write_transaction的InSetup为0x23,这点通过control_transaction很容易完成。之后发包触发写write_transaction操作,会走到:
SMB_STATUS SRVFASTCALL ExecuteTransaction (
IN OUT PWORK_CONTEXT WorkContext)
{
//...
transaction = WorkContext->Parameters.Transaction;
//...
command = SmbGetUshort(&transaction->InSetup[0]);
//...
switch( command ) {
case TRANS_TRANSACT_NMPIPE:
resultStatus = SrvTransactNamedPipe( WorkContext );
break;
case TRANS_PEEK_NMPIPE: //0x23
resultStatus = SrvPeekNamedPipe( WorkContext );
break;
case TRANS_CALL_NMPIPE:
resultStatus = SrvCallNamedPipe( WorkContext );
break;
//...
}
//...
}
由于之前已经将write_transaction的InSetup修改为0x23, 所以会call SrvPeekNamedPipe。
# ChildEBP RetAddr
00 b21f8d44 b24cdcce srv!ExecuteTransaction+0x23b (FPO: [0,0,0])
01 b21f8d7c b248a836 srv!SrvSmbTransactionSecondary+0x2f1 (FPO: [Non-Fpo])
02 b21f8d88 b249ad98 srv!SrvProcessSmb+0xb7 (FPO: [0,0,0])
03 b21f8dac 805c7160 srv!WorkerThread+0x11e (FPO: [Non-Fpo])
04 b21f8ddc 80542dd2 nt!PspSystemThreadStartup+0x34 (FPO: [Non-Fpo])
05 00000000 00000000 nt!KiThreadStartup+0x16
SrvPeekNamedPipe() 调用IoCallDriver最终调到 RestartPeekNamedPipe()函数
VOID SRVFASTCALL RestartPeekNamedPipe (
IN OUT PWORK_CONTEXT WorkContext)
{
//...
//
// Success. Generate and send the response.
//
transaction = WorkContext->Parameters.Transaction;
pipePeekBuffer = (PFILE_PIPE_PEEK_BUFFER)transaction->OutParameters;
readDataAvailable = (USHORT)pipePeekBuffer->ReadDataAvailable;
messageLength = (USHORT)pipePeekBuffer->MessageLength;
namedPipeState = (USHORT)pipePeekBuffer->NamedPipeState;
//
// ... then copy them back in the new format.
//
respPeekNmPipe = (PRESP_PEEK_NMPIPE)pipePeekBuffer;
SmbPutAlignedUshort(
&respPeekNmPipe->ReadDataAvailable,
readDataAvailable
);
SmbPutAlignedUshort(
&respPeekNmPipe->MessageLength,
messageLength
);
SmbPutAlignedUshort(
&respPeekNmPipe->NamedPipeState,
namedPipeState
);
//
// Send the response. Set the output counts.
//
// NT return to us 4 ULONGS of parameter bytes, followed by data.
// We return to the client 6 parameter bytes.
//
transaction->SetupCount = 0;
transaction->ParameterCount = 6;
transaction->DataCount = WorkContext->Irp->IoStatus.Information - (4 * sizeof(ULONG));
//...
}
该函数最终会修改Transaction->DataCount 为 0x23c。
kd> ub
srv!RestartPeekNamedPipe+0x42:
b7700137 66895004 mov word ptr [eax+4],dx
b770013b 83614c00 and dword ptr [ecx+4Ch],0
b770013f c7415406000000 mov dword ptr [ecx+54h],6
b7700146 8b4678 mov eax,dword ptr [esi+78h]
b7700149 8b401c mov eax,dword ptr [eax+1Ch]
b770014c 83e810 sub eax,10h
b770014f 85ff test edi,edi
b7700151 894160 mov dword ptr [ecx+60h],eax
kd> ?ecx+0x60
Evaluate expression: 367768 = 00059c98
kd> r eax
eax=0000023c
kd> dd 00059c38
00059c38 109c020c 00000000 ffdff500 89a29c18
00059c48 e1ed9900 e15e2960 0005acf8 00058ba8
00059c58 00020000 00059cd0 00002307 00000001
00059c68 00000000 00059cd4 8993b3e5 00059cd4
00059c78 00059d14 ffdff500 0005acd4 00000000
00059c88 00000000 00000006 00000000 00000ff0
00059c98 0000023c 00000000 00000001 00000000
00059ca8 00000101 08000000 0800cee6 00000000
kd> k
# ChildEBP RetAddr
00 b7c0ed88 b76dad98 srv!RestartPeekNamedPipe+0x5f
01 b7c0edac 805c6160 srv!WorkerThread+0x11e
02 b7c0eddc 80541dd2 nt!PspSystemThreadStartup+0x34
03 00000000 00000000 nt!KiThreadStartup+0x16
至此,已经成功修改了write_transaction的DataCount值,之后便可以越界读出 leak_transacion->Connection值: 89a29c18。
5.1.1.3 SRV global data pointer
kd> dds 89a29c18
89a29c18 02580202
89a29c1c 0000001d
89a29c20 00000000
89a29c24 00000000
89a29c28 00000000
89a29c2c 00000000
89a29c30 00000000
89a29c34 00000000
89a29c38 00000000
89a29c3c b76d8bec srv!SrvGlobalSpinLocks+0x3c
89a29c40 899d0020
89a29c44 000005b3
89a29c48 8976c200
89a29c4c 00004000
89a29c50 10000100
89a29c54 00000000
89a29c58 8988e010
89a29c5c 89aedc30
89a29c60 89a7d898
89a29c64 0001ffff
其中srv!SrvGlobalSpinLocks+0x3c 就是所谓的 SRV global data pointer :b76d8bec
5.1.1.4 Locating function tables
kd> dds b76d8bec-0x654
b76d8598 b7709683 srv!SrvSmbOpen2
b76d859c b76f62a8 srv!SrvSmbFindFirst2
b76d85a0 b76f74e5 srv!SrvSmbFindNext2
b76d85a4 b76f6309 srv!SrvSmbQueryFsInformation
b76d85a8 b7707293 srv!SrvSmbSetFsInformation
b76d85ac b77041ad srv!SrvSmbQueryPathInformation
b76d85b0 b7703ce7 srv!SrvSmbSetPathInformation
b76d85b4 b77025ad srv!SrvSmbQueryFileInformation
b76d85b8 b770367f srv!SrvSmbSetFileInformation
b76d85bc b7705c85 srv!SrvSmbFsctl
b76d85c0 b7706419 srv!SrvSmbIoctl2
b76d85c4 b7705c85 srv!SrvSmbFsctl
b76d85c8 b7705c85 srv!SrvSmbFsctl
b76d85cc b77047bb srv!SrvSmbCreateDirectory2
b76d85d0 b7709a51 srv!SrvTransactionNotImplemented
b76d85d4 b7709a51 srv!SrvTransactionNotImplemented
b76d85d8 b76fb144 srv!SrvSmbGetDfsReferral
b76d85dc b76faf7e srv!SrvSmbReportDfsInconsistency
b76d85e0 00000000
5.1.2 泄露Npp Buffer (shellcode buffer)
这里又得回到ExecuteTransaction函数:
SMB_STATUS SRVFASTCALL ExecuteTransaction (
IN OUT PWORK_CONTEXT WorkContext)
{
//...
header = WorkContext->ResponseHeader;
response = (PRESP_TRANSACTION)WorkContext->ResponseParameters;
ntResponse = (PRESP_NT_TRANSACTION)WorkContext->ResponseParameters;
//
// Setup output pointers
//
if ( transaction->OutParameters == NULL ) {
//
// Parameters will go into the SMB buffer. Calculate the pointer
// then round it up to the next DWORD address.
//
transaction->OutParameters = (PCHAR)(transaction->OutSetup +
transaction->MaxSetupCount);
offset = (transaction->OutParameters - (PCHAR)header + 3) & ~3;
transaction->OutParameters = (PCHAR)header + offset;
}
if ( transaction->OutData == NULL ) {
//
// Data will go into the SMB buffer. Calculate the pointer
// then round it up to the next DWORD address.
//
transaction->OutData = transaction->OutParameters +
transaction->MaxParameterCount;
offset = (transaction->OutData - (PCHAR)header + 3) & ~3;
transaction->OutData = (PCHAR)header + offset;
}
//...
command = SmbGetUshort(&transaction->InSetup[0]);
//...
switch( command ) {
case TRANS_TRANSACT_NMPIPE:
resultStatus = SrvTransactNamedPipe( WorkContext );
break;
case TRANS_PEEK_NMPIPE:
resultStatus = SrvPeekNamedPipe( WorkContext );
break;
//...
}
这在这个函数里有这么一个逻辑,当transaction->OutParameters==NULL里,会将PWORK_CONTEXT->ResponseHeader加上一定的offset赋于它,PWORK_CONTEXT->ResponseHeader就是个NonePagedPool.
kd> ub eip
srv!ExecuteTransaction+0x60:
b76e8d05 46 inc esi
b76e8d06 50 push eax
b76e8d07 d1e0 shl eax,1
b76e8d09 2bc2 sub eax,edx
b76e8d0b 8d440803 lea eax,[eax+ecx+3]
b76e8d0f 83e0fc and eax,0FFFFFFFCh
b76e8d12 03c2 add eax,edx
b76e8d14 894640 mov dword ptr [esi+40h],eax
kd> dd 5ace0
0005ace0 109c020c 00000000 89b2c948 89a29c18
0005acf0 e1ed9900 e15e2960 0005bda0 00059c50
0005ad00 00020000 0005ad78 00002361 40010036
0005ad10 00000000 0005ad0c 8993fa25 0005ad7c
0005ad20 8993fa28 0005ad7c b76d84ec 00000004
0005ad30 00000000 00000000 00000000 00000010
0005ad40 00000000 00000000 00000100 00000000
0005ad50 00000101 08000000 0800cee6 0000004b
kd> ? esi+0x40
Evaluate expression: 372000 = 0005ad20
kd> r eax
eax=8993fa28
5.1.2.1 transaction->OutParameters=NULL
Transaction 初始状态下OutParameters并不为NULL:
kd> dd 5ace0
0005ace0 109c020c 00000000 89b2c948 89a29c18
0005acf0 e1ed9900 e15e2960 0005bda0 00059c50
0005ad00 00020000 0005ad78 00002361 00000001
0005ad10 00000000 0005ad7c 00000000 0005ad7c
0005ad20 0005adbc 0005ad7c 0005bd7c 00000000
0005ad30 00000000 00000000 00000000 00000fc0
0005ad40 00000000 00000040 00000000 00000000
0005ad50 00000101 08000000 0800cee6 0000004b
这里通过write_transaction越界 写 leak_transaction->OutParameters为NULL, 然后发包触发写leak_transaction操作,之后leak_transaction->OutParameters便为一 Npp Buffer值了。
5.1.2.2 leak_transaction->OutData = &leak_transaction->OutParameters
这里要事先泄露leak_transaction的基址,其实也不难,通过读leak_transaction的OutData 或 OutParameters 或 InData 字段的值再减去一定的偏移便得到了基址,使leak_transaction->OutData = &leak_transaction->OutParameters之后,发包触发leak_transaction读操作便将该Npp buffer地址泄露出来了。
5.1.2.3 写shellcode到Npp Buffer
将control_transaction->OutData设为Npp Buffer+0x100地址,然后发包发送shellcode,便将shellcode写到了Npp Buffer+0x100内。
5.2 代码执行
至此,直接将Npp buffer+0x100写到之前泄露出来的函数表里
kd> dds b76d8598
b76d8598 b7709683 srv!SrvSmbOpen2
b76d859c b76f62a8 srv!SrvSmbFindFirst2
b76d85a0 b76f74e5 srv!SrvSmbFindNext2
b76d85a4 b76f6309 srv!SrvSmbQueryFsInformation
b76d85a8 b7707293 srv!SrvSmbSetFsInformation
b76d85ac b77041ad srv!SrvSmbQueryPathInformation
b76d85b0 b7703ce7 srv!SrvSmbSetPathInformation
b76d85b4 b77025ad srv!SrvSmbQueryFileInformation
b76d85b8 b770367f srv!SrvSmbSetFileInformation
b76d85bc b7705c85 srv!SrvSmbFsctl
b76d85c0 b7706419 srv!SrvSmbIoctl2
b76d85c4 b7705c85 srv!SrvSmbFsctl
b76d85c8 b7705c85 srv!SrvSmbFsctl
b76d85cc b77047bb srv!SrvSmbCreateDirectory2
b76d85d0 8993fb28
b76d85d4 b7709a51 srv!SrvTransactionNotImplemented
b76d85d8 b76fb144 srv!SrvSmbGetDfsReferral
b76d85dc b76faf7e srv!SrvSmbReportDfsInconsistency
b76d85e0 00000000
之后发包就能触发该函数调用:
kd> k
# ChildEBP RetAddr
WARNING: Frame IP not in any known module. Following frames may be wrong.
00 b72b4cf0 b76e8d76 0x8993fb28
01 b72b4d04 b76e341f srv!ExecuteTransaction+0xdb
02 b72b4d7c b76ca836 srv!SrvSmbTransaction+0x7ac
03 b72b4d88 b76dad98 srv!SrvProcessSmb+0xb7
04 b72b4dac 805c6160 srv!WorkerThread+0x11e
05 b72b4ddc 80541dd2 nt!PspSystemThreadStartup+0x34
06 00000000 00000000 nt!KiThreadStartup+0x16
6 关于补丁
了解了漏洞原理之后修补都很简单了。只要在srv!SrvFindTransaction里面判断一下SMB COMMAND的类型是否一致就好了。
修补前
TRANSCATION *__fastcall SrvFindTransaction(int pConnect, SMB_HEADER *Fid, __int16 a3)
{
_DWORD **pTransList; // eax@4
_DWORD *v6; // ebx@4
PDEVICE_OBJECT v7; // ecx@5
TRANSCATION *pTransaction; // esi@6
char Command_Trans; // al@10
char Command_header; // dl@10
__int16 MIDorFID; // [sp+Ch] [bp-Ch]@2
struct _ERESOURCE *Resource; // [sp+14h] [bp-4h]@4
if ( Fid->Command == 0x2F )
MIDorFID = a3;
else
MIDorFID = Fid->MID;
Resource = (struct _ERESOURCE *)(pConnect + 0x19C);
ExAcquireResourceExclusiveLite((PERESOURCE)(pConnect + 0x19C), 1u);
pTransList = (_DWORD **)(*(_DWORD *)(pConnect + 0x160) + 8);
v6 = *pTransList;
if ( *pTransList == pTransList )
goto LABEL_14;
v7 = WPP_GLOBAL_Control;
while ( 1 )
{
pTransaction = (TRANSCATION *)(v6 - 6);
if ( *((_WORD *)v6 + 49) == Fid->TID
&& pTransaction->PID == Fid->PID
&& pTransaction->UID == Fid->UID
&& pTransaction->MID == MIDorFID )
{
break;
}
LABEL_13:
v6 = (_DWORD *)*v6;
if ( v6 == (_DWORD *)(*(_DWORD *)(pConnect + 0x160) + 8) )
goto LABEL_14;
}
//
// 这里添加了对COMMAND的比较。比较pTransaction和请求中SMB_HEADER中的COMMAND进行对比
//
Command_Trans = pTransaction->Command;
Command_header = Fid->Command;
if ( Command_Trans != Command_header )
{
if ( (PDEVICE_OBJECT *)v7 != &WPP_GLOBAL_Control )
{
WPP_SF_qDD(v7->AttachedDevice, v7->CurrentIrp, (_BYTE)v6 - 24, Command_header, Command_Trans);
v7 = WPP_GLOBAL_Control;
}
goto LABEL_13;
}
if ( BYTE1(pTransaction->field_0) == 2 )
{
_InterlockedIncrement((volatile signed __int32 *)(pTransaction->field_8 + 4));
goto LABEL_15;
}
LABEL_14:
pTransaction = 0;
LABEL_15:
ExReleaseResourceLite(Resource);
return pTransaction;
}
补丁点就是if ( Command_Trans != Command_header )看注释的地方。
7 总结
总之,这个漏洞还是非常好的,远程任意地址写,还可以信息泄露。威力很大。
8 联系作者
如果你感觉排版不舒服可以去这里下载原始的markdown文件。
传送门
【漏洞分析】MS 17-010:NSA Eternalblue SMB 漏洞分析