Cracking the Agrippa Code

In 1992, cyberpunk author William Gibson published a 300-line poem on a 3.5″ floppy disk, titled Agrippa (A Book of the Dead). The poem was stored as a Macintosh program that, when run, would display the poem and then irreversibly encrypt itself, destroying the stored poem.

In 2012, I became interested in unlocking the encryption of Agrippa through the Agrippa Challenge, which enlisted the Internet to figure out the program. Here’s what I figured out.

The Agrippa program was developed in Macintosh Common Lisp, a dialect of Lisp augmented with routines to interface with the classic Macintosh OS of that era. Only the compiled binary was provided, and the source code has been lost to time.

On The Agrippa Files, a site dedicated to collecting information and resources related to Agrippa, there is an earlier draft version of the program’s source code:

Agrippa prototype code, page 3

Agrippa prototype code, page 5

The encryption algorithm there is a very straightforward in-house “encryption” algorithm that encodes data in 3-byte blocks. The design is clearly ad-hoc, and it does not reflect any known public cryptosystem; in particular, the reference to DES has nothing to do with the actual DES cryptosystem (the un-make-des function more closely resembles the RSA algorithm). This algorithm encodes data in 3-byte blocks. First, the 8 bits in each byte are permuted through a 8-position permutation (do-it), then the bits are split into two 12-bit integers (by taking the low 4 bits of the second byte and the 8 bits of the first byte as the first 12-bit integer, and the 8 bits of the third byte and the 4 high bits of the second integer as the second 12-bit integer). Each is individually encrypted by taking them to the 3491th power, mod 4097; the bits are then reassembled into 3 bytes.
Side note: 3491 appears nowhere in the code, but it is the exponent which reverses the action of taking the 11th power, mod 4097 (the implementation of un-make-des); that is, taking a number to the 3491th power mod 4097, then to the 11th power, again mod 4097, will result in the original number (mod 4097) for most integers. (This doesn’t work for numbers divisible by 17 or 241; evidently, the developer never ran into such values when using his encryption system as they would otherwise corrupt the text).
The 8 bits of the middle byte are permuted through a different 8-position permutation (do-too-it) and the three bytes are output as encoded bytes. The big block of data is the whole poem encoded with this algorithm and printed as MacRoman characters (with escapes, e.g. 0x5c, the slash character, is output as \\). Sadly, a combination of font and printing limitations means that the printout is not enough to directly decode the poem: several characters are unprintable (and so are “invisible” in the printout), there is no way to reliably distinguish tabs (0x09) from spaces (0x20), and some characters have been printed incorrectly by the font (e.g. the e in the repetitive " e\\" sequences should actually be an e with a circumflex (ê), but the printout doesn’t show this). Consequently, the poem can’t be completely recovered from the printout, but I am confident that the analysis is correct.
Note that the design of this algorithm ensures that any particular (aligned) block of 3 characters will encrypt the same way, since each block of 3 characters is independently encrypted. Thus, it exhibits a number of cryptographic weaknesses, primarily exhibited as regular patterns due to repeated text in the original plaintext.
A few sample encryptions:
The encryption of three consecutive spaces is " ê\" (quotes added for clarity); since repeated spaces appear very often (e.g. before each chapter marker), this sequence can be seen repeated often.

The encryption of

tonight red lanterns are battered,

laughing,
in the mechanism.  

