so many machines

Good Intentions on the Old-Timey Internet

or, the things the greybeards built for fun were almost immediately weaponized by the rabble. A look at the Character Generator Protocol which ended its life as an amplifier for DDoS attacks.

In May of 1983, Jon Postel wrote a series of six Request for Comments or RFCs. This in itself wasn’t irregular. Postel was the RFC Editor from 1969 until his death in 1998 and co-authored over 200 during that time. In Where Wizards Stay Up Late, Postel is introduced by his appearance: “he wore a bushy beard, wore sandals year round, and had never put on a tie in his life.” He is described as an underachiever and mediocre student, and by chapter eight Postel is designing TCP in a hallway of a technical conference on scraps of paper. Among his technical contributions that you touch daily are the definition of TCP/IP, Domain Name Service (DNS), and Simple Mail Transfer Protocol (SMTP). An oft-quoted part of his legacy is Postel’s Law: “be liberal in what you accept, and conservative in what you send.”

Postel in 1994, with map of Internet top-level domains. (Irene Fertik, USC News Service. © 1994, USC)

These six Postel RFCs are of a piece. Unlike typical RFC fare, these documents are short – some under a page – and easily understandable. Each outlines a “useful debugging and measurement tool” to allow network operators to test the reachability of other hosts and troubleshoot connectivity issues. They each allowed operators to connect to another host and receive some data toverify end-to-end connectivity. Echo Protocol sent you back what you sent it, Quote of the Day Protocol returned a small message a la fortune, Daytime Protocol printed out the current date and time - you get the idea. All six are now obsolete.

RFC 864 defines the Character Generator Protocol which “simply sends data without regard to the input.” This protocol existed in blissful obscurity until people learned that requests with a forged source address could be used to flood their enemies (or for lols). Today it only remains implemented and enabled on old printers with ancient fireware that hopefully never get accidentally connected to the internet.

In this post, we’ll look at CHARGEN, its implementation, and its demise.


The CHARGEN protocol is specified in two flavors. The first is based on TCP and stream-oriented. It listens for connections on port 19 and once established will send a continuous stream of characters. The RFC notes that the server should be prepared for “the rude abort” presumably when a client receives enough characters and closes the connection.

The specification doesn’t directly define the characters that should be returned, though it does make a suggestion:

One popular pattern is 72 chraracter lines of the ASCII printing characters. There are 95 printing characters in the ASCII character set. Sort the characters into an ordered sequence and number the characters from 0 through 94. Think of the sequence as a ring so that character number 0 follows character number 94. On the first line (line 0) put the characters numbered 0 through 71. On the next line (line 1) put the characters numbered 1 through 72. And so on. On line N, put characters (0+N mod 95) through (71+N mod 95). End each line with carriage return and line feed.

This results in a barber’s pole kind of pattern when animated. The RFC goes on to provide a sample of the suggested output starting with these eight lines:

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn
()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno

This pattern can be generated by taking advantage of these characters’ ordinal positions1:

chars = [chr(i) for i in range(32, 127)]

for line in range(1, 9):
    for i in range(72):
        char = chars[(line + i) % len(chars)]
        print(char, end='')

    print()

This short Python script mimics the example provided in the RFC:

$ python output.py
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn
()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno

With the pattern generator in place, we can connect this to a tiny TCP server that listens on port 19 to satisfy the Character Generator Protocol specification.

import threading
import socket
from itertools import count

CHARS = [chr(i).encode('ascii') for i in range(32, 127)]
NEWLINE = '\r\n'.encode('ascii')
PORT = 19
WIDTH = 72


def generate(client):
    for line in count(1):
        try:
            for column in range(WIDTH):
                char = CHARS[(line + column) % len(CHARS)]
                client.send(char)

            client.send(NEWLINE)
        except Exception:
            client.close()
            break


def server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    sock.bind(('', PORT))
    sock.listen()

    while True:
        client, _ = sock.accept()
        thread = threading.Thread(target=generate, args=(client,))

        thread.start()


if __name__ == '__main__':
    server()

Running this server (as root, given port 19 is a privileged port), we can use netcat to test its functionality against the spec. Here we see the same pattern from above:

$ nc 127.1 19 | head -8
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn
()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno

After eight lines, head will terminate and netcat will close the connection. Without piping to head, the stream will continue until interrupted:

Voilà! Now we can, er, use this useful debugging and measurement tool, I guess.

This implementation is largely safe albeit not obviously useful. Its UDP counterpart is not safe however. Below we’ll explore how it works and how it was exploited.


