A858DE45F56D9BC9 archive

The mask digit

The "version" digit of every GUID in A858's lists is C#'s Random seeded with the current millisecond.

Most of A858's posts, both the plain lists from July 2011 and the inner layer of the 2012–2013 encrypted posts, are lists of GUIDs in a slightly odd format. A858 itself hinted at this format in the PostAnalyzer code (posted as hex in ihicx):

if (raw.Substring(13, 1) == "4")
    return root.DecryptRaw(raw);
else
    return root.DeMD5(raw);

Finding: the odd digit is the output of the standard .NET System.Random, seeded with a number from 0 to 999 (the current millisecond). This explains every masked GUID list in the corpus.

3,499posts with a known generator seed
3,493re-checked by this site against the GUID lists it publishes
2runs, with two seeds, in one post (if4fl)

The other 6 are lists of 32-bit words rather than GUIDs (ihwxg, iiafz, iirqk, jqt3x, mymne, n1ley; see the 32-bit word posts), so their seed drives Next(lo, hi) calls, not a mask digit.

The format

A GUID is 16 bytes, usually written as 32 hex characters. The first character of the third group is the version, and the first character of the fourth group is the variant:

              v version (normally 4 for random GUIDs)
79faefbf-730a-0520-81f7-652183d4500b
                   ^ variant (8, 9, a or b)

In A858's lists the variant is always 8, 9, a or b, like a normal GUID. The version is not 4, though: it is a digit from 0 to 8, spread evenly. Every other character looks uniformly random. These are ordinary random (v4) GUIDs, as Guid.NewGuid() makes them, whose version digit has been overwritten. That digit is the mask.

The mask digits come from Random.Next(9)

Take all the GUIDs in one post and read off the version digit of each. For ifxpi (201107031600):

79faefbf-730a-0520-81f7-652183d4500b -> 0
101fa8b6-ef51-4c2c-b3a7-18dfdd76b981 -> 4
827d17ea-0a37-18a2-990d-461317c56b16 -> 1
3b33f45d-2784-0621-8a98-a1bd7bd93f08 -> 0
01298ccc-8734-1a15-a304-fb07025dba26 -> 1
...

The whole post is 80 GUIDs, so 80 digits:

04101007154885713683710134010462115554510158685657525768386507667266825716264134

This is exactly what new Random(527) gives when asked for 80 numbers from 0 to 8:

var random = new Random(527);
var str = new StringBuilder();
for (var i = 0; i < 80; ++i)
    str.Append(random.Next(9));
Console.WriteLine(str);
// 04101007154885713683710134010462115554510158685657525768386507667266825716264134

This is not a coincidence: the chance that a random 80-digit string matches one of 1,000 seeds is about 1 in 1073. The same holds for:

Every post page on this site shows its mask digits with the seed that reproduces them, and the site build re-checks each seed against the decrypted list.

How it was found

Turning each decrypted inner-layer post (3,464 posts from 2012-06-26 to 2013-10-08, with more than four GUIDs) into a digit string like this, the strings share their beginnings and never shift, so a shorter post is a prefix of a longer one. That is what you would expect if each post is one stream from a generator with about 1,000 possible seeds. These posts use 966 distinct seeds; by the birthday problem, 3,464 draws from 1,000 seeds gives about 969. A858 used .NET (the PostAnalyzer source, and the .NET 4 executable in 14oru6), so the obvious generator was the first one to try.

0255075100125seeds 0–24: 112 posts0seeds 25–49: 83 postsseeds 50–74: 84 postsseeds 75–99: 86 postsseeds 100–124: 75 posts100seeds 125–149: 93 postsseeds 150–174: 78 postsseeds 175–199: 90 postsseeds 200–224: 83 posts200seeds 225–249: 80 postsseeds 250–274: 82 postsseeds 275–299: 89 postsseeds 300–324: 83 posts300seeds 325–349: 87 postsseeds 350–374: 70 postsseeds 375–399: 91 postsseeds 400–424: 68 posts400seeds 425–449: 73 postsseeds 450–474: 82 postsseeds 475–499: 65 postsseeds 500–524: 93 posts500seeds 525–549: 80 postsseeds 550–574: 88 postsseeds 575–599: 90 postsseeds 600–624: 97 posts600seeds 625–649: 89 postsseeds 650–674: 114 postsseeds 675–699: 91 postsseeds 700–724: 83 posts700seeds 725–749: 88 postsseeds 750–774: 86 postsseeds 775–799: 87 postsseeds 800–824: 98 posts800seeds 825–849: 95 postsseeds 850–874: 98 postsseeds 875–899: 88 postsseeds 900–924: 92 posts900seeds 925–949: 85 postsseeds 950–974: 112 postsseeds 975–999: 90 posts
Distribution of the 3,498 known mask seeds, in bins of 25: flat over 0–999, as expected of a millisecond.

