{
	"id": "5fb0602a-0247-4c87-bfd4-ae7e6037b88a",
	"created_at": "2026-04-10T03:20:40.534289Z",
	"updated_at": "2026-04-10T13:11:33.727053Z",
	"deleted_at": null,
	"sha1_hash": "e8de982b5060cbb1d85042ce95c0f7437ab8901e",
	"title": "The Faulty Precursor of Pykspa's DGA",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 77776,
	"plain_text": "The Faulty Precursor of Pykspa's DGA\r\nArchived: 2026-04-10 02:30:05 UTC\r\nPyskpa is a worm that spreads over Skype. The malware has been relying on a domain generation algorithm\r\n(DGA) to contact its command and control targets since at least October 2013. Even though the C2 infrastructure\r\nseems to be long abandoned, there are still many infected clients. Virustracker, who has been tracking Pyskpa\r\nsince March, shows that the DGA is still used by well over 50`000 infected clients. You can find a description of\r\nthe underlying DGA here.\r\nA few days ago, Daniel Plohmann at Fraunhofer FKIE discovered new Pyskpa domains within the ShadowServer\r\nfeeds. He kindly provided me the sample, from which I reversed the algorithm behind the newly found Pykspa\r\ndomains. This short post first shows the algorithm, then examines its properties in comparison with the other DGA\r\nversion.\r\nThe characteristics and spread of the emerged DGA variant lead me to believe that it is the predecessor of the\r\nother version. As shown later, the algorithm behaves in strange ways that were probably not intended by the\r\nmalware authors. I therefore refer to the DGA in this post as the Precursor DGA, and call the other DGA the\r\nImproved DGA.\r\nThe Precursor DGA\r\nThe precursor DGA generates sets of 5000 distinct hostnames. It is seeded with the current unix timestamp\r\ndivided by 172800, which corresponds to a granularity of two days. Here’s an implementation of the DGA in\r\nPython:\r\nfrom datetime import datetime\r\nimport argparse\r\nfrom time import mktime\r\ndef get_sld(sld_len, r):\r\n a = sld_len ** 2\r\n sld = \"\"\r\n for i in range(sld_len):\r\n x = i*(r % 4567 + r % 19) \u0026 0xFFFFFFFF\r\n y = r % 123456\r\n z = r % 5\r\n p = (r*(z + y + x)) \u0026 0xFFFFFFFF\r\n ind = (a + p) \u0026 0xFFFFFFFF\r\n sld += chr(ord('a') + ind % 26)\r\n r = (r + i) \u0026 0xFFFFFFFF\r\n r = r \u003e\u003e (((i**2) \u0026 0xFF) \u0026 31 )\r\n a += sld_len\r\n a \u0026= 0xFFFFFFFF\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 1 of 13\n\nreturn sld\r\ndef dga(seed, nr_domains = 5000):\r\n tlds = [\"biz\", \"com\", \"net\", \"org\", \"info\", \"cc\"]\r\n r = seed\r\n for domain_nr in range(nr_domains):\r\n r = int(r ** 2) \u0026 0xFFFFFFFF\r\n r += domain_nr\r\n r \u0026= 0xFFFFFFFF\r\n domain_length = (r % 10) + 6\r\n sld = get_sld(domain_length, r)\r\n tld = tlds[r % 6]\r\n domain = \"{}.{}\".format(sld, tld)\r\n print(domain)\r\ndef generate_domains(date, nr):\r\n unix_timestamp = mktime(date.timetuple())\r\n seed = int(unix_timestamp // (2*24*3600) )\r\n date_range = []\r\n for i in range(2):\r\n ts = (seed+i)*2*24*3600\r\n date_range.append(datetime.fromtimestamp(ts).strftime(\"%Y-%m-%d %H:%M\"))\r\n t = \"pykspa domains valid through {} - {}\".format(*date_range)\r\n print(\"{}\\n{}\".format(t, \"*\"*len(t)))\r\n dga(seed, nr)\r\nif __name__==\"__main__\":\r\n parser = argparse.ArgumentParser()\r\n parser.add_argument(\"-d\", \"--date\", help=\"date for which to generate domains\")\r\n parser.add_argument(\"-n\", \"--nr\", help=\"nr of domains to generate\", type=int, default=5000)\r\n args = parser.parse_args()\r\n if args.date:\r\n d = datetime.strptime(args.date, \"%Y-%m-%d\")\r\n else:\r\n d = datetime.now()\r\n generate_domains(d, args.nr)\r\nFor example, these are the 20 first domains active at 2015-07-19 00:00:\r\n./dga.py -n 20 -d 2015-07-19\r\npykspa domains valid through 2015-07-18 02:00 - 2015-07-20 02:00\r\n****************************************************************\r\nkmambodsholapet.com\r\nsiaiheiq.biz\r\noagsesiugkeq.net\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 2 of 13\n\njshbzafox.org\r\nxfawpafox.cc\r\nkspongeoya.net\r\ncmvccqeoya.info\r\nsbtrssdsholapet.com\r\naumarenansnan.cc\r\nyeuwwiiugkeq.biz\r\naolmbo.info\r\ndxnydafox.cc\r\nqmzmtufqbex.org\r\nskxsiyeoya.net\r\nichnvyeoya.info\r\nlopevkn.com\r\nzjrckkn.com\r\noitykueoya.net\r\nmegsoeiq.net\r\nzfdthsn.cc\r\nProperties and Comparison with the Improved DGA\r\nThe following table compares some the properties of the precursor DGA to the improved DGA.\r\nPrecursor DGA Improved DGA\r\nseeding\r\nGranularity is 2 days. Seed corresponds to divided\r\ntimestamp:\r\nseed = int(unix_timestamp // (2*24*3600))\r\nGranularity of 20 days. Seed corresponds to divided\r\ntimestamp, passed through a cryptographic function:\r\nindex = int(unix_timestamp//(20*3600*24))\r\nseed = some_cryptographic_function(index)\r\nnoise domains\r\nno noise domains\r\nInterleaves the usable domains with noisy domains\r\nthat are generated by the same DGA, but with an\r\nunpredictable seed.\r\nnr domains per run\r\n5000 200 usable domains + 800 noisy domains\r\nnext random number\r\nUses repeated squaring, which lets random number\r\nconverge to two values.\r\nUses increasingly larger increments. No convergence.\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 3 of 13\n\nPrecursor DGA Improved DGA\r\nr = int(r ** 2) (mod 2^32)\r\nr += domain_nr (mod 2^32)\r\nr += (r % (domain_nr + 1) + 1) (mod 2^32)\r\nnext second level domain length\r\nLength of second level domain is between 6 and 15\r\ncharacters:\r\ndomain_length = (r % 10) + 6\r\nLength of second level domain is between 6 and 12\r\ncharacters:\r\ndomain_length = ((r + domain_nr) % 7) + 6\r\nsecond level domain algorithm (all calculations mod 2^32)\r\nRandom number is right shifted after each letter,\r\nleading to convergence at the end of domain names.\r\na = sld_len ** 2\r\nsld = \"\"\r\nfor i in range(sld_len):\r\n index = (a + (r*(r % 5 + r % 123456 +\r\n i*(r % 4567 + r % 19))) +\r\n i*(r % 4567 + r % 19)))) % 26\r\n a += sld_len\r\n r = (r + i)\r\n r = r \u003e\u003e (((i**2) \u0026 0xFF) \u0026 31 )\r\n sld += chr(ord('a') + index)\r\nNo visible randomness decay for the later letters in\r\ndomains, otherwise very similiar.\r\na = sld_len ** 2\r\nsld = \"\"\r\nmodulo = 541 * sld_len + 4\r\nfor i in range(sld_len):\r\n index = (a + (r*((r % 5) + (r % 123456) +\r\n i*((r \u0026 1) + (r % 4567))) )) % 26\r\n a += sld_len;\r\n r += (((7837632 * r * sld_len) ) +\r\n 82344) % modulo;\r\n sld += chr(ord('a') + index)\r\ntop level domain algorithm\r\nRandom pick from 6 top level domains:\r\ntlds = [\"biz\", \"com\", \"net\", \"org\",\r\n \"info\", \"cc\"]\r\ntld = tlds[r % 6]\r\n \r\nRandom pick from 4 top level domains, although 5\r\ndomains are hardcoded.\r\ntlds = ['com', 'net', 'org', 'info', 'cc']\r\ntld = tlds[r % 4]\r\nBoth DGAs are very similar. The precursor DGA has a defective random number generators though:\r\n1. Within the main loop: changing the random number for each domains; the problem lies with r = int(r **\r\n2) .\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 4 of 13\n\n2. Within the code to generate the second level domains: changing the random number for each letter; the\r\nculprit here is r \u003e\u003e (i**2) .\r\nThe defects cause a drastic loss of randomness the more random numbers are generated. For example, here are\r\nsome of the domains whose second level domain has 15 characters:\r\nkmambodsholapet.com\r\nsbtrssdsholapet.com\r\nsrjukodsholapet.org\r\nizbqukdsholapet.com\r\nybdetodsholapet.com\r\nmgesbsdsholapet.org\r\nuafudkdsholapet.cc\r\nwrsxuodsholapet.com\r\nycokbodsholapet.org\r\nckocgkdsholapet.org\r\nyndccsdsholapet.cc\r\nslwcnodsholapet.cc\r\nanljvkdsholapet.cc\r\noznevadsholapet.org\r\newgxsodsholapet.cc\r\nwigiakdsholapet.com\r\nmvomnkdsholapet.com\r\nwvmqfodsholapet.cc\r\nWhile the beginning of the domain is pretty divers, after the sixth letter always follows “dsholapet”. This is true\r\nfor all 15 letter domains, making for a pretty solid network detection rule.\r\nThe other defect, i.e., repeatedly squaring the random number, is arguably even worse. It causes the random\r\nnumber to converge to one of two values, depending on the sign of the initial seed. Although the malware authors\r\nprobably wanted to have a fresh set of 5000 domains every two days, the convergence causes all but the first 19\r\ndomains to be very consistent, only ever alternating between the same two sets of domains. The following image\r\nillustrates this behavior:\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 5 of 13\n\nEvery saturated color represents a new, yet unseen domain. The desaturated colors stand for revisited domains. As\r\nexpected, every second day reuses the domains of the previous day, in accordance with the granularity of 2 days.\r\nThe first 9 domains change after 48 hours, as desired. The later domains, however, increasingly revisit older\r\ndomains, up to the point where no new domains are generated.\r\nThe next picture evaluates the domains over one year in increments of four days. A blue square represents a new\r\ndomain, while a grey square represents a revisited domain. Clearly all domains after 19 stay the same. But already\r\nthe second domain now and then reuses an older domain:\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 6 of 13\n\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 7 of 13\n\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 8 of 13\n\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 9 of 13\n\nConclusion\r\nThe precursor DGA is very similiar to the improved version, yet suffers from bad random number generators. The\r\nDGA still got deployed in the wild, as the following screenshot of Virustracker shows:\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 10 of 13\n\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 11 of 13\n\nHowever, the number of hits is only about 600, compared to over 60'000 of the improved DGA:\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 12 of 13\n\nSource: https://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nhttps://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/\r\nPage 13 of 13",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.johannesbader.ch/2015/07/pykspas-inferior-dga-version/"
	],
	"report_names": [
		"pykspas-inferior-dga-version"
	],
	"threat_actors": [],
	"ts_created_at": 1775791240,
	"ts_updated_at": 1775826693,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/e8de982b5060cbb1d85042ce95c0f7437ab8901e.pdf",
		"text": "https://archive.orkl.eu/e8de982b5060cbb1d85042ce95c0f7437ab8901e.txt",
		"img": "https://archive.orkl.eu/e8de982b5060cbb1d85042ce95c0f7437ab8901e.jpg"
	}
}