Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2020-1206 | Kitploit
Tools/GitHubGitHub/datntsec/cve-2020-1206
Memory ForensicsVulnerability AnalysisExploitationInformation GatheringPenetration TestingBinary Exploitation
GitHubdatntsec/cve-2020-1206

CVE-2020-1206

View Repository
5 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

In the SMBGhost vulnerability (CVE-2020-0796) I talked about a write-what-where primitive technique through the use of an integer overflow bug to change the pointer Alloc.Userbuffer to point to an address we desire and write arbitrary data into it. Similar to SMB Ghost, this vulnerability also exists in the Srv2DecompressData function in srv2.sys. Let's review the Srv2DecompressData function related to the SMBGhost vulnerability (CVE-2020-0796) simplified by Zecops``` c typedef struct _COMPRESSION_TRANSFORM_HEADER { ULONG ProtocolId; ULONG OriginalCompressedSegmentSize; USHORT CompressionAlgorithm; USHORT Flags; ULONG Offset; } COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;

typedef struct _ALLOCATION_HEADER { // ... PVOID UserBuffer; // ... } ALLOCATION_HEADER, *PALLOCATION_HEADER;

NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize) { PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset), NULL); If (!Alloc) { return STATUS_INSUFFICIENT_RESOURCES; }

root@kitploit:~
ULONG FinalCompressedSize = 0;


