{
	"id": "08730e24-1940-4419-9e16-37542325a1ca",
	"created_at": "2026-04-06T00:15:39.246488Z",
	"updated_at": "2026-04-10T03:20:54.695912Z",
	"deleted_at": null,
	"sha1_hash": "643b8648ccf14f353a772321a44072803e278d20",
	"title": "From Zero To 50k Infections - PseudoManuscrypt Sinkholing - Part 1",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 774545,
	"plain_text": "From Zero To 50k Infections - PseudoManuscrypt Sinkholing -\r\nPart 1\r\nBy Stanislas Arnoud\r\nPublished: 2022-10-05 · Archived: 2026-04-05 20:30:50 UTC\r\nAt Bitsight we do a lot of malware sinkholing, and by late 2021 we started registering some DGA-like domains\r\nthat not only did not belong to any known domain generation algorithm (DGA) but were also being classified as\r\ndifferent types of malware like SmokeLoader, PrivateLoader, Socelars, and Redline. We registered more than 50\r\ndomains, all in the form [a-jm-r]{10}.com, and started investigating what we thought could be an unknown\r\nmalware family.\r\nIn this post we'll go through a technical analysis of this unknown malware, describing how we went from\r\nunknown DGA-like domains to sinkholing and emulating a fairly recent botnet that in the last 8 months has\r\ninfected nearly 500,000 machines (2.2M unique IPs) in total, across at least 40 countries, and has a current\r\nestimated botnet size of around 50,000 machines.\r\nThe process of tracing back a domain to a specific malware sample that generates traffic can vary; It can\r\nsometimes be as easy as opening the first result on a Google search for the domain, or as annoying as going\r\nthrough multiple search engines and ending on a Wayback Machine page that does not resolve. In this section\r\nwe'll detail how we traced a sample for this specific malware family, going from the domains and their traffic to a\r\nrecent family dubbed PseudoManuscrypt by Kaspersky.\r\nWe were confident that we had registered DGA domains given the pattern and traffic of the domains. We also\r\nknew that the domains were generating UDP traffic on port 53 (Figure 1), so we started by looking at\r\ncommunicating files from VirusTotal Intelligence (Figure 2) to get a better sense of the infection chain and\r\nhopefully reach a sample that would communicate with our sinkholes.\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 1 of 11\n\nFigure 1\r\nOur first impression was that there were a lot of files generating traffic to the DGA (Figure 2) and that many were\r\ninstallers or archives that contained detections for multiple families, explaining why our systems' classification\r\nwas also reporting different families for the set of domains.\r\nFigure 2.\r\nThe large amount of bundled communicating files makes it harder to track down the exact source of the traffic, so\r\nwe went through some trial and error, sending multiple files to a sandbox and searching both for traffic to the\r\nDGA domains, as well as the uncommon UDP traffic on port 53. While going through the sandbox's runs we\r\nfound this sample, which contains UDP traffic on port 53 to toa.mygametoa[.]com. By searching for this domain\r\non Google, we found a fairly recent (by the time of the research) article from Kaspersky mentioning a new\r\nmalware family: PseudoManuscrypt.\r\nThe article's description of how PseudoManuscrypt is distributed and communicates with the C2 matches what we\r\nwere seeing, and it filled the gap for some of our unknowns: the uncommon UDP traffic we were seeing was\r\nactually KCP over UDP, and the kill chain that led to the communication was originating from a .dat and .dll file.\r\nLooking back at this sample, we could see a call to rundll32.exe with a file ending in sqlite.dll. This file (together\r\nwith the .dat) was being downloaded by one of the bundled executables from t.gogamec[.]com (Figure 3).\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 2 of 11\n\nFigure 3.\r\nThe article gave us high confidence that we were seeing PseudoManuscrypt traffic to our sinkholes. There was,\r\nhowever, no mention of any Domain Generation Algorithm within the research.\r\nDistribution and Execution Flow\r\nIn the previous section we found the malware family that was generating the traffic we were observing, and we\r\nalso identified one of the distribution methods. In this section we'll dig further into the distribution and execution\r\nflow of PseudoManuscrypt, enabling us to easily obtain recent samples of it, as well as giving us a starting point\r\nfor a more in-depth analysis of the communication protocol.\r\nWe already know based on Kaspersky's work that there are multiple distribution methods for the\r\nPseudoManuscrypt family. For us it was obvious from the beginning that some of it is done from within archive\r\nfiles containing many other malware families, like Socelars, Smokeloader or Redline (hence our DGA domains\r\nbeing incorrectly classified), but during some parallel work we had on PrivateLoader, we noticed samples that\r\nincluded a hardcoded URL to fetch the main PseudoManuscrypt executable (Figure 4).\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 3 of 11\n\nFigure 4.\r\nWe do not know the purpose of the hardcoded URL list that PrivateLoader has, since we didn't see PrivateLoader\r\nsamples fetching those URLs, but the presence of this URL hints at the possibility of PrivateLoader distributing\r\nPseudoManuscrypt. At the time of writing the URL redirected to another domain (b.dxyzgame[.]com) that wasn't\r\nresolving to any IP, but in the past it was dropping this executable.\r\nBoth the previously mentioned executable and the executables bundled in the installers and archives show signs of\r\nbeing a dropper for the PseudoManuscrypt family. Their behavior coincides with Kaspersky's description as well\r\nas with what we've seen, where the executable downloads 2 files from a domain that, as far as we've seen, always\r\ncontains the \"game\" keyword (check IOCs for domain list).\r\nAs for the files that are downloaded:\r\nOne is a packed data file, usually pointed by the URI /X/sqlite.dat or X.html where we believe that X is the\r\ncampaign number.\r\nThe other file is a 32-bit Windows library, pointed by the URI /sqlite.dll or login.html.\r\nMost recent droppers write both files to %APPDATA%\\Local\\Temp with the names sqlite.dll and sqlite.dat, and\r\nthen run the following command:\r\nrundll32.exe \"C:\\Users\\X\\AppData\\Local\\Temp\\sqlite.dll\",global\r\nThe \"global\" export will locate the file named sqlite.dat in the same directory, unpack it in memory and then\r\nexecute it (Figure 5). The unpacking algorithm includes a rolling XOR, and a decompression routine. sqlite.dll\r\nruns the rolling XOR on sqlite.dat, and then jumps to the beginning of it. The decrypted shellcode will then XOR\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 4 of 11\n\nand decompress a part of itself using the LZNT1 algorithm. We've shared a Python script to unpack the .dat file\r\nhere.\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 5 of 11\n\nFigure 5.\r\nThis execution flow would suggest that the DLL that is downloaded simply behaves as the unpacker for the\r\npacked core component. This claim is backed by the fact the unpacker DLL is always identical, whereas the\r\npacked core changes frequently.\r\nWe close this section with a better understanding of the distribution and execution flow of this family (Figure 6),\r\nand with easy access to the core component that will enable us to better understand the communication protocol.\r\nFigure 6.\r\nIn this section we'll detail some of the analysis we did on the core component of PseudoManuscrypt. We won't\r\ndetail every aspect of the sample, as our main focus was on understanding the basics of the communication\r\nprotocol to create a custom sinkhole.\r\nHardcoded C2s and DGAs\r\nBy this point we had access to PseudoManuscrypt's core component, and our hypothesis was that there was a main\r\n(possibly hardcoded) C2 domain (toa.mygametoa[.]com) and a fallback DGA that would trigger on specific\r\nconditions. To quickly test our hypothesis, we manually ran the unpacker and the core, while also changing the\r\nhosts file to point toa.mygametoa[.]com to 127.0.0.1. This triggered the DGA (Figure 7), confirming our\r\nhypothesis.\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 6 of 11\n\nFigure 7.\r\nWe then started looking at the core component itself to find and reverse the DGA. Just by looking at the strings\r\ncontained in the core, we were able to identify two hardcoded C2s:\r\ntoa.mygametoa[.]com\r\ntob.mygametob[.]com\r\nBy looking for references to the hardcoded C2s, we identified that the client tries to connect to the first one, if it is\r\nnot able to communicate with it, the infected machine will fallback to the DGA. As for the second hardcoded\r\ndomain, our understanding is that it will be used only if the C2 server instructs the client to use it.\r\nThe DGA uses the hardcoded C2 and a key as seeds. The algorithm is as follows (Figure 8):\r\n1. The domain and key variables are concatenated with a \",\"\r\n2. The string is MD5 hashed\r\n3. 8 bytes are retrieved from the hash, starting at index 4 and ending at position 12\r\n4. For the first 10 hexadecimal values of the retrieved bytes:\r\n1. If it's a number, 0x31 is added (outputs \"a\" to \"j\")\r\n2. If an uppercase letter, 0x2c is added (outputs \"m\" to \"r\")\r\n5. \".com\" is appended to the resulting string\r\nFor every call to the DGA algorithm, the previously generated domain is used as the first argument, while the\r\nsecond argument is always the same string. This makes the domains dependent on the previously generated\r\ndomain, making a circular group of all possible domains.\r\nA Python implementation of this DGA is available here.\r\nFigure 8.\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 7 of 11\n\nThe infected client will try to connect to each of the sequentially generated domains, and everytime it fails it will\r\ngenerate a new one.\r\nGiven the circular characteristics of the DGA and the small size of the domains, we generated all possible domains\r\nfor both the hardcoded C2s. Here you can see a list with all possible 585,723 unique domains for the\r\ntoa.mygametoa[.]com C2, and here is a list with all possible 812,811 unique domains for the\r\ntob.mygametob[.]com C2.\r\nIn order to collect infection telemetry from infected machines, we needed to implement the protocol the malware\r\nuses.\r\nCommunication protocol\r\nFrom Kaspersky's research and our own, we know that this family communicates using KCP over UDP, and TCP\r\nas a fallback. The KCP protocol has been designed as a replacement for TCP, and its specification can be found on\r\nGitHub. PseudoManuscrypt's code of KCP is very similar to the linked repository, suggesting that they might be\r\nusing that specific implementation.\r\nAlthough the communication protocol uses UDP port 53 and TCP port 443, they implement their own messaging\r\nprotocol. We focused on the first exchange between the client and the server, which allowed us to map\r\nconnections from the domains into infections, and also extract a lot of information about the infected machines.\r\nThis custom protocol can be divided into 2 layers. The first layer (L1) contains the message to be sent, while the\r\nsecond layer (L2) contains metadata about the message. Both layers are independent of each other.\r\nL2 has the following format:\r\nStruct L2\r\n{\r\nBYTE header;\r\nBYTE compression_type;\r\nint32 L2_size;\r\nint32 L1_size;\r\nBYTE L1[L2_size - 10];\r\n}\r\nWhere:\r\n`header` byte is always 0x43\r\n`compression_type` can be one of the following:\r\n0x0F -\u003e no compression, no encryption\r\n0x1F -\u003e no compression, L1 xored with 0x88\r\n0x2F -\u003e zlib compression, no encryption\r\n0x3F -\u003e zlib compression, then L1 xored with 0x88\r\n0x4F -\u003e RtlCompressBuffer compression, no encryption\r\n0x5F -\u003e RtlCompressBuffer compression then L1 xored with 0x88\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 8 of 11\n\n`L2_size` is the size of L2, which contains both the metadata and the compressed L1\r\n`L1_size` is the real size of L1\r\n`L1` is an array with L1 content\r\nThe L1 format will vary based on the type of received message. We believe that the generic format is the\r\nfollowing:\r\nStruct L1\r\n{\r\nBYTE message_type;\r\n... varies based on message_type\r\n}\r\nFirst message\r\nWhen the infected machine first communicates with the C2, it sends a lot of information about the computer. In all\r\nsamples we found, the `compression_type` was always 0x3F (zlib compression, then 1-byte-xor with 0x88) for the\r\nL1 format. The following is what we believe is the L1 format for this initial message:\r\nstruct L1_layer\r\n{\r\nBYTE message_type;\r\nBYTE padding1[3];\r\nint32 campaign;\r\nchar client_id[33];\r\nBYTE padding2[3];\r\nint32 major_winver;\r\nint32 minor_winver;\r\nint32 build_number;\r\nint32 platform_id;\r\nint32 is_intel_amd;\r\nint32 is_running_wow64;\r\nint32 winserv_version; // Not working (always 0)\r\nint32 is_winserv;\r\nBYTE padding3[256];\r\nint16 number_of_proc;\r\nBYTE padding4[2];\r\nint32 proc_freq_mhz;\r\nint32 mem_size_mb;\r\nchar hostname[50];\r\nBYTE padding5[2];\r\nint32 ms_needed_to_connect;\r\nBYTE tbd2[100];\r\nwchar client_release_date[9];\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 9 of 11\n\nBYTE padding6[182];\r\nwchar client_version[9];\r\nBYTE padding7[22];\r\nint32 has_a_camera;\r\nchar internal_ip_port[94];\r\nint16 port;\r\nint32 tickcount;\r\nint32 pid;\r\nint32 sz_firmware_table;\r\nBYTE firmware_table[sz_firmware_table];\r\n}\r\nMost of the fields are self-explanatory, we'd just like to detail that:\r\n`message_type` is 0x99 for this first message\r\n`paddingX` fields are always null\r\n`campaign` always matches the number that is seen in the URL from there the sqlite.dat was downloaded\r\nfrom\r\n`client_id` is the MD5 sum of the substructure containing the fields `sz_firmware_table` and\r\n`firmware_table`\r\n`client_release_date` and `client_version` are dates and are almost always the value, with the exception that\r\n`client_version` has a `v` in the beginning.\r\nFirst response\r\nOnce the server receives the first message, it can send various types of commands to execute on the system, like:\r\nrunning a binary, clearing event logs, modifying registry keys, etc. The C2 usually responds with a sleep\r\ncommand of 3600 seconds (`message_type` 0x00), unless the `client_version` field in the first message isn't the\r\nmost recent one, and if so the C2 replies with a \"binary update\" (`message_type` 0x01) command, and sends the\r\nmost recent version of PseudoManuscrypt's core.\r\nWe did not investigate the communication protocol any further, as at this point we had enough information to be\r\nable to identify and sinkhole PseudoManuscrypt infections.\r\nWith the previous work we developed our own custom sinkhole for this family. We've been tracking it for over 8\r\nmonths, seeing a daily botnet size of around 16,000 machines until the end of August, where a new version with a\r\nnew C2 was pushed (consequently a new DGA) and the daily numbers dropped to around 7k.\r\nRoughly 5 days before the new version was pushed, we saw a sudden increase in infections that peaked at around\r\n51,500 unique client IDs (Figure 9). We hypothesize that the hardcoded C2 could have had an issue, making the\r\nbots fallback to the DGA, and consequently communicate with our sinkhole.\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 10 of 11\n\nFigure 9.\r\nAs PseudoManuscrypt seems to be a recent, but fairly large botnet, in a future post we'll detail some insights about\r\nthe telemetry we're getting from sinkholing the DGA domains. We'll also detail the botnet's evolution and its bots.\r\nVT Graph\r\nsqlite.dll dd19804b5823cf2cab3afe4a386b427d9016e2673e82e0f030e4cff74ef73ce1 sqlite.dat\r\necdfa028928da8df647ece7e7037bc4d492b82ff1870cc05cf982449f2c41786\r\ntoa.mygametoa[.]com\r\ntob.mygametob[.]com\r\nb.dxyzgame[.]com\r\n56.jpgamehome[.]com\r\ngp.gamebuy768[.]com\r\nv.xyzgamev[.]com\r\nc.xyzgamec[.]com\r\nxv.yxzgamen[.]com\r\ng.agametog[.]com\r\nSource: https://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nhttps://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.bitsight.com/blog/zero-50k-infections-pseudomanuscrypt-sinkholing-part-1"
	],
	"report_names": [
		"zero-50k-infections-pseudomanuscrypt-sinkholing-part-1"
	],
	"threat_actors": [],
	"ts_created_at": 1775434539,
	"ts_updated_at": 1775791254,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/643b8648ccf14f353a772321a44072803e278d20.pdf",
		"text": "https://archive.orkl.eu/643b8648ccf14f353a772321a44072803e278d20.txt",
		"img": "https://archive.orkl.eu/643b8648ccf14f353a772321a44072803e278d20.jpg"
	}
}