(the last few lines of the poem) is ΣÑÀQ$£¿Ì[ıƒ^ü…ïÓ êÊyMIÇ…~/ôì≈^Èür˙Q¯(ÀA}|yÙ¥3W+£¶ò∆©«ò\ which again matches the last few characters in the data block of the Lisp program listing.


Next, for the actual Agrippa program itself. I ran the programs in an emulator (Mini vMac), having no access to suitably old hardware myself.
The first major observation I made was that the application was compiled with a version of Macintosh Allegro Common Lisp dating back to 1989, possibly version 1.2.2 or 1.2.3. The application contains a bug which allows users to enter the Lisp REPL, a fully-featured interactive environment in which the code and data of the program can be directly manipulated. To exploit this bug, all you have to do is press some keys on the keyboard when the very first (empty) window appears. The bug is caused by a missing keyboard entry handler in that window; hitting keys on the keyboard should trigger the missing handler and result in an error. Upon any errors, the Lisp interpreter is configured to start its REPL:
Exploiting this bug allows us to examine the program’s execution in much more detail than was previously possible. First, to get a mouse cursor, enter (_ShowCursor) at the prompt (with the parens and the underscore). Editing becomes easier after that. Typing (lisp-implementation-type) gives “Macintosh Allegro Common Lisp”, as expected. To get a list of *every* variable defined, type (apropos-list ""). This takes a long time to run – here’s what that output looks like:
You can see some unusual names at the end, like shoot-gun, *mingzi*, *about-agri*, etc. (note that Lisp is not case-sensitive with its symbol or variable names). In fact, all variables after *TRACE-OUTPUT* are variables from the Agrippa program. Examining the Lisp code from the Agrippa Files, we see that many of the variable names are the same, but not all are. This immediately suggests that some variables were renamed, added or removed during the process of software development, implying that the Lisp listing was for an earlier pre-release version of the program.
The function UN-ROLL-ZI is suspicious, as it mirrors the name of un-roll-the-text in the original Lisp listing (in fact, “zi” means “words” in Chinese; a few variable names are named in languages other than English). However, the zi variable is not currently available. If you start the program again (from a fresh disk image, i.e. “for the first time”), and press keys when the poem window appears, typing in zi at the REPL will yield the complete, original text of the poem.
However, we are interested in establishing whether the encryption algorithm used to store zi in the Lisp program is the same as the one used in the older Lisp listing. In our original REPL, it turns out that the variable cl has the value 3, the same as the cell-length variable in the original listing. If we set cl to some other value, we can skip the decryption routine, allowing us to see the original “encrypted” data. I set cl to 10000 (larger than the poem itself), then used (continue). Agrippa dutifully begins to scroll a mountain of encrypted gibberish:
and we can see the telltale " ê\" sequence in this screenshot. I can actually confirm that the decryption algorithm used in the Agrippa binary is functionally identical to the one implemented in the Lisp listing, i.e. that part of the program did not change between that listing and the final shipped version of the program. Therefore, the major mystery of “what encryption algorithm was used” has been solved.
There are a few more things to resolve:
  1. Scanning the binary for these encrypted strings turns up absolutely nothing. The reason is that the Macintosh Common Lisp compiler compresses the main program code into the executable, then fishes it out and decompresses it during startup. So, string searching will not turn up anything. The best bet if you want to get the encrypted mass of text is to use Linux ckpt or a similar tool to get a memory dump of Mini vMac after the Agrippa program has been loaded. The compression is not in any way a form of encryption, so we won’t discuss it in depth.
  2. What’s that mass of “encrypted” text that gets printed out at the end? From my observations I can tell it contains around 40 unique (printable) symbols, far short of the 96 one would expect to see for a properly encrypted piece of text (as an example, the original encrypted text has a lot more unique symbols compared to the garbage shown at the end). From experimentation, I suspect that the text is generated by applying the decryption algorithm again to the plain text to reduce it to gibberish. Here’s an example of that output:

    Later, I worked out that the plaintext poem is simply fed through the un-waymute-it (upp-it) function line-by-line, character-by-character, which effectively implements a simple substitution cipher over the characters. The characters are thus modified to become unrecognizable, but the encryption is trivially reversed by putting the text through waymute-it. Unfortunately, due to the limitations of the text display in the program, it is impossible to precisely determine the ciphertext (since some characters are replaced with boxes, and others are missing entirely); thus, “decrypting” the ciphertext that scrolls at the end is impossible to do precisely. However, since it’s a simple substitution cipher, the characters that are readable can be used to reconstruct at least part of the text.

  3. Why doesn’t the program run again the second time? This one’s pretty easy: the program corrupts itself at startup. Originally (as indicated in the Lisp program listing), the plan was to just write 40000 0xff bytes to the binary (“trash”), which would inevitably render the program “corrupt”. At some point, someone clearly thought it would be more artistic to use a “genetic code”, so the final version writes 6000 randomly-chosen (but fixed) As, Cs, Gs and Ts to a particular spot in the application file. This corrupts several routines in the program, preventing it from starting or running normally (or, depending on the computer used, freezing or crashing the whole system). Note that the genetic code has a codon entropy of 5.97 bits/codon, much higher than any natural DNA sequence known (they top out at around 4-5 bits/codon), and a BLAST nucleotide search turned up no results. It is therefore likely that it is randomly selected (or matched against an artificial DNA sequence, e.g. the sequence shown in the book).

Finally, I wrote some code to assist me in my reverse engineering efforts. It implements all the encryption and decryption routines, and further provides a demonstration decryption of the readable parts of the final encrypted text scroll (point #2 above).

# -*- coding: MacRoman -*-

pmq = 4097 # p-q
em2 = 11 # e-2
little = 16
lot = 256
cell_length = 3

def make_des(x): # reverse-engineered from un_make_des
    return pow(x, 3491, 4097) # 3491*11 = 1 mod 3840 (4097 = 17*241, 3840 = 16*240)

def un_make_des(x):
    return pow(x, em2, pmq)

def do_it(x): # reverse-engineered from un_do_it
    return (7-(x-5)*3) % 8

def un_do_it(x):
    return (3*(7-x)+5) % 8

def do_too_it(x): # reverse-engineered from un_do_too_it
    return (13-5*x) % 8

def un_do_too_it(x):
    return (5*(13-x)) % 8

def gen_permute(x, f):
    new = 0
    for temp in xrange(8):
        if x%2 == 1:
            new += 2 ** f(temp)
        x //= 2
    return new

def permute_it(x):
    return gen_permute(x, do_it)

def waymute_it(x):
    return gen_permute(x, do_too_it)

def un_permute_it(x):
    return gen_permute(x, un_do_it)
    '''
    new = 0
    for temp in xrange(8):
        if x%2 == 1:
            new += 2 ** un_do_it(temp)
        x //= 2
    return new
    '''

def un_waymute_it(x):
    return gen_permute(x, un_do_too_it)
    '''
    new = 0
    for temp in xrange(8):
        if x%2 == 1:
            new += 2 ** un_do_too_it(temp)
        x //= 2
    return new
    '''

def un_happenin(x1, y1, z1):
    this_num = un_make_des(x1 + (y1 % little) * lot)
    that_num = un_make_des((z1 * little) + (y1 // little))
    return (
        this_num % lot,
        (this_num // lot) + (that_num % little) * little,
        that_num // little
    )

def happenin(x1, y1, z1): # reverse-engineered from un_happenin
    this_num = make_des(x1 + (y1 % little) * lot)
    that_num = make_des((z1 * little) + (y1 // little))
    return (
        this_num % lot,
        (this_num // lot) + (that_num % little) * little,
        that_num // little
    )

def roll_the_text(the_text, wend=None): # reverse-engineered from roll_the_text
    '''
    Encrypt a piece of text with the "Agrippa cipher".
    Its length should be a multiple of 3.
    '''
    # default argument
    if wend is None:
        wend = len(the_text)

    num_cells = wend / cell_length

    new_text = []

    for rip_off in range(num_cells):
        c1 = chr(permute_it(ord(the_text[3*rip_off])))
        c2 = chr(permute_it(ord(the_text[3*rip_off+1])))
        c3 = chr(permute_it(ord(the_text[3*rip_off+2])))

        temp1, temp2, temp3 = happenin(ord(c1), ord(c2), ord(c3))
        new_text.append(chr(temp1))
        new_text.append(chr(waymute_it(temp2)))
        new_text.append(chr(temp3))

    return ''.join(new_text)

def un_roll_the_text(the_text, wend=None):
    '''
    Decrypt a piece of text using the "Agrippa cipher".
    Its length should be a multiple of 3.
    '''
    # default argument
    if wend is None:
        wend = len(the_text)

    num_cells = wend / cell_length

    new_text = []

    for rip_off in range(num_cells):
        c1 = the_text[3*rip_off]
        c2 = chr(un_waymute_it(ord(the_text[3*rip_off+1])))
        c3 = the_text[3*rip_off+2]

        temp1, temp2, temp3 = un_happenin(ord(c1), ord(c2), ord(c3))
        new_text.append(chr(un_permute_it(temp1)))
        new_text.append(chr(un_permute_it(temp2)))
        new_text.append(chr(un_permute_it(temp3)))

    return ''.join(new_text)

# "zi" variable dumped from vMac memory image
zi = '''
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
'''.decode('base64')

#print roll_the_text('   ').decode('mac-roman')
#print '-----'
#print roll_the_text('\rtonight red lanterns are battered,\r\rlaughing,\rin the mechanism.  ').decode('mac-roman')
#print '-----'
poem = un_roll_the_text(zi)
print poem.replace('\r', '\n').decode('mac-roman')

print '========================ENCRYPTED TEXT FOLLOWS======================'

enclines = [''.join(chr(un_waymute_it(ord(c))) for c in line) for line in poem.split('\r')]
delchars = '\x00\x02\x03\x04\x05' # invisible
boxchars = '\x01\x06\x07\x08\x0a\x0b\x0c\x0d\x0e\x0f' + ''.join(chr(i) for i in range(16, 32)) # these appear as boxes
box = u'\u25AF'
for page_start in range(19, len(enclines) - len(enclines) % 17, 17):
    print
    for line in enclines[page_start:page_start+17]:
        dispenc = line
        dispenc = dispenc.decode('mac-roman')
        for c in delchars:
            dispenc = dispenc.replace(c, '')
        for c in boxchars:
            dispenc = dispenc.replace(c, box)
        print dispenc
    print '------------------------[PAGE BREAK]----------------------------'

print '====================PARTIAL DECRYPTION========================'
ciphertext = '''•ù;•âã•©•ã••üπã9••ã••ù;•9••ãâ•;üèã©••ùõ••ù•;üô©•õ9•)••©ãúüª©çü;©
®•ãù•••;•ù•èã
∫ïòï•ö••;üù••9ï
•ùâ•;üèã©••ùõÖ••üèè•Ö
#'1°

®•ãù••ã•õç´ãâ•••;••üâ•••)9•ù©;•âüªù
•ù╪9ü©ã•´ùâã9•©•ãè
•ù••••ç•á畕㕪••©ã•)ãù••ç5
•(•)•ì;•;•ª•è•ççÖ••´õï•#'#'ï•'''
lines = ciphertext.split('\n')
for line in lines:
    print ''.join(chr(waymute_it(ord(c))) for c in line).decode('mac-roman')

This program prints out several pages of ciphertext. You can compare the generated encrypted text with the text that is scrolled during the last 25 seconds of the emulated run of Agrippa to see that the implemented encryption routine matches the original. Note that the last line of ciphertext on each “page” is not visible due to the limited height of the text display window. Finally, the script prints out a partial decryption of the first page of ciphertext based on what is visible (boxes have been replaced with • characters, which decrypt to <).