The seed is the current millisecond

The seeds are spread evenly over 0–999 and do not correlate with the title, length or anything else in the post. But in a steady run of posts, the seed changes smoothly from one post to the next. In February 2013 (run 42) A858 posted about every 66 minutes, and the seed goes down by roughly 50 each time:

PostTime (UTC)GUIDsMask seed
195ea82013-02-24 20:51:00605
195iwq2013-02-24 21:57:0048896
195nos2013-02-24 23:03:0040851
195s2t2013-02-25 00:09:0060791
195wlp2013-02-25 01:15:0056746
19612s2013-02-25 02:21:0044679
1965mm2013-02-25 03:27:0248635
196a8p2013-02-25 04:33:0036589
196eed2013-02-25 05:39:0048529
196i1f2013-02-25 06:44:5944471
196kup2013-02-25 07:50:5932426
196n122013-02-25 08:56:5940364

(It wraps from 5 to 896 at the start, since it is mod 1000.) Across all the 2013 runs, the median change is about −55 per hour, −142 over 3 hours and −310 over 6 hours. That is what you get from new Random(DateTime.Now.Millisecond) inside a timer that fires on a fixed interval and runs slightly fast (about 14 ppm): the millisecond it fires at slowly drifts backwards. The same drift is visible in the post times, which slip from :00 to :59 during a run.

The generator is therefore roughly:

var random = new Random(DateTime.Now.Millisecond);
for (var i = 0; i < count; ++i)
{
    var guid = Guid.NewGuid();
    // replace the version digit with random.Next(9)
    ...
}

What this means

The mask digit carries no message. It is a timestamp's millisecond fed through an RNG. The other 30 characters of each GUID are Guid.NewGuid() output, which comes from the Windows crypto RNG and cannot be predicted or reconstructed. So these lists, including the ~3,500 decrypted inner layers people spent years on, are filler. That fits with 3j99nf (2015-09-01): "The treasure is amoung the trash."

The same generator also explains the six 2011 "32-bit group" posts (iirqk, iiafz, jqt3x, ihwxg, mymne, n1ley). Each 4-byte word there is four Next(lo, hi) calls in a row from one millisecond-seeded Random, with a fixed range for each byte: see the 32-bit word posts.

The number of GUIDs per post comes from a second, long-lived Random: see the count RNG.

Check it yourself

This Python script ports the .NET Framework Random (enough for seeds 0–999), fetches a post from Arctic Shift, reads off the version digits and tries all 1,000 seeds. It works directly on the plain posts (ifa54, ifhho, ifhif, ifxpi); for encrypted ones, use the decrypted list from the post's JSON file on this site.

import json, re, 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

def mask_digits(seed, n):
    rng = net_random(seed)
    return ''.join(str(int(next(rng) * 9)) for _ in range(n))

post = sys.argv[1] if len(sys.argv) > 1 else 'ifxpi'
url = f'https://arctic-shift.photon-reddit.com/api/posts/ids?ids={post}&fields=selftext'
req = urllib.request.Request(url, headers={'User-Agent': 'a858-seed-check'})
text = json.load(urllib.request.urlopen(req))['data'][0]['selftext']
hexstr = re.sub(r'[^0-9a-fA-F]', '', text).lower()
guids = [hexstr[i:i + 32] for i in range(0, len(hexstr) - 31, 32)]
digits = ''.join(g[12] for g in guids)
print(len(guids), 'GUIDs, mask digits:', digits)
print('seeds:', [s for s in range(1000) if mask_digits(s, len(digits)) == digits])

Two runs in one post

if4fl (201107021123) was long the one masked list that no seed fits. Its 40 GUIDs are posted as two blocks, 19 and 21, separated by a blank line, and the blank line is the answer: each block is a separate run of the generator with its own Random. The first block's mask digits are new Random(360).Next(9) and the second's are new Random(318).Next(9). Read as one list, the 40 digits mix two sequences, so no single seed can fit.

Both runs skip one draw before the first mask digit, while the other lists start at the first draw. What that draw was for is not known. To check it with the script above, split the post at the blank line, take mask_digits(seed, n + 1)[1:] and compare.