The UDP flavor of CHARGEN is datagram-oriented. Like its TCP counterpart, this flavor listens on port 19 and responds with random data. Instead of a continuous stream of data, the specification specifies that the server must send an answering datagram containing a random amount of characters up to the length 512. The specification goes on to explain no history or state information is kept, so there is no continuity between one request and another.

This implementation is largely the same as the TCP server with some modifications for the protocol. This script will always return exactly 511 characters, including carriage returns and line feeds:

import socket
from itertools import count

BUFFER_SIZE = 4096
CHARS = [chr(i).encode('ascii') for i in range(32, 127)]
MAX = 511
NEWLINE = '\r\n'.encode('ascii')
PORT = 19
WIDTH = 72


def generate():
    lines = count(1)
    reply = b''

    while len(reply) < MAX:
        line = next(lines)

        for column in range(WIDTH):
            reply += CHARS[(line + column) % len(CHARS)]

        reply += NEWLINE

    return reply[:MAX]


def server():
    reply = generate()
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    sock.bind(('', PORT))

    while True:
        _, address = sock.recvfrom(BUFFER_SIZE)

        sock.sendto(reply, address)


if __name__ == '__main__':
    server()

This server returns a similar pattern, though a truncated version of it. Using netcat with -u to send UDP datagrams, it returns the barber’s pole:

$ echo | nc -u 127.1 19
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi

A similar implementation with nearly identical output. What makes this implementation unsafe?


Distributed Denial of Service attacks rely on compromised computer systems as sources of traffic. In the early internet, several flaws in the early protocols and design of networks could be used to generate large amounts of traffic against targets. An early attack, smurf2, relied on the design of broadcast IP traffic and ICMP echo and echo reply to overwhelm target networks. In this attack, a single forged ICMP request could result in a torrent of dozens or hundreds of replies to a single target.

UDP is a connection-less protocol that does not validate source IP addresses. Thus, a receiving server that doesn’t have any mechanism to authenticate the traffic will respond to whatever address is provided blindly. CHARGEN is especially appealing as an amplifier as a small request yields a relative large amount of data. In the example above, an UDP packet with a single carriage return yields a much larger response.

Using tcpdump, we can see the size difference:

$ sudo tcpdump port 19 -vv -X -c 1000 -i lo0
tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
16:45:19.501717 IP (tos 0x0, ttl 128, id 61881, offset 0, flags [none], proto UDP (17), length 29, bad cksum 0 (->4b14)!)
    localhost.63750 > localhost.chargen: [bad udp cksum 0xfe1c -> 0xfebf!] UDP, length 1
        0x0000:  4500 001d f1b9 0000 8011 0000 7f00 0001  E...............
        0x0010:  7f00 0001 f906 0013 0009 fe1c 0a         .............