NTSTATUS Status = SmbCompressionDecompress(
    Header->CompressionAlgorithm,
    (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
    (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
    (PUCHAR)Alloc->UserBuffer + Header->Offset,
    Header->OriginalCompressedSegmentSize,
    &FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}


if (Header->Offset > 0) {
    memcpy(
        Alloc->UserBuffer,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
        Header->Offset);
}


Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;

}

root@kitploit:~
The Srv2DecompressData function takes a compressed message sent by the client and proceeds to allocate a necessary memory region, decompress the message into it. Then, if the Offset field is non-zero, it copies the data (RawData) before the compressed data to the beginning of the allocated memory region.

![](https://assets.kitploit.com/production/public/readmes/24502/a5bf8b5059336fb3677162343bace0d0b45085f2eb89e42d85f81e032fe233dc.png)

The SMBGhost bug lies in the function not checking for integer overflow, leading to incorrect allocation size and causing a buffer overflow. Three months after Microsoft patched SMBGhost, the vulnerability CVE-2020-1206 (SMBleed - as named by [Zecops Blog](https://blog.zecops.com/)) was discovered. This vulnerability allows us to leak the address of another machine, and if combined with SMBGhost, we can achieve RCE. To have a simpler view of the Srv2DecompressData function, we will reuse this function as it was before the SMBGhost patch, assuming it has been patched.

# Faking OriginalCompressedSegmentSize
Like SMBGhost, this time we will still fake the OriginalCompressedSegmentSize with a number slightly larger than the decompressed data we send. For example, we compress data of size x bytes; instead of setting x into the OriginalCompressedSegmentSize field, we set it to x + 0x1000. See the following image for clarity:

![](https://assets.kitploit.com/production/public/readmes/24502/687d3bdc67e4b2f5bb3cecbe41ea52be98a5c904a8688b325a69acb0a854ca75.png)

Uninitialized kernel data will be treated as part of the message.

As I mentioned in the analysis of [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796), Srv2DecompressData will still skip the checking stage after the SmbCompressionDecompress function if the decompression is successful:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}

Mặc dù trường OriginalCompressedSegmentSize được đặt thành x + 0x1000 thay vì x, nhưng sau khi giải nén thành công, biến FinalCompressedSize không chứa giá trị x, mà sẽ chứa giá trị x + 0x1000:```c NTSTATUS SmbCompressionDecompress( USHORT CompressionAlgorithm, PUCHAR UncompressedBuffer, ULONG UncompressedBufferSize, PUCHAR CompressedBuffer, ULONG CompressedBufferSize, PULONG FinalCompressedSize) { // ...

root@kitploit:~
NTSTATUS Status = RtlDecompressBufferEx2(
    ...,
    FinalUncompressedSize,
    ...);
if (status >= 0) {
    *FinalCompressedSize = CompressedBufferSize;
}

// ...

return Status;

}

root@kitploit:~
Because after successful decompression, FinalCompressedSize is updated to hold the CompressedBufferSize value (corresponding to the OriginalCompressedSegmentSize passed to the SmbCompressionDecompress function). This subsequent update and check is almost unnecessary and may lead to some unexpected errors.

## Basic Exploitation
The message structure that Zecops uses to demonstrate the vulnerability is the [SMB2 WRITE message](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8). This structure contains fields such as the number of writable bytes, flags, etc., followed by a buffer of arbitrary length. This is quite perfect for exploiting the vulnerability, since we can create a message and specify the header, with a buffer containing uninitialized data.

Based on [Zecops'](https://blog.zecops.com/) POC in Microsoft's WindowsProtocolTestSuites repository, to get a clearer view of this, we will add this small addition to the compression function:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
    ((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
    compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}

Note that this POC requires authentication and write share permissions, which are often available in many cases. However, the error return applies to every message (including messages with or without authentication), so it's possible we can exploit without authentication. Another thing is that the memory we will leak comes from previous allocations in NonPagedPoolNx, and since we can control the allocation size, we can control the data we leak to some extent.

SMBleed POC Source Code

So if there is no authentication, can we still leak a kernel address? To answer this question, let's analyze SMB deeper.

Deep dive into SMB

When authenticating, the client sends the following messages:

SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP

If authentication fails, the connection is terminated after the second SMB2 SESSION_SETUP packet:

Assuming we don't have authentication, we will check if there is any command that can be sent without needing authentication. Through searching, we notice:

  • The first command must be sent is SMB2 NEGOTIATE and it is also the only SMB2 NEGOTIATE command during a session.
  • The subsequent commands, until successful authentication, must be SMB2 SESSION_SETUP.

Among them, the SMB2 NEGOTIATE message will not be compressed. The bug lies in the decompression function, so we will not consider it and only examine the SMB2 SESSION_SETUP messages.

SMB2 SESSION_SETUP

As mentioned above, a normal session will have 2 SMB2 SESSION_SETUP commands sent. The returned packets do not contain any data necessary for us to exploit, and we have no way to affect the returned packet. However, the second returned packet will have an empty body with status 0xC000006D (STATUS_LOGON_FAILURE) in the packet header. Notice that the first SMB2 SESSION_SETUP packet will contain the request NTLM Negotiate message and the second packet will contain the NTLM Authenticate message. The NTLM Negotiate message is quite simple and may not be interesting, so we will go deep into the NTLM Authenticate message.

NTLM Authenticate message

After studying the NTLM Authenticate message, we notice that the most complex part of this message, most suitable for exploitation, is the NTLM2 V2 Response structure. This structure is a byte array of variable size, mainly containing the NTLMv2_CLIENT_CHALLENGE structure. We see that if this structure does not pass the initial checks, the value 0xC000000D (STATUS_INVALID_PARAMETER) will be returned instead of 0xC000006D (STATUS_LOGON_FAILURE). One of the initial checks is the check of the AvPairs field.

The AvPairs field is a byte array of variable size containing AV_PAIR structures. Each AV_PAIR defines an attribute/value pair; the attribute is defined by the AvId field, the AvLen field defines the length in bytes of the value, and the Value field is a byte array of variable size containing the value itself. An item with the attribute MsvAvEOL and zero length marks the end of the array.

The Authenticate message is processed by the function SsprHandleAuthenticateMessage in the msv1_0.dll module. During the initial checks, this function ensures that the AvPairs array contains the following attributes: 0x0001 (MsvAvNbComputerName), 0x0002 (MsvAvNbDomainName). However, their values are not checked; it only checks by traversing the array and checking whether the required attribute exists and whether its length is within the structure. If the length is too large, the traversal stops. So, in practice, MsvAvEOL is not checked for validity.

At this point, we have found that we can create a request that can help us answer the following question: Given two bytes at offset x, of type uint16, is the value greater than y? x and y are controlled by us. Consider the following packet:

The content of the value 0x0001 (MsvAvNbComputerName) is unimportant, so we can use it to adjust the offset of the second value. For the second value, we only set the attribute to 0x0002 (MsvAvNbDomainName), without initializing len and value. At the same time, set the size of the entire packet so that there are y bytes according to the length field. Two possible results occur depending on the uninitialized value of the length field of the second value:

  • length <= y: In this case, the check passes because the valid value 0x0002 (MsvAvNbDomainName) is found. The server returns 0xC000006D (STATUS_LOGON_FAILURE) because the credentials are incorrect.
  • length > y: In this case, the check fails because the second value has an invalid length and is discarded. The server returns 0xC000000D (STATUS_INVALID_PARAMETER) for this case.

Based on the server response, we can always deduce the answer to the above question.

However, the NTLM Authenticate message is limited to 0xB48 bytes and will be discarded if larger. The check is performed by the function SspContextGetMessage in the msv1_0.dll module. So suppose we only write 1 byte of len, and the remaining byte contains an uninitialized value, can we bypass this? Unfortunately not, because the uint16 value is encoded in little endian. Thus, we cannot achieve what we want in a single SMB session; we will examine other factors.

Observation #1: Lookaside lists

As mentioned in previous research (CVE-2020-0796), the SMB processing modules in the kernel (srv2.sys and srvnet.sys) use a custom allocation function - SrvNetAllocateBuffer - exported by srvnet.sys. This function uses lookaside lists for small allocations to optimize performance. Lookaside lists are used to efficiently store a set of fixed-size buffers that can be reused by the driver.

Lookaside lists are created at initialization; the list for each size and logical processor is described in the following table:

Each cell with the symbol "📝" is a separate lookaside list. To simplify analysis, we will assume our target has only one logical processor. In this case, as long as the same number of bytes is allocated, the same lookaside list is used, and the same buffer will be reused many times. We can use this to gain some control over uninitialized data.

Observation #2: Failing the decompression

Let's revisit what happens when a compressed packet is decompressed (refer to the CVE-2020-0796 writeup for more details and pseudo-code):

In the case where CompressedData is invalid, the decompression phase fails, the copy phase is not executed, and the connection is terminated. But decompression can fail only after partially decompressing valid CompressedData. This allows us to create a request such that our chosen data is written at our chosen offset, as shown below:

Back to the NTLM Authenticate message

We can use the above observations to make our technique work by using two steps:

  1. Send a message with invalid compressed data so that only a single zero byte is decompressed. That byte will be the first byte of the length field of the second value in the AvPairs array.
  2. Send a similar message as before, but ensure that the same lookaside list is used for the allocation, so that the zero byte is there.

This time, the technique can answer the question: Given a byte at offset x, is its value greater than y? As before, x and y are controlled by us.

Since we can reuse the buffer multiple times by ensuring the same lookaside list is used, we can repeat the steps many times while varying y and eventually deduce the value of the byte at a certain offset.

However, this technique has a limitation - the offset of the byte we can read is limited to byte 0xADB from the start of the packet buffer. This is because the offset of the NTLM Authenticate message (AUTHENTICATE_MESSAGE) is limited to 0x40 bytes after the end of the SMB2 SESSION_SETUP headers (enforced by the function Smb2ValidateSessionSetup in srv2.sys) and the size of the NTLM Authenticate message (AUTHENTICATE_MESSAGE) is limited to 0xB48 bytes. We will find a way to overcome this.

Suppose we want to read a byte at offset 0x1100. We cannot do that directly with the above technique; however, we can still use the following technique: since buffers are reused from lookaside lists, we can "lift" the target byte through the decompression function by setting the Offset field to skip past that byte. We just need to ensure that the data at that location can be interpreted as valid compressed data; otherwise, the Copy will not occur.

The packet buffer containing the data sent by the client will include an additional 16-byte header that is not copied during decompression. As a result, the copied and decompressed data, including the target byte, is copied to a position 16 bytes closer to the beginning of the allocated buffer. We can repeat this several times until the offset of the target byte is low enough.

Address leak POC

You can find a script demonstrating the above technique here. Remember that we assumed the server machine has only one logical processor, so you must configure your virtual machine correctly for the script to work. If everything goes well, the script will read and leak the address of the NonPagedPoolNx pool. In fact, that will be the address of one of the buffers in the same lookaside list.

Since this technique has many limitations, I will not analyze it further. However, you can still read the above script and analyze it yourself.

A different approach – decompression

During our research, Zecops realized that the SMB packet being decompressed is not the only complex structure that can be invalid in various ways. Even before processing all SMB-related structures, the compressed buffer can also be invalid. If decompression fails, the connection to the server is terminated.

Microsoft provides three compression algorithms to choose from when implementing SMB: LZNT1, Plain LZ77, and LZ77 + Huffman. We will only consider LZNT1 because it is quite simple - about 80 lines of Python for a decompression function. I will briefly describe the decompression process: compressed data consists of a sequence of compressed blocks, each block starts with a uint16 indicating the length of that block. When a length of 0 is encountered, decompression is complete. We will use this to write a sequence of zero bytes representing valid compressed data. The purpose is to answer the above question: Given a byte at offset x, is its value greater than y? Of course, x and y are still controlled by us.

Below is an example of the compressed data we will send:

Two possible results occur depending on the uninitialized value of the first byte of the length field:

  • length <= y: In this case, the first block will be all zeros, which is completely valid, and the length of the next block will be 0, decompression completes. The server returns a response.
  • length > y: In this case, the first or second compressed block will contain 0xFF bytes; this block will not decompress. The server terminates the connection due to invalid compressed data.

Similar to the previous technique, we can use Observations #1 and #2 to create a message with an uninitialized byte in the middle of the message by using two steps:

  1. Send a message with invalid compressed data so that only part of the data is decompressed, similar to the above figure.
  2. Send a second message and ensure that the same lookaside list as in message 1 is used, so that the bytes from step 1 will be there.

Note that the Offset value in the SMB packet header points to the compressed data, which may be valid or not depending on the value of the uninitialized byte.

The most notable advantage of this technique over the previous one is that there is no longer an offset limitation.

Thus, in summary, we have two techniques to read uninitialized memory from the pool buffer allocated by the SrvNetAllocateBuffer function of the srvnet.sys module. The first technique creates a special SMB packet and then deduces information through the server's response. The second technique, with fewer limitations, creates specially compressed data and sends it, then deduces information based on whether the server terminates the connection.

Thus, we can use either of the two techniques to exploit. And as I said, the first technique has many limitations, so we will only go deep into the second technique.

This technique will help us exploit the write-what-where primitive that Zecops demonstrated earlier in previous research on achieving local privilege escalation. We will use this technique to leak addresses in the memory layout in order to use the write-what-where primitive. Unfortunately, the memory allocated by the SrvNetAllocateBuffer function is mainly used for network data like SMB packets and does not contain any system pointers. And since we need to achieve RCE, leaking uninitialized memory from previous allocations by the SrvNetAllocateBuffer function is useless because we cannot be certain of the location of the needed pointer. We need to find something more useful.

SrvNetAllocateBuffer and the allocated buffer layout

As I mentioned in the research on local privilege escalation (CVE-2020-0796), the SrvNetAllocateBuffer function does not just return a buffer of the requested size. Instead, it returns a pointer to an area just below the user buffer of the pool-allocated memory block, which contains information about the allocated buffer. The layout of the pool-allocated memory block is as follows:

Although our read technique can only read bytes from the "User Buffer" area, we can use another technique to copy parts of the SRVNET_BUFFER_HDR structure into the "User Buffer" of another buffer so that we can read it. By setting the Offset field to point to the SRVNET_BUFFER_HDR structure outside the data we want to read. We just need to ensure that the data at that location can be interpreted as valid compressed data; otherwise, the copy will not occur.

Hunting for pointers

Let's examine the fields of the SRVNET_BUFFER_HDR structure and see if there is any content worth reading:``` c #pragma pack(push, 1) struct SRVNET_BUFFER_HDR { /00/ LIST_ENTRY ConnectionBufferList; /10/ WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list /12/ WORD LookasideListIndex; // 0 to 8 /14/ WORD LookasideListLogicalProcessor; /16/ WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2 /18/ PBYTE UserBufferPtr; /20/ DWORD UserBufferSizeAllocated; /24/ DWORD UserBufferSizeUsed; /28/ DWORD PoolAllocationSize; /2C/ BYTE unknown1[4]; /30/ PBYTE PoolAllocationPtr; /38/ PMDL pMdl1; /40/ DWORD BytesProcessed; /44/ BYTE unknown2[4]; /48/ SIZE_T BytesReceived; /50/ PMDL pMdl2; /58/ PVOID pSrvNetWskStruct; /60/ DWORD SmbFlags; /64/ PVOID TracingPtr1; /6C/ SIZE_T TracingUnknown1; /74/ PVOID TracingPtr2; /7C/ SIZE_T TracingUnknown2; /84/ BYTE unknown3[12]; }; #pragma pack(pop)

root@kitploit:~
The pointers `UserBufferPtr`, `PoolAllocationPtr`, `pMdl1`, `pMdl2` are pointers pointing into the pool-allocated memory block, with offsets that can be computed in advance, so we only need to read one of them. Having a pointer to the pool-allocated memory block will definitely help us in exploitation. Additionally, the following pointers are also very important:
- **ConnectionBufferList**: A linked list of all received but unprocessed buffers of a connection. The head of this list is a connection object created by the function `SrvNetAllocateConnection` in `srvnet.sys`. A buffer is added to the list by the function `SrvNetWskReceiveComplete`. In our case, there will be only one buffer in the list, so both pointers (Flink and Blink of the `LIST_ENTRY` structure) will point to the head of the list inside the connection object.
- **pSrvNetWskStruct**: Initially, a pointer pointing to the connection object mentioned above. The pointer is set by the function `SrvNetWskReceiveEvent`, but is overwritten by the function `SrvNetWskReceiveComplete` with a pointer pointing to the `SRVNET_BUFFER_HDR` structure. Therefore, reading it is not more useful than reading one of the four pointers already mentioned. Incidentally, if you search for "pSrvNetWskStruct", you will see that it plays a role in EternalBlue exploitation.
- **TracingPtr1/2**: These pointers are only used when tracing is enabled.

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

As you can see, the only other useful pointer for us to read is a pointer in the `ConnectionBufferList` structure. Both pointers (Blink and Flink in the `LIST_ENTRY` structure) point to the connection object. This object is named `SRVNET_RECV` by the EternalBlue researcher, so we will also use this name.

## Getting a module base address
Now, we know how to obtain two pointers – one pointing to the pool-allocated memory block and one pointing to the `SRVNET_RECV structure` – we can freely modify the two buffers using the write-what-where primitive. There may be many ways to achieve RCE, but obtaining a module base address will be the simplest choice because there are many things from them that we can modify in the data section of a module. As we have seen, there is no pointer in the memory block allocated by `SrvNetAllocateBuffer` that points to a module. However, there are still some pointers pointing to modules:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

The reading technique we have only allows us to read data in the "User Buffer" area, while these pointers are quite far away and are pointed to by many other pointers. We need a piece of code that can do the following to copy the pointer value into the "User Buffer" area:``` c
ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

If we can find such a piece of code, we will trigger it to copy the first pointer (e.g., HandlerFunctions) into the "User Buffer", read it, then copy the second pointer (e.g., the Srv2ConnectHandler function pointer) into the "User Buffer" and read it, deducing the module base address from it. The Zecops team searched for such a piece of code for a long time, but did not find any suitable one. Finally, they used another option related to the SrvNetFreeBuffer function (simplified as below) which has a nearly desired functionality:``` c void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer) { PMDL pMdl1 = Buffer->pMdl1; PMDL pMdl2 = Buffer->pMdl2;

root@kitploit:~
if (pMdl2->MdlFlags & 0x0020) {
    // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
    MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);
}

if (Buffer->BufferFlags & 0x02) {
    if (Buffer->BufferFlags & 0x01) {
        pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
        pMdl1->ByteCount -= 0x50;
        pMdl1->ByteOffset += 0x50;
        pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

        pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
        pMdl2->ByteCount = pMdl1->ByteCount;
        pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
        pMdl2->Size = /* some calculation */;
        pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL
    }

    Buffer->BufferFlags = 0;

    // ...

    pMdl1->Next = NULL;
    pMdl2->Next = NULL;

    // Return the buffer to the lookaside list.
} else {
    SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
    ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}

}

root@kitploit:~
When releasing the buffer, if the buffer flags are 0x02 (meaning the buffer is part of a lookaside list) and 0x01 (meaning the buffer has no transport header) are set, some operations are performed on two MDL objects to add the transport header before resetting the flags to 0 and returning the buffer to the lookaside list. If we look closely behind the operations on the MDL objects, we can notice that the code performs a double-dereference-read followed by a double-dereference-write with two variables we control (two MDL pointers), which is what we are looking for. The downside is that the content we want to read is also modified, a side effect we hope to avoid.

With the above, here is how we manage to read the AcceptSocket pointer:
1. Prepare buffer A from a lookaside list so that the "User buffer" region is filled with zeros. The user buffer region of this buffer will contain the pointer we will read.
2. Prepare buffer B from a different lookaside list so that:
- The pMdl1 pointer points to the address of the AcceptSocket pointer minus 0x18 (because the offset of MappedSystemVa is 0x18 in the MDL structure).
- The pMdl2 pointer points to the "User buffer" region of Buffer A.
- The Flags field is set to 0x03.

We can overwrite the SRVNET_BUFFER_HDR structure fields by decompressing them from the larger buffer using the technique described in [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) above.

3. When Buffer B is released, the following operations will occur:
- The MDL flags will be read from the second MDL at buffer A. If the MDL_PARTIAL_HAS_BEEN_MAPPED flag is set, MmUnmapLockedPages will be called and the system may crash. That is why we must fill the buffer with zeros in step 1.
- The AcceptSocket pointer and the memory around it will be modified as described here:```
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  AcceptSocket
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • The AcceptSocket pointer and the memory around it will be read as described here:``` +00 | __ __ __ __ __ __ __ __ +08 | __ __ __ __ __ __ __ __ +10 | __ __ __ __ __ __ __ __ +18 | ab cd ef gh ij kl mn op <-- AcceptSocket +20 | __ __ __ __ __ __ __ __ +28 | qr st uv wx __ __ __ __
root@kitploit:~
- The “User buffer” of buffer A will be modified as described here: (The orange bytes contain the pointers we want to read, we just need to arrange them correctly)```
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 c0 ef gh ij kl mn op
+28 |  qr st uv wx ab 0d 00 00

  1. Read the AcceptSocket pointer from the “User buffer” area of buffer A.

Good news: we have read the pointer. Bad news: we have corrupted some data in the SRVNET_RECV structure. Fortunately for us, the error does not affect the system as long as nothing happens to the relevant connection. When something does happen, e.g., closing the connection, the system will crash. That is not a problem because we will soon have RCE and we can fix the error if we want.

After reading the AcceptSocket pointer, we continue using the same technique to read the srvnet!SrvNetWskConnDispatch pointer. The reason we read the AcceptSocket pointer instead of the HandlerFunctions pointer is that the array of HandlerFunctions is shared between all connections, while the buffer pointed to by AcceptSocket is not shared with other connections. Therefore, if we corrupt parts of AcceptSocket, it will only affect the stability of one connection.

If we have a copy of the srvnet.sys file used on the target machine, we can easily deduce the base address of the srvnet.sys module by subtracting the offset of the SrvNetWskConnDispatch pointer that we have leaked.

Implementing arbitrary read

Assuming we have the base address of the srvnet.sys module, we can call any function of the module. But what about the function arguments? The function srv2!Srv2ReceiveHandler is called by SrvNetCommonReceiveHandler and the call has the following form:``` c HandlerFunctions = *(pSrvNetRecv + 0x118); Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128); Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130); (HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

root@kitploit:~
The first two arguments are read from the SRVNET_RECV structure, so we can control them; however, we cannot control the remaining arguments. The x86-64 calling convention specifies that the caller is responsible for allocating and freeing stack space for arguments, so even though the function is intended to be called with 8 arguments, we can replace the pointer with a function expecting any other.

![](https://assets.kitploit.com/production/public/readmes/24502/223827f49f26993604c20b873d75ea5b11d306a85ff5210ceb2583f0a0b9452c.png)

Below are the steps we will use to trigger the function call:
1. Send a specially crafted message so that the connection's SRVNET_RECV structure pointer will be copied into a buffer that we can read.
2. Send another valid message, which will reuse the same SRVNET_RECV structure, but without closing the connection. Note that when the connection is closed, the SRVNET_RECV structure is not freed. The function `SrvNetPrepareConnectionForReuse` is called to reset the structure so it can be reused for the next connection.
3. Read the SRVNET_RECV structure pointer we copied in step 1.
4. Replace the `HandlerFunctions` pointer and its arguments using the write-what-where primitive.
5. Send an additional message over the connection from step 2 so that the replacement function for `srv2!Srv2ReceiveHandler` is called.

Now all we have to do is find a function to copy memory from one location to another, so we can arbitrarily copy memory into a pool buffer that we can read from. `memcpy` is an option, and `srvnet.sys` has such a function (more precisely, `memmove`), but this function requires a third argument, which determines the number of bytes to be copied, and we cannot control it. However, we are not limited to functions implemented in `srvnet.sys`; we can also call functions from `srvnet`'s import table, and `RtlCopyUnicodeString` is a perfect choice to achieve what we want.

The function `RtlCopyUnicodeString` takes two `UNICODE_STRING` pointers as arguments and copies the contents of the source string to the destination string. Unlike C strings terminated with a null character, strings in the kernel are defined by the `UNICODE_STRING` structure, which contains a pointer to the string and the length of the string in bytes. The string buffer can contain any binary data. If you look at the code of `RtlCopyUnicodeString`, you can see that the copy is performed via `memmove`, i.e., it copies pure binary data. All we have to do is prepare two `UNICODE_STRING` structures and call `RtlCopyUnicodeString`, then read the copied data:

![](https://assets.kitploit.com/production/public/readmes/24502/8a450b86ecf834ec4905a2bb72dab3781d1dd2ad56ee197a003e282180ce51bc.png)

## Executing shellcode
After achieving a convenient arbitrary read primitive, we move to the next challenge towards the goal of Remote Code Execution by running a shellcode. We will use the technique presented by Morten Schenk in his [Black Hat USA 2017](https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf) talk (slides 47-51).

The idea is to write shellcode below the `KUSER_SHARED_DATA` structure, which has a constant address—the only address not randomized in the kernel memory layout of recent Windows versions. Then, modify the relevant page table entry to make the page executable. The base address of page table entries in the kernel is randomized, but it is retrieved from the function `MiGetPteAddress` in `ntoskrnl.exe`. Below are the steps we will use to execute our shellcode:
1. Use the arbitrary read primitive to get the base address of `ntoskrnl.exe` from the import table of `srvnet`.
2. Read the base address of the page table entry from the `MiGetPteAddress` function, as described in Morten's slides.
3. Write shellcode to address `KUSER_SHARED_DATA + 0x800` (0xFFFFF78000000800). Note that we could also use one of the pool buffers to store the shellcode; using `KUSER_SHARED_DATA` is for simplicity.
4. Calculate the relevant page table entry address and clear the NX bit to allow execution, as described in Morten's slides.
5. Call the shellcode using the technique previously described to call an arbitrary function.

The shellcode used by Zecops for the reverse shell is [sleepya's shellcode](https://github.com/worawit/MS17-010/tree/master/shellcode), written for the purpose of exploiting EternalBlue. They modified the shellcode to work on recent Windows versions.

# Debug

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

The structure of an SMB packet is roughly as shown above. Suppose we need to leak the address of the User Buffer; we will need to read the `UserBufferPtr` pointer. To read this pointer, we will leverage the [technique](https://github.com/datntsec/CVE-2020-1206#srvnetallocatebuffer-and-the-allocated-buffer-layout) of placing the offset field beyond the pointer so that it is copied into the user buffer of another buffer.

An example of an SMB packet sent by the client with the following content:```c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x0
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x2116
Data = ‘A’ * 0x1101.

This packet, upon arrival at the server, will be stored in a buffer created by the SrvNetAllocateBuffer function. Since the entire packet size falls within the range 0x1100 to 0x2100, this function returns an allocation with a user buffer of size 0x2100 (we will call it Alloc A), and then stores the client-sent information as shown in the figure below:

We can see that the part from address 0xffffd38439044050 to 0xffffd38439045160 is data sent by the client, the part from 0xffffd38439045160 to 0xffffd38439046150 is uninitialized data on the server side, and the part from 0xffffd38439046150 to 0xffffd38439046240 is the data of the SRVNET_BUFFER_HDR of Alloc A. Thus, the pointer we want to read will be at 0xffffd38439046150 + 0x18 = 0xffffd38439046168.

To read this pointer, I used the technique I mentioned above, setting the offset field to exceed the pointer to be read. Therefore, even though the above packet has a size smaller than 0x2100, the offset is set to 0x2116.

Next, the SMB server will call the SrvNetAllocateBuffer function to allocate a memory region based on the sum of OriginalCompressedSegmentSize and Offset (0x2116). From that, it allocates an allocation with a user buffer of size 0x4100 (we will call it Alloc B). The allocated data will look like this:

To avoid unexpected errors, I previously created and re-created multiple buffers in the same lookaside list as Alloc B and filled them with 0x0 bytes.

Next, the SMB server will proceed to decompress the compressed data and copy the uncompressed data sent by the client into the user buffer of Alloc B:

As we can see, there is no compressed data because OriginalCompressedSegmentSize = 0, so the program will copy data from 0xffffd38439044060 to 0xffffd38439044060 + 0x2116 = 0xffffd38439046176 of Alloc A into the user buffer of Alloc B. Thus, a portion of the SRVNET_BUFFER_HDR information of Alloc A has been copied into the user buffer of Alloc B.

Now we will use the technique we mentioned earlier to leak the address of the allocation pool (the address of the User Buffer).

Suppose we want to know whether a byte at address 0xffffd3843636f15e is greater than 0x7f or not? We will create an SMB packet with the following information:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0x7f+3) + ‘\xff’(0xff - 0x7f)) + ‘\xff’*0x1fe9
root@kitploit:~
Why we need to create such an SMB, we will analyze step by step. First, the sum of OriginalCompressedSegmentSize and Offset is 0x4100, so it will reuse an alloc with a similar user buffer, i.e., Alloc B used previously. Because we want to guess whether a byte at address 0xffffd3843636f15e is greater than 0x7f, this address is located 0x210e bytes from the user buffer address, so the uncompressed data will have 0x210e bytes ('B' * 0x210e). Next comes a valid compressed data region (compressed by the compress() function), followed by an invalid compressed data region ('\xff'* 0x1fe9). So when decompression is performed, only the valid compressed data is decompressed into a different alloc, then the connection is aborted due to the trailing invalid compressed data, and the uncompressed data region will not be copied into that alloc, thus the data we copied earlier will be preserved.

![](https://assets.kitploit.com/production/public/readmes/24502/3e1ffd768b120645917e7f1757987f80c79514b52f3bc18e46fdfb0a2d2ca05f.png)

Above is an Alloc containing the information we discussed above created by the SMB server. Next, the SMB server will call the SrvNetAllocateBuffer function to create a corresponding Alloc. Since the sum of its OriginalCompressedSegmentSize and Offset is 0x4100, Alloc B will be reused:

![](https://assets.kitploit.com/production/public/readmes/24502/5b218cbd142d58da175ee6a58bfc505646a560467db6db28ab70e0db34ee9ce3.png)

Then the SMB server decompresses the information sent from the client into the user buffer of Alloc B at the corresponding offset.

![](https://assets.kitploit.com/production/public/readmes/24502/f3b545366e8f3bcff6a69d184cc383bfcf70643c23ff0ac01ece1a95cc0fd88d.png)

The red data is the decompressed data, the rest will be preserved. As shown above, we can see that the byte we need to know is preserved, and immediately after it is the just-decompressed data.

To determine whether this byte is greater than 0x7f, we proceed as follows:

We continue to create an SMB packet with the following content:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x2004
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x20fd
Data = ‘B’ * 0x20f1

Although the total of OriginalCompressedSegmentSize and Offset is greater than 0x4100, when allocating an alloc region to store the packet sent from the client (with a total size less than 0x4100), the SMB server still only allocates an alloc with a user buffer region of 0x4100 bytes as shown below:

The data highlighted in green above is the data of the previously allocated Alloc B, which is reused because it is from the same lookaside list.

Next, the SMB Server will call the SrvNetAllocateBuffer function to allocate an Alloc to hold the decompressed data:

Through decompression, the SMB server will retrieve data from User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. The retrieved data will look like this:

With the decompression algorithm mentioned earlier, it will take the first 2 bytes and use them as the block length. Based on that length, it will take the next part after the length and decompress. As above, the length will be 0xB0D3, however, according to the algorithm, its true length follows the formula: length = length & 0xFFF + 1 → the length will be 0xD4. It will take the next D4 bytes and proceed to decompress normally until it encounters the FF byte (since the 0xD4 bytes will include all 00 bytes and part of the FF bytes). At this point, the compressed data is considered invalid, it will stop decompressing and disconnect.

Based on the server's disconnection, we can guess that the byte we need to know is greater than 0x7f.

So what about when the byte we need to guess is smaller? We will continue the analysis above, but this time we will use the comparison byte D7, so D3 is smaller than D7. Let's see what happens:

First, send the following packet to the SMB server:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0xd7+3) + ‘\xff’(0xff - 0xd7)) + ‘\xff’*0x1fe9
root@kitploit:~
The SMB server will create an alloc for storage as follows:

![](https://assets.kitploit.com/production/public/readmes/24502/3df2c6c8e5de90714455663bbb922efec7ae6888a970a3d65e389a28aea16837.png)

Next, it will call the `SrvNetAllocateBuffer` function to allocate an alloc as follows:

![](https://assets.kitploit.com/production/public/readmes/24502/51affc73361727907ada6fb3f439831d0e95cee531a65106fa79ce4519ceaab8.png)

Of course, this alloc is reused from an alloc with the same lookaside list (Alloc B). Then the program proceeds with normal decompression on valid compressed data, and disconnects on invalid compressed data:

![](https://assets.kitploit.com/production/public/readmes/24502/01a9cc43acbc6b7cc283f40a0dbe348608a3566bfc34ce24aa2cc694315e67a3.png)

In the next step, we will send another data packet similar to the previous one, and the SMB server will allocate a corresponding alloc:

![](https://assets.kitploit.com/production/public/readmes/24502/ed438d4d0049b2188573e0fb3171cadf23701c7b0cef2de43a52434c07381341.png)

The data highlighted in green above is the data from Alloc B that was allocated previously; because they share the same lookaside list, it is reused.

Next, the SMB Server will call the `SrvNetAllocateBuffer` function to allocate an Alloc containing the decompressed data:

![](https://assets.kitploit.com/production/public/readmes/24502/7ed3dca4594bd1772c44689190d4df3b597d43daf7c87edb281b3e82a834884e.png)

Through decompression, the SMB server will fetch data from `User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d`. The extracted data will have the following form:

![](https://assets.kitploit.com/production/public/readmes/24502/68f677093ef93e02c5df9d70dce560e4d06e74446970f1f14f1ad37f08ce898e.png)

Similar to the previous time, the initial length will be `0xB0D3`, which after calculation becomes `0xD4`. It will then extract the next D4 bytes and proceed with normal decompression. However, because `D4 < D7`, the compressed data extracted for decompression at this point contains only zero bytes. It will decompress normally until the end of that block. Next, it will extract the length of the next block based on the length of the previous block; the next two bytes are D5 and D6, both `0x0`, so its length is now `0x0`, therefore the extracted length is `0x0000` → decompression ends → decompression succeeds → the SMB server returns a response → we know that the target byte is less than or equal to `0xD7`.

We continue similarly until we leak all 6 bytes of an address. We will obtain the allocation pool address.

Once we have the allocation pool address, we proceed to find the srvnet base address by obtaining the pointer to the `SRVNET_RECV` structure using a method similar to leaking the allocation pool address.

After obtaining two addresses: allocation pool and `SRVNET_RECV` with respective values: `0xffffd38439044000` and `0xffffd3843654ddd8`, we proceed to leak the srvnet base address.

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

![](https://assets.kitploit.com/production/public/readmes/24502/239c0c59c0e492097bdaf2eac907812124a89ef8bb31841287202ae80f4d13d0.png)

To read the `AcceptSocket` pointer, we need to do the following:
1. Prepare Alloc A from a lookaside list so that the "User buffer" area is filled with zeros. This buffer will later contain the pointer we are going to read. Here, Alloc A will be taken from the Alloc corresponding to the allocation pool address we leaked. Therefore, the User buffer area of Alloc A will start at address `0xffffd38439044050` by using the same lookaside list.
2. Prepare Alloc B from a different lookaside list so that:
- The `pMdl1` pointer points to the address of the `AcceptSocket` pointer minus `0x18` (because the offset of `MappedSystemVa` is `0x18` in the MDL structure).
- The `pMdl2` pointer points to the "User buffer" area of Buffer A.
- The `Flags` field is set to `0x03`.

Thus, the addresses of the two Mdl pointers are: `mdl1_ptr`: `0xffffd3843654de68`, `mdl2_ptr`: `0xffffd38439045250`.

We can overwrite the `SRVNET_BUFFER_HDR` structure fields by decompressing them from a larger buffer using the technique described in [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression).

I will elaborate on this step right after step 4.

3. When Buffer B is freed, the following operations occur:
- The MDL flags are read from the second MDL in buffer A. If the `MDL_PARTIAL_HAS_BEEN_MAPPED` flag is set, `MmUnmapLockedPages` will be called and the system may crash. That is why we filled the buffer with zeros in step 1.
- The "User buffer" area of Alloc A will be modified and contain the information we need to read.
4. Read the `AcceptSocket` pointer from the "User buffer" area of buffer A.
- Use the address leaking technique already used above to read the `AcceptSocket` pointer.

Now I will describe the above steps in more detail:

Step 1 is quite simple and similar to the above, so I will not discuss it further.

In step 2, first we create a packet to send to the SMB Server with the following content:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = -0x38
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x10138
Data = ‘A’ * 0x10138 + compress(mdl1_ptr  + ‘\x00’*0x10 + mdl2_ptr) + ‘\xff’*0x10

As we can see, OriginalCompressedSegmentSize contains a negative value and the sum OriginalCompressedSegmentSize + Offset = 0x10100. However, the packet size sent by the client to the server is larger than 0x10100. Thus, the initial Alloc created by the server before decompression will be larger than the Alloc containing the data after decompression. The value of OriginalCompressedSegmentSize is set to negative here to make the sum of OriginalCompressedSegmentSize and Offset exactly equal to 0x10100, without affecting the location of the compressed data, since it depends on Offset. Meanwhile, 0x38 is the offset of the Mdl1 pointer in the SRVNET_BUFFER_HDR structure.

Thus, the server will create an Alloc containing the client’s data as follows:

Next, it will call the SrvNetAllocateBuffer function to allocate an Alloc with a User buffer size of 0x10100, i.e., Alloc B as per the steps above:

It proceeds to decompress, but of course only a portion of the valid data is decompressed:

Based on the images above, it can be seen that the two Mdl pointers in the SRVNET_BUFFER_HDR of Alloc B have been modified to the values we desire.

Similarly to the above, this time we will set the flag to 3 by adjusting the offset of the sent packet as follows:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = -0x10
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x10110 Data = ‘A’ * 0x10110 + compress(‘\x00\x03’) + ‘\xff’*0x10
root@kitploit:~
Finally, it will be in the form:

![](https://assets.kitploit.com/production/public/readmes/24502/467ea07dff7cfaca7062ed50d1605e40586c412f40bcadaf01aa37d862efd3bd.png)

When Alloc B is freed, the following code blocks will be executed:``` c
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl1->ByteCount -= 0x50;
pMdl1->ByteOffset += 0x50;
pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteCount = pMdl1->ByteCount;
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
pMdl2->Size = /* some calculation */;
pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

As above, pMdl1->MappedSystemVa (offset 0x18) will contain the value of pMdl1->MappedSystemVa + 0x50 = 0xffffd3843654de68 + 0x18 + 0x50 = 0xffffd3843654ded0.

Before freeing Alloc B, SRVNET_RECV will be:

After running the first 4 lines of the above code:

Before freeing Alloc A:

After running all of the above code:

And the bytes we need to read in Alloc A are the blue bytes below:

Thus, we only need to use the byte-by-byte leak technique above to obtain the address of AcceptSocket + 0x50. As in this section, it will be 0xffffd3843ea02418 → AcceptSocket: 0xffffd3843ea023c8

Similarly, we will do this to leak the address of AcceptSocket → srvnet!SrvNetWskConnDispatch

We need to prepare everything as follows:

After Alloc B is freed, everything will change as follows:

The bytes we need to know to obtain the address of AcceptSocket-> srvnet!SrvNetWskConnDispatch + 50 will be in Alloc A; those bytes are the ones highlighted in blue in the image below:

Thus, AcceptSocket-> srvnet!SrvNetWskConnDispatch will be 0xfffff80060e9d170; assuming we already know its offset in the srvnet.sys module, we can calculate the base address of srvnet.

In this section, the srvnet base is: 0xFFFFF80060E70000 with the offset of srvnet!SrvNetWskConnDispatch being 0x2d170.

Next, we will use the Write-what-where primitive technique from CVE-2020-0796 to arbitrarily write to a memory region.

First, we will find a way to leak the ntoskrnl base address by leaking the address of the IoSizeofWorkItem function that srvnet imports. To do this, we first create 2 UNICODE_STRING structures as follows:``` c // Destination unicode string desLength = 6; desMaximumLength = 6; desBuffer = allocation_pool_object_ptr + 0x1650 + 0x20 + 2;

// Source unicode string srcLength = 6; srcMaximumLength = 6; srcBuffer = srvnet_base_ptr + OFFSETS['srvnet!imp_IoSizeofWorkItem'];

root@kitploit:~
With `allocation_pool_object_ptr` being the leaked allocation pool address and `OFFSETS['srvnet!imp_IoSizeofWorkItem']` being the offset of the IoSizeofWorkItem function imported by srvnet.

These two UNICODE_STRING structures will be stored at `allocation_pool_object_ptr + 0x1650` using the Write-what-where technique found in CVE-2020-0796. 
First, we will store the Destination unicode string at `allocation_pool_object_ptr + 0x1650`, then we proceed to create an SMB packet as follows:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0xffffffff
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x22
Data:
sentinel = os.urandom(2)  // 16 bits for verification
data = struct.pack('<HHIQ', desLength, desMaximumLength, 0, desBuffer)  // dest unicode string
data += struct.pack('<HHIQ', srcLength, srcMaximumLength, 0, srcBuffer) // src unicode string
data += sentinel
data_to_compress = os.urandom(0x1100 - len(data))
// 0x18 null bytes that override the struct.
data_to_compress += b'\x00'*0x18
// Target address.
data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650)
data = data + compress(data_to_compress)

Above, the data contains a sentinel generated by the os.urandom(2) function, which will be 2 bytes long. These 2 bytes will help us determine whether the address we leak is actually the address we need to leak by comparing it after the leak process succeeds.

If the total size of the packet sent from the client is greater than 0x1100 (this will depend on the data randomized before compression), then allocation_pool_object_ptr will definitely be used to store it on the SMB server:

Next, the SMB server will call the SrvNetAllocateBuffer function to allocate a memory region for decompression. However, because the sum of OriginalCompressedSegmentSize and Offset is 0x21, it only allocates a region with a user buffer size of 0x1100:

The heap overflow error occurs (as described in CVE-2020-0796) and after the SMB server decompresses (before the uncompressed data copy takes place), the following happens:

Thus, the UserBufferPtr pointer now points to the beginning of allocation_pool_object_ptr + 0x1650, and when the copy process takes place, allocation_pool_object_ptr will:

Similarly, we will insert an additional sentinel below by sending the following packet:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0xffffffff
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x2 Data: data = sentinel data_to_compress = os.urandom(0x1100 - len(data)) // 0x18 null bytes that override the struct. data_to_compress += b'\x00'*0x18 // Target address. data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650 + 0x28) data = data + compress(data_to_compress)
root@kitploit:~
Thus, when the SMB server receives the packet, it will allocate the corresponding memory region. At this point, the allocated memory region is `allocation_pool_object_ptr` and it will contain data as follows:

![](https://assets.kitploit.com/production/public/readmes/24502/24cc306a702cdb5f575bd1d612ea6b35db87aaf875fd45fa64977e50c61e85ed.png)

After the decompression process, the data will be:

![](https://assets.kitploit.com/production/public/readmes/24502/d6442e5a936e1c46ebb9e3a4651e8859dca56c1aa09e9e79ed9155ea105dede2.png)

Thus, we have created 2 unicode strings and two sentinels to verify the data we leaked.

Next, we will call the `RtlCopyUnicodeString` function and pass the two unicode strings above into it.

To call the `RtlCopyUnicodeString` function, we first overwrite the HandlerFunctions pointer with the address of the `RtlCopyUnicodeString` function. This function is imported by the srvnet module and has offset (according to my module) of 0x32288.

Thus, using the write-what-where technique, we write the address 0xFFFFF80060E70000 + 0x32288 - 0x8 into HandlerFunctions.

First, we leak a SRVNET_RECV pointer (0xffffe00f0b593dd8).

![](https://assets.kitploit.com/production/public/readmes/24502/f7c5bde869eb5ba21a0b9dd4e2034417487150592178aa1eb1aef523e246a5c2.png)

We will save the connection to continue sending packets below.

Next, we use the write-what-where technique to write into the pointer RtlCopyUnicodeString - 0x8 (the reason for - 0x8 is to replace the Srv2ReceiveHandler function in HandlerFunctions with the RtlCopyUnicodeString function).

![](https://assets.kitploit.com/production/public/readmes/24502/a88b44518f26ed6f0958772bdd4bf124c7c2403a525944b060a37cf17309a092.png)

Next, we will write the two pointers of the two unicode strings created above into the two arguments of HandlerFunction.

![](https://assets.kitploit.com/production/public/readmes/24502/691bdb0ce599face8b1f35393db9cc37e1f85a075c4d259a7fb96adee9fb66d2.png)

At this point, on the same connection, Srv2ReceiveHandler has been replaced by RtlCopyUnicodeString. Therefore, when we send a packet, the RtlCopyUnicodeString function will be called and copy the Unicode String.

![](https://assets.kitploit.com/production/public/readmes/24502/2410149b163fd4ebbfb13fa5d2a127cafcfdc82146c33544b0a6cc6905c13d2b.png)

The next thing we need to do is to leak 10 bytes of addresses from 0xffffd38439045670 to 0xffffd3843904567a (including the two sentinels at both ends of the address to leak). Then check if the first and last 2 bytes of the leaked address are sentinels. If they are sentinels, we have leaked correctly (0xfffff8068152c380).

After leaking the nt!IoSizeofWorkItem address (0xfffff8068152c380), we subtract its offset (0x12C380) to obtain the ntoskrnl base address (0xfffff80681400000)

Note that each module file's offset varies across different Windows versions, so make sure you have the correct module file on the target machine.

Similarly, after obtaining the ntoskrnl base address, we will get MiGetPteAddress (0xBA968) and obtain the PTE base address (MiGetPteAddress + 0x13) :

![](https://assets.kitploit.com/production/public/readmes/24502/5c92df61b331c038bd90b2ccb878b7027bdbf6338602aecdda1b49a867eb42b1.png)

Next step, we will write the shellcode to 0xFFFFF78000000800 using the write-what-where technique. Then recalculate the shellcode address in the PTE using the formula below and clear the NX bit so that the shellcode can be executed:``` c
shellcode_addr >>= 9
shellcode_addr &= 0x7FFFFFFFF8
shellcode_addr += pte_base

Finally, we will write the shellcode address to allocation_pool_object_ptr + 0x50 + 0x1600 and then call the shellcode by replacing that address with HandlerFunctions and passing the nt_base_ptr address to the shellcode.

Enjoy RCE :))

References

  • SMBleedingGhost Writeup: Chaining SMBleed (CVE-2020-1206) with SMBGhost
  • SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE
  • SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE
  • Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC
  • lznt1.py
  • TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAING WRITEWHAT-WHERE VULNERABILITIES IN CREATORS UPDATE
  • VERGILIUS_MDL
  • Exploit Development: Leveraging Page Table Entries for Windows Kernel Exploitation
  • RtlUnicodeStringCopy function
  • UNICODE_STRING structure

DatntSec. Viettel Cyber Security.

Download Tool
→ Allocation size ↓Logical Processor0x11000x21000x41000x81000x101000x201000x401000x801000x100100
Processor 1📝📝📝📝📝📝📝📝📝
Processor 2📝📝📝📝📝📝📝📝📝
...
Processor n📝📝📝📝📝📝📝📝📝