By: Valentina Palmiotti (@chompie1337), Lead Security Researcher
At Grapl, we believe that in order to build the best defensive system we need to deeply understand attacker behaviors. As part of that goal we're investing in offensive security research. Keep up with our blog for new research on high risk vulnerabilities, exploitation, and advanced threat tactics.
RCE PoC for CVE-2020-1350 SIGRed can be found here: https://github.com/chompie1337/SIGRed_RCE_PoC
SIGRed, CVE-2020-1350, is a vulnerability in the Microsoft Windows DNS service that was disclosed on July 14, 2020. It was discovered by Sagi Tzadik, of Check Point Research, who released an in-depth write up of the bug the day the patch was released. The vulnerability received a CVSS score of 10.0, the highest level of severity . On Windows, a DNS server is a Domain Controller, and its Administrators are part of the Domain Admins group. By default, the Domain Admins group is a member of the Administrators group on all computers that have joined a domain, including the domain controllers. If exploited carefully, attackers can execute code remotely on the vulnerable system and gain Domain Admin rights, effectively compromising the entire corporate infrastructure. This write up contains a detailed breakdown of the exploit methods used in the released proof of concept.
CVE-2020-1350 is an integer overflow vulnerability that leads to a heap-based buffer overflow when processing malformed DNS SIG resource records. A SIG record is a type of DNS resource record that contains a digital signature for a record set(one or more DNS records with the same name and type). To exploit SIGRed, an attacker can configure an“evil” domain whose NS record points to a malicious DNS server. When a client makes a DNS query for the“evil” domain to the victim server, the victim server will query the DNS server above it. The DNS server will respond back with an NS record indicating that the malicious DNS server is the authority for that domain, and the record will be cached by the victim. Afterwards, when a client sends the victim a DNS SIG query for the domain, the victim server will query the malicious DNS server. The malicious DNS server will send a malformed DNS SIG record as a response.
The vulnerability is present in the function dns!SigWireRead (in the DNS service binary, dns.exe ), which is used to cache a DNS SIG record response from another DNS server.
Refer to line 11 in the decompilation of the vulnerable function dns!SigWireRead. The function RR_AllocateEx is passed a 16 bit unsigned integer for the size parameter. It is possible to send a DNS SIG record response such that size calculated is greater than 0xFFFF, which triggers the integer overflow.
Triggering the Vulnerability
Due to packet size constraints, it is not possible to trigger the vulnerability by solely sending a SIG record with a very large signature. In fact, triggering the vulnerability is not possible over UDP, because the maximum allowed size of a DNS message over UDP is either 512 or 4096 bytes, depending on whether the server supports EDNS0. In either case, it is not enough to trigger the vulnerability. Using DNS truncation , the malicious DNS server can tell the victim server to retry the request over TCP. The total size limit of a DNS message over TCP is 64KB (0xFFFF). This is still not enough to trigger the vulnerability, as the message limit includes space for headers and the original query.
DNS Name Compression
DNS names can be, and often are, compressed in a DNS message. By manipulating the compression of the name in the DNS message over TCP, it is possible to inflate sigName.Length without increasing the total size of the DNS message.
In the above example, the 0xC0 byte (highlighted in the green box above) has the most significant two bits set. This indicates that the following 14 bits represent the offset of the DNS name, relative to the start of the DNS message. In the example pictured above, it is 0xC bytes (highlighted in the blue box) from the start of the message.
DNS names are encoded as follows: a single byte denotes the number of characters before a ‘.’ character, terminating in a null byte. For example, www.google.com may be encoded as:
which indicates that the name consists of a string of 3 bytes, a period, 6 bytes, a period, and then three bytes terminated by a null.
In the case pictured in the packet capture above, the offset points to 0x1, because the requested domain begins in ’9.’. Therefore, if we change the following 14 bits from 0x0C to 0x0D, the DNS name offset will point to 0x39, indicating that the next part of the DNS name is 0x39 bytes forward, which extends into the signature portion of the packet. This way, we can trick the Windows DNS service into calculating the DNS name size to be much larger than the number of actual bytes used to represent the DNS name.
As seen in the decompilation of dns!SigWireRead, the total size passed to RR_AllocateEx is calculated as: signatureLength + sigName.Length + 0x14. The maximum length for a DNS name is 0xFF bytes. The additional bytes from the fake name length are enough to trigger the vulnerability and also stay under the maximum message size of 65KB!
For a more detailed explanation of the vulnerability and how to trigger it, including how to trigger it from a browser, please see the original Check Point Research article .
This was my first time writing a RCE exploit in user mode, and I learned a lot about heap based exploitation. It is my hope that this write up will help others who are interested in learning heap exploitation.
The exploitation strategy for this bug is interesting because it requires both a malicious client and server. It also depends on careful heap manipulation to not only achieve RCE but also make the exploitation reliable. This section will describe all the necessary pieces and show how they are used together to obtain RCE on the victim server.
Triggering the Vulnerability without Crashing
The most time consuming portion of this exploit was understanding the correct way to manipulate the heap. This section will focus on the details of heap grooming to avoid crashes and control heap buffer freeing and allocation.
WinDNS Heap Manager
It is first necessary to understand how the WinDNS service manages heap memory. The WinDNS service manages its own memory pools . If the requested size of a buffer is more than 0xA0 bytes, it will request the memory from the Windows native heap manager (HeapAlloc). Otherwise, it will use a memory pool bucket (sizes 0x50, 0x68, 0x88, and 0xa0). The buffers in each of the buckets are stored in a singly linked list. If there are no more available buffers in the selected bucket, a memory chunk will be requested from the native heap, divided into separate buffers, then added to the list of the corresponding bucket. For buffers of sizes 0x50, 0x68, 0x88, and 0xA0 memory chunks of sizes 0xFF0, 0xFD8, 0xFF0, and 0xFA0 are requested, respectively.
When buffers in one of the memory buckets are freed, they are not returned to the Windows native heap. Instead, they are added back to the list of available buffers for that bucket. Buffers are allocated on Last-In-First-Out(LIFO) basis, meaning the last buffer to be freed will be the next to be allocated.
WinDNS Buffer Structure
The structure of a WinDNS buffer is as follows:
This will become useful during exploitation.
Avoiding a Segmentation Fault During memcpy
The first issue I ran into when writing my exploit, was a segmentation fault occurring during the memcpy of the large signature into the allocated heap based buffer. I had to make sure the entirety of the overflow bytes were copied into a valid memory address.
While investigating the heap layout, I found that the Windows native heap manager allocated the bucket memory chunks within “internal” heap segment of sizes 0x41FD0-0x41FF0. From my observations, these heap segments onlycontain memory chunks used for the WinDNS memory buckets. So, if we ensure the size of the overflown buffer is less than 0xA0, we can be sure it will be within one of these chunks.
Heap segments containing WinDNS memory chunks after heap spray
This buffer will be somewhere inside a heap segment of size ~0x41ff0. The total number of bytes needed is slightly more than 0xFFFF. Therefore the probability that the entirety of the overflow ends up at valid memory address is relatively high.
Making a hole
We can guarantee our chances of landing at a valid memory address if we can trigger the freeing of a buffer in the middle of a many contiguous heap segments, reallocate it and overflow the buffer. This is a common technique in heap exploitation.
In this case, we can cause a buffer to be freed by making a query to the victim client and having our malicious DNS server return a response with a short TTL (Time-To-Live). Similarly, we can ensure cached record buffers won’t be freed by assigning a long TTL. Expired records are freed every ~2 minutes.
The process to make a hole in the heap and reallocate it has a few basic steps:
- Make many queries for subdomains of the evil domain to the victim server.
- The malicious DNS server will give the victim a response, which the victim will cache in heap memory (heap spray).
- The malicious DNS server will assign a long TTL (Time-To-Live) for all subdomains except one, which will be given a short TTL.
- WinDNS frees buffers for expired records every ~2 minutes, so we wait for the buffer to be freed.
- Make another query for the subdomain whose SIG record just expired; this time the malicious DNS server will give a malformed response to trigger the overflow.
- Because buffers are LIFO allocated, the new record buffer will have the same address of the expired SIG record in memory.
Avoiding Crashes due to Overwriting Other Objects on the Heap
While I was able to reliably avoid segmentation faults during memcpy, I still encountered many other crashes from overwriting objects on the heap.
I decided to look in WinDbg to see what types of buffers were being allocated near the overflown buffer.
I could see that new WinDNS memory chunks of size 0xFF0 and 0xFA0 were being allocated near the heap buffer I was overflowing. The latter came as a result of the heap spray (record buffers of size 0xA0). But what about the chunks of size 0xFF0? These contained buffers of size 0x88, used to store objects related to the DNS record cache, which is saved as a binary tree. I was overwriting cache tree objects and causing a crash when the tree was traversed.
The solution became clear at this point. Remember that the overflown buffer is within a heap segment that is shared only by other WinDNS managed memory chunks. This means the objects being overwritten are of a size <= 0xA0 and fit into one of the memory buckets managed by WinDNS. We know that buffers in these memory buckets are never released back to the native heap, and are instead returned to the free buffer list of the corresponding bucket size. So, we can groom the heap by forcing the allocation of many buffers of size 0x88 and cause them to be freed. Once they are freed, they will be returned to the free buffer list, avoiding the need to allocate new heap memory. We can then spray the heap with many buffers that will not be freed to ensure the buffer we overflow will be in a new heap segment away from the objects we don’t want to overwrite.
Overwriting Objects in the Heap
Now that we have sufficiently groomed the heap to avoid a crash, the next step is to overwrite heap objects that will create the exploit primitives. In the last section, we made a hole for the overflown buffer. With this hole, we are set up to overwrite the “throwaway” cached records we sprayed the heap with.
Know Your Surroundings
Because we spray the heap, many new memory chunks will be allocated. These buffers are added contiguously to the freelist, meaning they are allocated in contiguous order. Therefore, the order the DNS SIG record queries are made will be the order in which they appear on the heap. So, we know exactly what records will be overwritten during the overflow.
First, let’s look at the structure of a cached WinDNS record:
Knowing this and the structure of WINDNS_BUFF will make it easy to craft fake RR_Record objects.
Controlling Buffer Freeing
Previously, we freed record buffers of our choosing by simply giving them a short TTL and waiting for them to expire and be freed. This is good, but waiting at least two minutes is a problem. Not because we’re impatient, but because it gives us less control over reallocation. It will be useful to be able to trigger the immediate freeing of buffers.
When a RR_Record object is retrieved from the cache to respond to a query, the fields dwTTL and dwTimeStamp are first checked before returning the response. This is because it is possible that the record’s TTL has already expired. Remember, the record cache is only cleaned up every 2 minutes. It is possible a record has expired in between cleanups. We can abuse this by simply zeroing out the dwTTL and dwTimeStamp fields in a fake RR_Record object and sending a query for the corresponding subdomain. This will cause the buffer to be freed.
Controlling Buffer Allocation
Now controlling buffer allocation is straight forward. Since WinDNS buffers are allocated LIFO, once we free a buffer, it will be next of that bucket size to be allocated. Even better, because we also control the values in the WINDNS_BUFF structure we can fake the size of the original buffer! This means that we can allocate objects of different sizes in the area of the heap that we control.
Leaking Heap Addresses
We can now leak an address in the heap by doing the following:
- Trigger the freeing of a fake RR_Record.
- Give the fake RR_Record above the freed one a large wRecordSize.
- Send the victim a SIG query for the subdomain with the fake large wRecordSize.
- The response will go past the real size of the buffer, and include the WINDNS_FREE_BUFF structure data of the freed record below it. This leaks a valid heap address in the pNextFreeBuff field.
Great, we obtained our first memory leak! However, we don’t actually know where the leaked pointer is relative to the area of the heap we control. It would be even better if we could get the address of our overflown buffer. To do this, we can simply free two fake RR_Record objects, and leak the WINDNS_FREE_BUFF of the buffer we freed last. When a buffer is freed, the pointer to the buffer that was freed before it is written to the pNextFreeBuff field.
We now know the exact address of portion of the heap we control! This will be useful later.
Leaking dns.exe Address
Next, we need to leak an address inside dns.exe to defeat ASLR . To leak addresses inside of dns.exe, we can trigger the allocation of a special kind of object that I will refer to as a DNS_Timeout object.
A DNS_TimeOut object has the following structure:
When a DNS record expires, dns!RR_Free is called. If a DNS record is type DNS_TYPE_NS, DNS_TYPE_SOA , DNS_TYPE_WINS, or DNS_TYPE_WINSR they are not freed immediately. Instead, dns!Timeout_FreeWithFunctionEx is called.
In Timeout_FreeWithFunctionEx, a WinDNS buffer is allocated for a DNS_Timeout object. Then, the address of RR_Free and a string are written to the pFreeFunction and pszFile fields, respectively. These will be our dns.exe address leaks. If we trigger the allocation of a timeout object in the area of the heap we control, we can use the same method as before to leak the addresses.
We trigger the allocation of the object by first freeing a fake RR_Record object with a fake buffer size of 0x50, which is the bucket memory size allocated for a DNS_Timeout object. Then, we make some NS queries to the victim for the evil domain. A timeout object will be allocated for each query once the records expire. It is necessary to make several of these queries in case new buffers of size 0x50 have been freed while waiting for the NS records to be expire. We can again leak the memory by making a request for the cached record above it, with a fake large wRecordSize.
Now that we have leaked addresses inside dns.exe, we can use them to calculate the addresses of functions inside the binary. By taking the last 12 bits of the leaked addresses, we can create a mapping to offsets for various versions of dns.exe.
Originally, I thought I could trigger the allocation of a timeout object by simply freeing a fake RR_Record object withwRecordType = DNS_TYPE_NS. Thus, avoiding having to wait the 2 minutes for the NS records to expire. However, when I tried to do this, some check prevents the call to RR_Free on fakeRR_Records with a modified wRecordType. I ran out of time while investigating the issue, so this is a potential area for improvement.
Finally, we have all the pieces for an arbitrary read primitive.
Note that we already have the ability to get code execution by overwriting the pFreeFunction pointer in theDNS_timeout object that was allocated. In the function dns!Timeout_CleanupDelayedFreelist, the function address in pFreeFunction is called for each timeout object in the CoolingDelayedFreeList. This list contains the DNS_Timeout objects representing records that are ready to be freed. Luckily, a DNS_Timeout object contains a field for one parameter that is passed to this function.
We can trigger the vulnerability again after the timeout object is allocated to overwrite these fields.
Modern versions of dns.exe are compiled with Control Flow Guard (CFG) . One known way to bypass CFG is to corrupt return addresses on the stack  and execute using a ROP  method. However, we currently don’t have a stable way to write to the stack. Instead, we can find a valid call target (i.e. a function within dns.exe) to use for a primitive. A suitable candidate is dns!NsecDnsRecordConvert, which takes one parameter .
A parameter to NsecDnsRecordConvert should have the following structure:
Inside this function, a buffer is allocated and a call to Dns_StringCopy is made. This is where the read primitive lies. Since we control the function parameter passed in and its content, we can make the pDnsString field an address we want to read. Inside DNS_StringCopy, a buffer is allocated and the data pointed to by pDnsString (up until a null byte) is copied in.
Since we also control wSize , we control the size of the buffer that gets allocated. So, we force the the allocation of the new buffer into the area of the heap we control. After the data has been copied, we leak the memory using the same method as before.
The address we read should be somewhere within the dns.exe import table that contains an address from msvcrt.dll. I chose dns!_imp_exit which contains the address to msvcrt!exit. This will break the ASLR of msvcrt.dll. With this, we can calculate the address of msvcrt!system.
Note: Dns_StringCopy expects to copy a null-terminated string. If the least significant byte of the address is 0x00, the size of the calculated string will be 1, and the address won’t be copied. In the samples I obtained this wasn’t a problem, but I did not test all possible versions of msvcrt.dll.
Remote Code Execution
All of the of the pieces are now in place to get remote code execution. We can again trigger the allocation of a DNS_Timeout object. Then, we overwrite the pFreeFunction with msvcrt!system and pFreeFuncParam with a heap address to memory that contains the payload command. To obtain a reverse shell, I chose to use mshta.exe to execute a HTA shell from an attacker hosted HTTP server. I found this to be the easiest solution, but there are many other possibilities. The exploit could also be reworked to use any other function(s) instead of system.
SIGRed RCE Exploit Demo Video | https://youtu.be/yiqLmfQCqeY
Detecting Exploitation and Workaround Fix
The released PoC includes a Grapl rule to detect exploitation of SIGRed. To implement a rule for your preferred SIEM, look for invalid child processes of dns.exe. Note that this rule will only detect exploitation with the released PoC and DoS exploits. It is possible an attacker can rework the exploit to implement a payload that stays contained within the dns.exe process context. Learn how Grapl can help keep your infrastructure secure.
If patching is not possible, a workaround fix is available:
The workaround blocks exploitation by limiting the DNS TCP receive message size to 0xFF00 bytes. This prevents the integer overflow from occurring when calculating the buffer size needed to cache a SIG record.
This blog post is part of Grapl’s initiative to strengthen defensive techniques by investing in offensive security research. If you enjoyed this post, please subscribe to Grapl's updates for more!
maxpl0it, who wrote the first public DoS PoC for this bug, and answered a ton of my questions. He also kindly provided me with some of his research notes. My exploit uses code from his PoC as a basis.
Andréa, for her excellent work creating the graphics in this write-up.
Connor McGarr, who also wrote a DoS PoC for SIGRed and answered my questions.