Introduction
Recently we announced a series of blog posts dissecting the exploits released by the ShadowBrokers in April 2017; specifically some of the less explored exploits. This week we are going to take a look at Eternal Synergy, an SMBv1 authenticated exploit. This one is particularly interesting because many of the exploitation steps are purely packet-based, as opposed to local shellcode execution. Like the other SMB vulnerabilities, this one was also addressed in MS17-010 as CVE-2017-0143. The exploit works up to Windows 8, but does not work as written against any newer platforms.
This post has four main parts. We will deep-dive into the vulnerability, followed by a discussion of how the vulnerability was weaponized to create Read/Write/eXecute primitives that are used as building blocks throughout the exploit. We will then next walk through the execution of EternalSynergy and see how these primitives were used to deliver a full exploit. Finally, we will briefly discuss the effect of recent mitigations on the presented exploit techniques.
The Vulnerability: CVE-2017-0143
The root cause of this vulnerability stems from not taking the command type of an SMB message into account when determining if the message is part of a transaction. In other words, as long as the SMB header UID
, PID
, TID
and OtherInfo
fields match the corresponding transaction fields, the message would be considered to be part of that transaction.
Usually, the OtherInfo
field stores a MID. In the case of SMB_COM_WRITE_ANDX
messages, however, it stores a FID instead. This creates a potential message type confusion: Given an existing SMB_COM_WRITE_ANDX
transaction, an incoming SMB message with MID equal to the transaction FID would be included in the transaction.
PTRANSACTION
SrvFindTransaction (
IN PCONNECTION Connection,
IN PSMB_HEADER Header,
IN USHORT Fid OPTIONAL
)
{
...
//
// If this is a multipiece transaction SMB, the MIDs of all the pieces
// must match. If it is a multipiece WriteAndX protocol the pieces
// using the FID.
//
if (Header->Command == SMB_COM_WRITE_ANDX) {
targetOtherInfo = Fid;
} else {
targetOtherInfo = SmbGetAlignedUshort( &Header->Mid );
}
...
//
// Walk the transaction list, looking for one with the same
// identity as the new transaction.
//
for ( listEntry = Connection->PagedConnection->TransactionList.Flink;
listEntry != &Connection->PagedConnection->TransactionList;
listEntry = listEntry->Flink ) {
thisTransaction = CONTAINING_RECORD(
listEntry,
TRANSACTION,
ConnectionListEntry
);
if ( ( thisTransaction->Tid == SmbGetAlignedUshort( &Header->Tid ) ) &&
( thisTransaction->Pid == SmbGetAlignedUshort( &Header->Pid ) ) &&
( thisTransaction->Uid == SmbGetAlignedUshort( &Header->Uid ) ) &&
( thisTransaction->OtherInfo == targetOtherInfo ) ) {
...
// A transaction with the same identity has been found
...
}
...
}
Exploitation
When a SMB message arrives, the appropriate handler will copy its contents into the corresponding transaction buffer, namely InData
. The SMB_COM_TRANSACTION_SECONDARY
handler assumes that the InData
address points to the start of the buffer.
if ( dataCount != 0 ) {
RtlMoveMemory(
transaction->InData + dataDisplacement,
(PCHAR)header + dataOffset,
dataCount
);
}
However, in the case of a SMB_COM_WRITE_ANDX
transaction, each time a SMB is received for that transaction, the InData
address is updated to point to the end of the existing data.
RtlCopyMemory(transaction->InData, writeAddress, writeLength );
//
// Update the transaction data pointer to where the next
// WriteAndX data buffer will go.
//
transaction->InData += writeLength;
Leveraging the packet confusion, an attacker can insert a SMB_COM_TRANSACTION_SECONDARY
message into a SMB_COM_WRITE_ANDX
transaction. In that case, the InData
will point past the start of the buffer, and so the SMB_COM_TRANSACTION_SECONDARY
handler can overflow the buffer during copying the incoming message data.
Taking Over a Transaction
The building block for the RWX primitives used in the exploit takes over a transaction structure by exploiting the message confusion described in the previous section. First, a ‘control’ transaction is allocated via a SMB_COM_TRANSACTION
client message.
kd> dt srv!TRANSACTION 0xfffff8a00167f010
...
+0x080 InData : 0xfffff8a0`0167f110
+0x088 OutData : (null)
...
+0x0a4 DataCount : 0x0
+0x0a8 TotalDataCount : 0x5100
...
+0x0ba Tid : 0x800
+0x0bc Pid : 0xab9e
+0x0be Uid : 0x800
+0x0c0 OtherInfo : 0x4000
Then, an SMB_COM_WRITE_ANDX
message is sent, crafted to exploit the packet confusion. As a result, the InData
pointer of the control transaction is corrupted to point past the start of the buffer. In this case it is off by 0x200
. bytes.
kd> dt srv!TRANSACTION 0xfffff8a00167f010
...
+0x080 InData : 0xfffff8a0`0167f310
+0x088 OutData : (null)
...
+0x0a4 DataCount : 0x200
+0x0a8 TotalDataCount : 0x5100
...
+0x0ba Tid : 0x800
+0x0bc Pid : 0xab9e
+0x0be Uid : 0x800
+0x0c0 OtherInfo : 0x4000
Next, an SMB_COM_TRANSACTION_SECONDARY
message is sent to the same transaction, and by leveraging the corrupted InData
pointer it modifies a neighboring, ‘victim’ transaction. We revisit below how the target write address is computed.
if ( dataCount != 0 ) {
RtlMoveMemory(
transaction->InData + dataDisplacement,
(PCHAR)header + dataOffset,
dataCount
);
}
The incoming message dataDisplacement
is large enough to reach the neighboring transaction.
kd> dv dataDisplacement
dataDisplacement = 0x5020
Specifically, it will overwrite a transaction’s OtherInfo
field with an attacker-controlled value (in this case 0), so that all future messages sent using MID=0
will be directed to the victim transaction. Below we see the victim transaction just before the overwrite happens.
kd> dt srv!TRANSACTION 0xfffff8a00167f310+0x5020-0xc0
...
+0x080 InData : 0xfffff8a0`0168436c
+0x088 OutData : 0xfffff8a0`01684ffc
...
+0x0ba Tid : 0x800
+0x0bc Pid : 0xab9f
+0x0be Uid : 0x800
+0x0c0 OtherInfo : 0x8ccb
After taking over a victim transaction, the exploit can predictably continue corrupting fields within the same or other transactions, and reliably trigger them by sending a message to the transaction. Note that for this technique to work, the attacker must be able to predictably allocate a pair of neighboring transactions.
Remote Arbitrary Write Primitive
The Arbitrary Write Primitive allows the attacker to modify memory contents on the victim system, and serves as the foundation for the rest of the techniques used in this exploit. To corrupt memory, it leverages the technique described in the previous section. Specifically, the Write Primitive is constructed in two steps:
In Step #1 , the victim transaction InData buffer address is overwritten such that it points to the target write address.
Next in Step #2 , the attacker can overwrite arbitrary kernel memory by sending a to the victim transaction. Upon receiving the message, its contents will be copied to the InData
buffer; however, in our case the buffer address was corrupted and so the contents are copied to the attacker-controlled address. Below is an example packet, where the shellcode contained in the ‘Extra byte parameters’ will be copied over to the victim machine.
Remote Arbitrary Read Primitive
The Arbitrary Read Primitive allows the attacker to remotely read the contents of any memory address from the target system. To use this primitive, the attacker must have successfully:
- Taken over a connection, and established the write primitive, as explained above.
- Leaked a valid
TRANSACTION
structure
As seen in Figure 6, we have a control transaction adjacent to a Victim#1 transaction for the write primitive and a Victim#2 transaction. Message#1 uses the write primitive to corrupt the Victim#1 InData
buffer address so that it points to the Victim#2 base address. That means that any message directed to the Victim#1 transaction will result in corrupting the Victim#2 transaction at the offset specified by the message’s ‘Data Displacement’ field. Victim#2 is the leaked TRANSACTION
structure and its base address can be inferred by its contents.
OutData
The rest of the messages contain the Transaction Secondary command (0x26), and use the same TID, PID and UID. Messages #2-5 target the Victim#1 transaction (MID=0) and perform overwriting of specific fields of the Victim#2 transaction. The table below summarizes the modifications made by each message:
Msg# | Offset | Transaction Field | Overwrite Value |
---|---|---|---|
5 | 0x001 | BlockHeader.State | 0x2 |
3 | 0x054 | Timeout | 0x40000023 |
2 | 0x088 | OutData | Attacker-specified address (e.g. 0xfffff88004b78150) |
2 | 0x090 | SetupCount | 0x4 |
2 | 0x094 | MaxSetupCount | 0x0 |
2 | 0x098 | ParameterCount | 0x0 |
2 | 0x09c | TotalParameterCount | 0x0 |
2 | 0x0a0 | MaxParameterCount | 0x10 |
2 | 0x0a4 | DataCount | 0x0 |
2 | 0x0a8 | TotalDataCount | 0x0 |
2 | 0x0ac | MaxDataCount | 0x20000 |
4 | 0x0e3 | Executing | 0x0 |
Figure 7 – List of the Victim#2 transaction fields that are corrupted during an Arbitrary Read operation
As an example, below is the message sent to execute a remote read operation. The payload is specified in the ‘Extra byte parameters’, the target address in the ‘Data Displacement’ and the size in the ‘Data Count’ field.
Message #5 is a dummy packet has a non-zero MID targeting the Victim#2 transaction, sent to trigger the server response. During that response message, contents of the memory address of the corrupted DataOut
pointer are copied out and sent back to the SMB client. An example of this type of message is seen below:
Code Execution
The techniques discussed in the previous sections operate on memory; the exploit still needs a way to alter control flow and invoke execution in a repeatable way. The exploit achieves this by corrupting pointers to SMB message handlers.
First, it uses the write primitive to overwrite entry 0xe
of the srv!SrvTransaction2DispatchTable
, with the address of the execution target. This is a dispatch table that contains pointers to SMB message handlers. This particular entry, targeting the TRANS2_SESSION_SETUP
subcommand handler, is convenient since it is not implemented, and thus, not expected to be used by “normal” SMB traffic. Details on how this global pointer is discovered and leaked back to the attacker are presented in the next section.
Next, a message of type SMB_COM_TRANSACTION2
and subcommand set to TRANS2_SESSION_SETUP
is sent to the victim, triggering the execution of the corrupted function pointer. The target transaction of this message is not important. An example packet is seen below.
Putting It All Together
In this section, we walk through the exploit and see how the above building blocks combine to achieve remote kernel code execution.
In this phase, a TRANSACTION
structure is leaked from the victim machine. This structure is used in two ways. First, it contains pointers (e.g. EndpointSpinLock
) that serve as the base for discovering other useful addresses. Second, it is used as a Victim#2 transaction, since in order to build a Read primitive the attacker needs the details of a valid TRANSACTION
structure. The method used to leak the pointer is similar to the one described in the Eternal Champion exploit.
Below, are the contents of the SMB_COM_TRANSACTION
message containing leaked pool memory. The leaked TRANSACTION
structure starts at offset 0xb0
. We can see that it contains, among other things, the transaction TID
, PID
, UID
and OtherInfo
. Also, pointers such as InData
(offset 0x130
) allow the attacker to determine the base memory address of the transaction.
0000 ff 53 4d 42 a0 00 00 00 00 98 03 c0 00 00 00 00 .SMB............
0010 00 00 00 00 00 00 00 00 00 08 37 ca 00 08 56 15 ..........7...V.
0020 12 00 00 00 04 00 00 00 c0 10 00 00 00 00 00 00 ................
0030 48 00 00 00 04 00 00 00 58 01 00 00 48 00 00 00 H.......X...H...
0040 b8 10 00 00 00 59 01 00 fc 84 36 3a 10 77 98 5a .....Y....6:.w.Z
0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060 00 01 02 03 46 72 61 67 00 00 00 00 00 00 00 00 ....Frag........
0070 20 51 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Q..............
0080 02 01 01 00 46 72 65 65 00 00 00 00 00 00 00 00 ....Free........
0090 01 01 eb 03 4c 53 74 72 30 a1 07 00 83 fa ff ff ....LStr0.......
00a0 8c 0e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00b0 0c 02 8c 0e 00 00 00 00 d0 2d e6 00 83 fa ff ff .........-......
00c0 90 bb e5 00 83 fa ff ff d0 2d 46 02 a0 f8 ff ff .........-F.....
00d0 d0 8d 41 02 a0 f8 ff ff 48 00 56 02 a0 f8 ff ff ..A.....H.V.....
00e0 48 c0 55 02 a0 f8 ff ff 00 00 00 00 00 00 00 00 H.U.............
00f0 00 00 02 00 00 00 00 00 68 b2 57 02 a0 f8 ff ff ........h.W.....
0100 6d 39 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 m9..............
0110 6c b2 57 02 a0 f8 ff ff 00 00 00 00 00 00 00 00 l.W.............
0120 6c b2 57 02 a0 f8 ff ff fc b2 57 02 a0 f8 ff ff l.W.......W.....
0130 6c b2 57 02 a0 f8 ff ff fc bf 57 02 a0 f8 ff ff l.W.......W.....
0140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0150 00 0d 00 00 00 00 00 00 90 00 00 00 00 00 00 00 ................
0160 00 00 00 00 01 01 00 00 00 00 00 0837 ca 00 08 ............7...
0170 5a 15 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Z...............
0180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0190 01 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
A series of SMB_COM_TRANSACTION
messages are sent in order to allocate a pair of neighboring control-victim transactions. Specifically, “groom” packets contain SMB messages crafted to create the packet confusion, or in other words, eligible to be a control transaction. “Bride” packets create transactions that are candidates for corruption, that is, victim transactions.
The exploit takes control of a neighboring victim transaction to be used for the R/W primitives.
The read primitive is exercised multiple times, in order to discover the location of the srv!SrvTransaction2DispatchTable
global pointer, used trigger shellcode execution.
The read primitive is again exercised multiple times to discover the base of ntoskrnl.exe
. The RWX memory found above is used as a scratch page, where shellcode is written and executed and return values are stored. This page exists due to a RWX section in the ntoskrnl.exe
. It is worth noting that ntoskrnl.exe
on Windows 8.1 and above does not have an RWX section.
kd> ?? 0xfffff802846f4000-0xfffff80284483000
unsigned int64 0x271000
kd> !dh nt -s
SECTION HEADER #3
RWEXEC name
1000 virtual size
271000 virtual address
0 size of raw data
0 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
E80000A0 flags
Code
Uninitialized Data
Not Paged
(no align specified)
Execute Read Write
This is when shellcode is copied and executed on the victim machine. First, using the write primitive, the exploit shellcode is copied to the scratch page. This shellcode is only a stub function that allocates memory in the pool, using nt!ExAllocatePoolWithTag
. Then, a SMB_COM_TRANSACTION2
message is sent to execute the shellcode. The return value is saved at a fixed offset on the scratch page and leaked back to the attacker using the Read Primitive. We can see the stub function below:
;
; Retrieve the _KPRCB structure
;
fffff802`846f400 65488b042520000000 mov rax,qword ptr gs:[20h]
;
; Access the PPNxPagedLookasideList.AllocateEx member
;
fffff802`846f4009 4805b0080000 add rax,8B0h
fffff802`846f400f 31c9 xor ecx,ecx
;
; Set NumberOfBytes (0xe4b) for size argument
;
fffff802`846f4011 8b151e000000 mov edx,dword ptr [fffff802`846f4035]
fffff802`846f4017 4883ec20 sub rsp,20h
;
; Call nt!ExAllocatePoolWithTag
;
fffff802`846f401b ff10 call qword ptr [rax]
fffff802`846f401d 4883c420 add rsp,20h
;
; Check for errors
;
fffff802`846f4021 85c0 test eax,eax
fffff802`846f4023 7407 je fffff802`846f402c
;
; Save the allocated memory address
;
fffff802`846f4025 48890501000000 mov qword ptr [fffff802`846f402d],rax
fffff802`846f402c c3 ret
Lastly, the scratch page is cleared, the attacker-provided shellcode is written to the pool-allocated page and a message is sent to trigger execution.
Impact of Mitigations on Exploit
The techniques used in this exploit as-written are not directly applicable to newer platforms due to several kernel security improvements. In particular:
- Hypervisor-enforced Code Integrity (HVCI) prevents unsigned kernel pages from being executed, and prevents attackers from copying and executing code, even in the presence of RWX memory.
- Control Flow Guard (CFG) prevents invalid indirect function calls, such as calling a corrupted function pointer, a technique used in this exploit to trigger code execution.
Final Words
I’d like to thank the following people for their work during the initial investigation of CVE-2017-0143: Swamy Shivaganga Nagaraju, Nicolas Joly (MSRC Vulnerabilities & Mitigations Team) and Viktor Brange (Windows Offensive Security Research Team).
Georgios Baltas MSRC Vulnerabilities & Mitigations Team