16:45:19.501800 IP (tos 0x0, ttl 128, id 1604, offset 0, flags [none], proto UDP (17), length 541, bad cksum 0 (->348a)!)
    localhost.chargen > localhost.63750: [bad udp cksum 0x001d -> 0x5a95!] UDP, length 513
        0x0000:  4500 021d 0644 0000 8011 0000 7f00 0001  E....D..........
        0x0010:  7f00 0001 0013 f906 0209 001d 2122 2324  ............!"#$
        0x0020:  2526 2728 292a 2b2c 2d2e 2f30 3132 3334  %&'()*+,-./01234
        0x0030:  3536 3738 393a 3b3c 3d3e 3f40 4142 4344  56789:;<=>?@ABCD
        0x0040:  4546 4748 494a 4b4c 4d4e 4f50 5152 5354  EFGHIJKLMNOPQRST
        0x0050:  5556 5758 595a 5b5c 5d5e 5f60 6162 6364  UVWXYZ[\]^_`abcd
        0x0060:  6566 6768 0d0a 2223 2425 2627 2829 2a2b  efgh.."#$%&'()*+
        0x0070:  2c2d 2e2f 3031 3233 3435 3637 3839 3a3b  ,-./0123456789:;
        0x0080:  3c3d 3e3f 4041 4243 4445 4647 4849 4a4b  <=>?@ABCDEFGHIJK
        0x0090:  4c4d 4e4f 5051 5253 5455 5657 5859 5a5b  LMNOPQRSTUVWXYZ[
        0x00a0:  5c5d 5e5f 6061 6263 6465 6667 6869 0d0a  \]^_`abcdefghi..
        0x00b0:  2324 2526 2728 292a 2b2c 2d2e 2f30 3132  #$%&'()*+,-./012
        0x00c0:  3334 3536 3738 393a 3b3c 3d3e 3f40 4142  3456789:;<=>?@AB
        0x00d0:  4344 4546 4748 494a 4b4c 4d4e 4f50 5152  CDEFGHIJKLMNOPQR
        0x00e0:  5354 5556 5758 595a 5b5c 5d5e 5f60 6162  STUVWXYZ[\]^_`ab
        0x00f0:  6364 6566 6768 696a 0d0a 2425 2627 2829  cdefghij..$%&'()
        0x0100:  2a2b 2c2d 2e2f 3031 3233 3435 3637 3839  *+,-./0123456789
        0x0110:  3a3b 3c3d 3e3f 4041 4243 4445 4647 4849  :;<=>?@ABCDEFGHI
        0x0120:  4a4b 4c4d 4e4f 5051 5253 5455 5657 5859  JKLMNOPQRSTUVWXY
        0x0130:  5a5b 5c5d 5e5f 6061 6263 6465 6667 6869  Z[\]^_`abcdefghi
        0x0140:  6a6b 0d0a 2526 2728 292a 2b2c 2d2e 2f30  jk..%&'()*+,-./0
        0x0150:  3132 3334 3536 3738 393a 3b3c 3d3e 3f40  123456789:;<=>?@
        0x0160:  4142 4344 4546 4748 494a 4b4c 4d4e 4f50  ABCDEFGHIJKLMNOP
        0x0170:  5152 5354 5556 5758 595a 5b5c 5d5e 5f60  QRSTUVWXYZ[\]^_`
        0x0180:  6162 6364 6566 6768 696a 6b6c 0d0a 2627  abcdefghijkl..&'
        0x0190:  2829 2a2b 2c2d 2e2f 3031 3233 3435 3637  ()*+,-./01234567
        0x01a0:  3839 3a3b 3c3d 3e3f 4041 4243 4445 4647  89:;<=>?@ABCDEFG
        0x01b0:  4849 4a4b 4c4d 4e4f 5051 5253 5455 5657  HIJKLMNOPQRSTUVW
        0x01c0:  5859 5a5b 5c5d 5e5f 6061 6263 6465 6667  XYZ[\]^_`abcdefg
        0x01d0:  6869 6a6b 6c6d 0d0a 2728 292a 2b2c 2d2e  hijklm..'()*+,-.
        0x01e0:  2f30 3132 3334 3536 3738 393a 3b3c 3d3e  /0123456789:;<=>
        0x01f0:  3f40 4142 4344 4546 4748 494a 4b4c 4d4e  ?@ABCDEFGHIJKLMN
        0x0200:  4f50 5152 5354 5556 5758 595a 5b5c 5d5e  OPQRSTUVWXYZ[\]^
        0x0210:  5f60 6162 6364 6566 6768 690d 0a         _`abcdefghi..

The first packet above is the request. It weighs in at 29 bytes. A single byte is in the payload with 20 bytes for the IP header and 8 bytes for the UDP header. The response is quite a bit larger weighing in at 541 bytes. Just considering the payload, that’s a 500x amplification factor. An attacker could sent a flood of traffic by spoofing the source address to be the target to a CHARGEN server

These vulnerabilities lasted way past the adolescence of the internet. In 2014, CERT published alert TA14-017A with specific guidance to network administrators to find and disable these services. Today, it’s mostly old printers and weirdly implemented IoT things that still respond to this kind of traffic. Many routers and infrastructure providers have additional protections to ensure source addresses are plausible given the source of the traffic.


Much of the protocol designs from this era were written assuming a walled garden of mostly academic users. The networks were designed to carry research, and later commercialized without much revision to the original assumptions. Postel himself seemed to buck against the expansion of the internet. In 1998, months before his death, he hijacked eight of the root DNS servers as a test. While his motivations for this test aren’t clear, it’s surmised that he did this as a statement toward government and commercial incursions into his project.

The ideals and good intentions of Postel’s work and this era echo throughout the technical designs. The fact that you have to deal with spam is due to the fact that SMTP was written for a world of researchers mailing each to coordinate conferences or share data or math jokes or whatever, without any thought toward commercialization and the hoi polloi. Who cares if people can forge source addresses? Everybody has the best of intentions.


  1. Code from this post can be found in this gist. ↩︎

  2. In researching this, I discovered the author of smurf, Dan Moschuk, who went by the handle of TFreak, passed away suddenly at the age of 29 in 2010. This is sad to hear – Dan and I crossed path many times during our misspent youth of IRC on EFNet and stolen conference calls. He was bright, creative, and generally a good influence on the other misfits around him. As a tribute, his dates of birth and death were added to the FreeBSD calendar. A suitable ending, I think. ↩︎