Six 2011 posts are lists of 32-bit words. Every word is four Next(lo, hi) calls on one seeded System.Random, one call per byte.
Six of A858's 2011 posts are not GUID lists but lists of 8-hex-digit groups: 32-bit words, separated by spaces. The
community called them the "32 bit group" posts. Here is the start of iirqk
(201107062222):
Finding: the words are generated, not encrypted, and carry no message. Each word is four consecutive calls to
the standard .NET System.Random, one per byte, each with its own fixed range:
The Random is seeded with a number from 0 to 999, the current millisecond, exactly like the
mask digit of the GUID lists. Given the seed, every word of every one of the six posts comes out
exactly: 620 words, 2,480 bytes, no exceptions.
The site's build regenerates each list from its seed every time it runs; the table above is that result, not a
copy of the notebook's. "Calls before word 1" is the number of Random calls that were made, and thrown away,
before the first posted word (see below).
The byte ranges
The first thing that stands out in these posts is that every word starts with DA, DB, DC, DD or DE. The
other three bytes are restricted too, less visibly. Plotting every byte of every word by position shows four
hard-edged bands:
Every byte of all 620 words in the 6 posts, by position. The shaded band is the range of the Next(lo, hi) call for that byte; bars are how often each value occurs. Hover for counts.
Each band is uniformly filled and stops dead at both ends, which is what Next(lo, hi) produces: an integer from
lo up to, but not including, hi, all equally likely. The ends are not guesses from the observed minimum and
maximum. Each bound was varied by ±8 and only the values above make every word come out right; 0xFE and 0x79 in
particular are the only upper bounds that work, even though 0xFD and 0x78 are the largest values seen.
The ranges are the same in all six posts, from July to December 2011, so they are constants in the program, not
values it drew.
Finding the seed
System.Random(seed) is deterministic: the same seed always gives the same sequence. So the test is simple. For
each seed from 0 to 999, and each number of skipped calls from 0 to 3,000, generate words and compare them to the
post. Every post fits exactly one (seed, skip) pair, and fits it for every word.
A chance fit is not plausible. One word has 5 × 169 × 104 × 111 ≈ 9.75 million possible values, so a random seed
matches a single word with probability about 1 in 10 million; even the shortest list here (32 words) is
astronomically beyond chance.
The order of the four calls matters too. An earlier attempt at these posts had the word boundary two calls too
early, which made bytes 2 and 3 look like they did not fit. With the boundary in the right place, the byte order is
simply the call order.
The discarded calls
Before its first word, each post's Random has already been used a few times:
the four July and August 2011 posts each start after 2 calls, plus some whole words: none for iirqk, half a
word for iiafz (4 calls), one word for jqt3x (6 calls) and 599 words for ihwxg (2 + 4 × 599 = 2,398 calls);
the two December 2011 posts, mymne and n1ley, start after 1 call.
The two-call preamble is the same in all four 2011 posts, so it is part of the program: some value drawn before
the words, perhaps a size or format choice, or a random delay. What exactly it is cannot be told from two numbers
that appear nowhere in the posts.
The 599 skipped words of ihwxg say more. The program generated about 600 words on the same Random before the 256
it posted, and none of those 600 were ever posted. A fresh new Random() per post could not do that, so the
instance lived across several runs of the tool: seeded once, by the millisecond, when the tool started, and then
reused. The same pattern, a long-lived Random whose earlier draws are invisible, is what later lets the
count RNG identify A858's posting runs.
Two later variants
mymne (2011-12-03) is the same kind of word list, encrypted with the usual key of the
time, MD5(A858DE45F56D9BC9) in 3DES-ECB. Decrypted, it is 42 words from new Random(798).
n1ley (2011-12-05) is unencrypted and has two parts: 32 words from new Random(591), an
all-zero word 00000000, and then 32 masked GUIDs whose mask digits are new Random(320).Next(9). It is the only
post that contains both generators.
Neither the word counts (60, 50, 180, 256, 42, 32) nor the seeds carry any visible meaning. Like the GUID lists,
these posts are filler: the output of a random generator whose only real inputs are the time and the program's
constants.
Check it yourself
This script uses the same .NET Random port as the mask digit page, fetches a
post from Arctic Shift and regenerates it. Run it with no arguments for iirqk, or give the post, seed and number of
skipped calls from the table, e.g. ihwxg 393 2398.
import json, sys, urllib.request
def net_random(seed):
# .NET Framework System.Random(seed), enough for seeds 0..999
M = 2147483647
sa = [0] * 56
mj = 161803398 - seed
sa[55], mk = mj, 1
for i in range(1, 55):
ii = 21 * i % 55
sa[ii], mk, mj = mk, (mj - mk) % M, mk
for _ in range(4):
for i in range(1, 56):
sa[i] = (sa[i] - sa[1 + (i + 30) % 55]) % M
a, b = 0, 21
while True:
a = a % 55 + 1
b = b % 55 + 1
sa[a] = (sa[a] - sa[b]) % M
yield sa[a] / M
RANGES = [(0xDA, 0xDF), (0x0F, 0xB8), (0x96, 0xFE), (0x0A, 0x79)]
def words(seed, skip, n):
rng = net_random(seed)
for _ in range(skip):
next(rng)
# Random.Next(lo, hi) = lo + (int)(Sample() * (hi - lo))
return [''.join(f'{int(next(rng) * (hi - lo)) + lo:02X}' for lo, hi in RANGES) for _ in range(n)]
post, seed, skip = (sys.argv[1], int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else ('iirqk', 107, 2)
url = f'https://arctic-shift.photon-reddit.com/api/posts/ids?ids={post}&fields=selftext'
req = urllib.request.Request(url, headers={'User-Agent': 'a858-word-check'})
posted = json.load(urllib.request.urlopen(req))['data'][0]['selftext'].split()
made = words(seed, skip, len(posted))
print(len(posted), 'words; first three:', posted[:3], made[:3])
print('identical' if made == posted else 'different')
It works directly on the four unencrypted 2011 posts. For mymne, use the decrypted text from its JSON file on this
site; for n1ley, compare only the words before 00000000.