{
	"id": "39b01084-49b1-4944-933a-9974289b618a",
	"created_at": "2026-04-06T00:16:22.429879Z",
	"updated_at": "2026-04-10T03:35:59.506852Z",
	"deleted_at": null,
	"sha1_hash": "0501179af18cbaaaa6ebb09fd0f9998a17d726f5",
	"title": "Nymaim revisited",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1143873,
	"plain_text": "Nymaim revisited\r\nArchived: 2026-04-05 13:15:28 UTC\r\nIntroduction\r\nNymaim was discovered in 2013. At that time it was only a dropper used to distribute TorrentLocker. In\r\nFebruary 2016 it became popular again after incorporating leaked ISFB code, dubbed Goznym. This\r\nincarnation of Nymaim was interesting for us because it gained banking capabilities and became a serious\r\nthreat in Poland. Because of this, we researched it in depth and we were able to track Nymaim activities since\r\nthen.\r\nHowever a lot of things have changed during the last two months. Most notably, Avalanche fast-flux network\r\n(which was central to Nymaim operations) was taken down and that struck a serious blow to Nymaim activity.\r\nFor two weeks everything went silent and even today Nymaim is a shadow of its former self. Although it’s\r\nstill active in Germany (with new injects), we haven’t observed any serious recent activity in Poland.\r\nObfuscation\r\nThis topic is really well researched by other teams, but it’s still interesting enough to be worth mentioning.\r\nNymaim is heavily obfuscated with a custom obfuscator – to the point that analysis is almost impossible. For\r\nexample typical code after obfuscation looks like this:\r\njz loc_4381B4\r\nxchg eac, [ebp-0Ch]\r\npush 053h\r\ncall sub_408D02\r\npush 050h\r\ncall sub_408D02\r\npush edx\r\npush 8AB4BF9EH\r\npush 754A35C1H\r\ncall sub_41CF77\r\nmov eax, 8CBFB5FFh\r\ncall sub_43AFBD\r\nmov ecx, [ebp-0Ch]\r\ncmp [ecx], ax\r\njnz loc_4381B4\r\nBut with some effort we can make sense of it. There are a lot of obfuscation techniques used, so we’ll cover\r\nthem one by one:\r\nFirst of all, registers are usually not pushed directly onto the stack, but helper function “push_cpu_register” is\r\nused. For example push_cpu_register(0x53) is equivalent to pushing ebx and push_cpu_register(0x50) is\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 1 of 19\n\nequivalent to pushing eax. Constants are not always the same, but registers are always in the same order\r\n(standard x86 ordering).\r\n. register constant\r\n0 eax 0x50\r\n1 ecx 0x51\r\n2 ebx 0x52\r\n3 edx 0x53\r\n4 esp 0x54\r\n5 ebp 0x55\r\n6 esi 0x56\r\n7 edi 0x57\r\nAdditionally, most constants in code gets obfuscated too – for example mov eax, 25 can be changed to:\r\nmov eax, 0x8CBFB5FF\r\ncall xor_eax_with_8CBFB5DA\r\nThe constant used in the example is 8CBFB5DA, but there’s nothing special about it – it’s a random dword\r\nvalue, generated just for the purpose of obfuscating this constant. The only thing that matters is the result of\r\nthe operation (0x25 in this case).\r\nAdditionally there other similar obfuscating functions are used sometimes – for example sub_*_from_eax and\r\nadd_*_to_eax.\r\nLast but not least, the control flow is heavily obfuscated. There are a lot of control flow obfuscation methods\r\nused, but all boil down to simple transformation – call X and jmp X are transformed to at least two pushes.\r\nThis obfuscation is in fact very similar to previous one – instead of jumping to 0x42424242, malware calls\r\nfunction detour with two parameters: 0x40404040 and 0x02020202. The detour adds it’s parameters and\r\njumps to the result. In pseudoasm instead of:\r\nwe have:\r\npush 0x40404040\r\npush 0x02020202\r\njmp detour\r\ndetour:\r\npop eax ; oversimplification, a detour can never spoil registers\r\npop ebx\r\nadd eax, ebx ; or xor, or sub, or add\r\njmp eax\r\nThere exists also a slight variation of this method – instead of pushing two constants, sometimes only one\r\nconstant is pushed and machine code after a call opcode is used instead of a second constant (detour uses\r\nreturn address as a pointer to the second constant).\r\nTo sum up, previously pasted obfuscated code should be read like this:\r\njz loc_4381B4\r\nxchg eac, [ebp-0Ch]\r\npush 053h\r\ncall push_cpu_register ; push ebx\r\npush 050h\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 2 of 19\n\ncall push_cpu_register ; push eax\r\npush edx\r\npush 8AB4BF9Eh\r\npush 754A35C1h\r\ncall detour_1 ; call f(8AB4BF9Eh, 754A35C1h)\r\nmov eax, 8CBFB5FFh\r\ncall xor_eax_const_4 ; eax ^= 8CBFB5DAh\r\nmov ecx, [ebp-0Ch]\r\ncmp [ecx], ax\r\njnz loc_4381B4\r\nWith this in mind, we created our own deobfuscator. This was quite a long time ago and since then other\r\nsolutions have shown up. Our deobfuscator probably isn’t the best, but is easily modifiable for our needs and\r\nit has some unique (as far as we know) features that we need, for example it imports recovery and decrypting\r\nencrypted strings stored in binary. Other deobfuscators include mynaim and ida-patchwork Nevertheless, with\r\nour deobfuscator we are able to untangle that messy code to something manageable:\r\njz loc_4381B4\r\nxchg eac, [ebp-0Ch]\r\n; nops\r\npush ebx\r\n; nops\r\npush eax\r\ncall sub_428b51\r\n; nops\r\nmov eax, 25h\r\nmov ecx, [ebp-0Ch]\r\ncmp [ecx], ax\r\njnz loc_4381B4\r\nWhen it comes to Nymaim obfuscation capabilities it’s not nearly over. For example external functions are not\r\ncalled directly, instead of it an elaborate wrapper is used:\r\nThis wrapper pushes hash of function name on the stack and jumps to the next dispatcher (even though call\r\nopcode is used, this code never returns here):\r\nA second dispatcher pushes hash of a dll name on the stack and jumps to the helper function:\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 3 of 19\n\nAnd finally real dispatcher is executed:\r\nAdditionally, real return address from API is obfuscated – return address is set to call ebx somewhere in the\r\nntdll (real return address is somewhere in ebx by then, of course). Most tools are very confused by it. Let’s\r\njust say, it’s very frustrating when debugging and/or single stepping.\r\nBut wait, there’s more! As we have seen, short constants are obfuscated with simple mathematical operations,\r\nbut what about longer constants, for example strings? Fear not, malware authors have a solution for that too.\r\nAlmost every constant used in the program is stored in a special data section. When Nymaim needs to use one\r\nof that constants, it is using special encrypted_memcpy function. At heart it is not very complicated:\r\nvoid encrypted_memcpy(char *to, char *from, int len) {\r\nif (is_in_encrypted_section(to)) {\r\nif (is_in_encrypted_section(from)) {\r\nmemcpy(to, from, len);\r\n} else {\r\nmemcpy_and_encrypt(to, from, len);\r\n}\r\n} else {\r\nif (is_in_encrypted_section(from)) {\r\nmemcpy_and_decrypt(to, from, len);\r\n} else {\r\nmemcpy(to, from, len);\r\n}\r\n}\r\n}\r\nInner workings of memcpy_and_decrypt are not that complicated either. Our reimplementation of the\r\nencryption algorithm in Python is only few lines long:\r\ndef nymaim_decrypt(self, raw, from_raw, length):\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 4 of 19\n\nfrom_va = from_raw + self.image_base\r\nxsize = from_va - self.off\r\ncur_key = self.key\r\nif xsize \u003c 0:\r\nraise RuntimeError(\"raw too small - min is \" + hex(self.off - self.image_base))\r\nfor _ in range(xsize / 4):\r\ncur_key = (cur_key + self.xstep) \u0026 0xffffffff\r\nr = ''\r\nlength = min(length, len(raw) - from_raw)\r\nfor i in range(length):\r\nr += chr(raw[from_raw + i] ^ (ror(cur_key, (xsize \u0026 3) * 8) \u0026 0xff))\r\nxsize += 1\r\nif xsize % 4 == 0:\r\ncur_key = (cur_key + self.xstep) \u0026 0xffffffff\r\nreturn r\r\nWe only need to extract constants used for the encryption (they differ between executables) – they are hidden\r\nin these portions of code:\r\n(These functions are not obfuscated, so extraction can be done with simple pattern matching).\r\nBut encryption of every constant was not good enough. Malware authors decided that they can do better than\r\nthat – why don’t encrypt the code too? That’s not very often used, but few critical functions are stored\r\nencrypted and decrypted just before calling. Quite an unusual approach, that’s for sure. Ok, let’s leave\r\nobfuscation at that.\r\nStatic Configuration\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 5 of 19\n\nAfter deobfuscation, the code is easier to analyze and we can get to interesting things. First of all, we’d like to\r\nextract static configuration from binaries, especially things like:\r\nC\u0026C addresses\r\nDGA hashes\r\nEncryption keys\r\nMalware version\r\nOther stuff needed for communication\r\nHow hard can that be? Turns out that harder than it looks – because this information is not just stored in the\r\nencrypted data section.\r\nFortunately, this time the encryption algorithm is rather simple.\r\ndef nymaim_config_crypt(self, mem, ndx):\r\n\"\"\"decrypt final config (read keys and length and decrypt raw data)\"\"\"\r\nkey0 = mem.dword(ndx)\r\nkey1 = mem.dword(ndx+4)\r\nlen = mem.dword(ndx+8)\r\nraw = mem.read(ndx + 12, len)\r\nprev_chr = 0\r\nresult = ''\r\nfor i, c in enumerate(raw):\r\nbl = ((key0 \u0026 0x000000FF) + prev_chr) \u0026 0xFF\r\nkey0 = (key0 \u0026 0xFFFFFF00) + bl\r\nprev_chr = ord(c) ^ bl\r\nresult += chr(prev_chr)\r\nkey0 = (key0 + key1) \u0026 0xFFFFFFFF\r\nkey0 = ((key0 \u0026 0x00FFFFFF) \u003c\u003c 8) + ((key0 \u0026 0xFF000000) \u003e\u003e 24)\r\nreturn result\r\nWe just need to point nymaim_config_crypt to the start of encrypted static config and everything will just\r\nwork.\r\nHow do we know where static config starts? Well… We tried few clever approaches (matching code, etc), but\r\nthey weren’t reliable enough for us. Finally, we solved this problem with a simplest possible solution – we just\r\ntry every possible index in binary and try to decrypt from there. This may sound dumb (and it is), but with few\r\ntrivial heuristics (static config won’t take 3 bytes of space, neither will it take 3 megabytes) this is quite fast –\r\nless than 1s on typical binary – and works every time.\r\nDespite this, after decrypting static config we get a structure, which is is quite nice and easy to parse. It\r\nconsists of multiple consecutive “chunks”, each with assigned type, length and data (for those familiar with\r\nfile formats, this is something very similar to PNG, or wav, or any other RIFF).\r\nstruct chunk {\r\nuint32_t type;\r\nuint32_t length;\r\nchar data[chunk_length];\r\n}\r\nGraphically this looks like this:\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 6 of 19\n\nAnd chunks are laid consecutive in static config block:\r\nSo we can quickly traverse through all chunks of a static config with a simple five-liner:\r\ndef parse_static_config(blob):\r\ni = 0\r\nwhile i \u003c len(blob):\r\nchunk_type = blob[i:i+4] # chunk type, also called \"hash\" or \"chunk hash\" in this article\r\nchunk_len = from_uint32(blob[i+4:i+8])\r\nchunk_content = blob[i+8:i+8+chunk_len]\r\nprocess_chunk(chunk_type, chunk_content) # this function should process every type of chunk\r\ni += 8 + chunk_len\r\nSnippet from process_chunk (hash == chunk_type):\r\nif hash == self.CFG_URL: # '48c2026b':\r\nparsed['urls'] += [{'url': append_http(x)} for x in filter(None, map(get_domainc, raw.split(';')))]\r\nelif hash == self.CFG_DGA_HASH: # 'd9aea02a':\r\nparsed['dga_hash'] = [uint32(h) for h in chunks(raw, 4)]\r\nelif hash == self.CFG_DOMAINS: # '095d4b1d':\r\nparsed['domains'] += map(lambda x: {'cnc': x}, filter(None, map(get_domainc, raw.split(';'))))\r\nelif hash == self.CFG_ENC_KEY: # '510be622':\r\nparsed['encryption_key'] = raw\r\n...\r\nAfter initial parsing the static config looks like this:\r\n(By the way, in this article chunk types are usually represented byte-order, i.e. big endian)\r\nAnd in a more human readable form with most interesting chunks interpreted:\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 7 of 19\n\nInfection timeline\r\nThere is more than one “kind” of Nymaims. As of now we distinguish between three kinds:\r\ndropper – first Nymaim that gets executed on the system. This is the only type distributed\r\ndirectly to victims.\r\npayload – module responsible for most of the “real work” – web injects for example\r\nbot_peer – module responsible for P2P communication. It tries to become supernode in the\r\nbotnet.\r\nThese are all one kind of malware and all of them share the same codebase, except few specialized functions.\r\nFor example our static config extractor works on all of them, just like our deobfuscator and they all use the\r\nsame network protocol.\r\nDropper role is simple. It performs few sanity checks – for example:\r\nMakes sure that it’s not virtualized or incubated\r\nCompares current date to “expiration time” from static config\r\nChecks that DNS works as it should (by trying to resolve microsoft.com and google.com)\r\nIf something isn’t right, the dropper shuts down and the infection doesn’t happen.\r\nThe second check is especially annoying, because if you want to infect yourself Nymaim has to be really\r\n“fresh” – older executables won’t work. Even if you override check in the binary, this is also validated server-side and the payload won’t be downloaded.\r\nIf we want to connect to a Nymaim instance, we need to know the IP address of peer/C\u0026C. Static config\r\ncontains (among others) two interesting pieces of information:\r\nDNS server (virtually always it’s 8.8.8.8 and 8.8.4.4).\r\nC\u0026C domain name (for example ejdqzkd.com or sjzmvclevg.com).\r\nNymaim is resolving that domain, but returned A records are not real C\u0026C addresses – they are used in\r\nanother algorithm to get a real IP address. We won’t reproduce that code here, but there is a great article from\r\nTalos on that topic. If someone is interested only in the DGA code, it can be found here:\r\nhttps://github.com/vrtadmin/goznym/blob/master/DGA_release.py\r\nWhen dropper obtains C\u0026C address, it starts real communication. It downloads two important binaries and a\r\nlot more:\r\npayload – banker module (responsible for web injects – passive member of botnet)\r\noptional bot module (it is trying to open ports on a router and become an active part of a botnet.\r\nWhen it fails to do so, it removes itself from a system).\r\nfew additional malicious binaries (VNC, password stealers, etc – not very interesting for us).\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 8 of 19\n\nDGA\r\nPayload is very different from dropper when it comes to network communication:\r\nNo hardcoded domain\r\nBut has DGA\r\nAnd P2P\r\nThe payload’s DGA algorithm is really simple – characters are generated one by one with simple pseudo-random function (variation of xorshift). Initial state of DGA depends only on seed (stored in static config) and\r\nthe current date, so we can easily predict it for any given binary. Additionally, researchers from Talos have\r\nbruteforced valid seeds, simplifying the task of domain prediction even more.\r\ndef dga_single(self, state):\r\nname = ''\r\nlen = self.getbyte(state, 8) + 5\r\nfor i in range(len):\r\nr = self.getbyte(state, 0xFFFFFFFF)\r\nc = self.getbyte(state, 26) + 0x61\r\nname += chr(c)\r\nn = 0\r\nwhile n == 0:\r\nn = self.getbyte(state, 5)\r\nname += '.' + [0, 'net', 'com', 'in', 'pw'][n]\r\nreturn name\r\ndef getbyte(self, state, param):\r\ntemp0 = ((state[0] \u003c\u003c 11) ^ state[0]) \u0026 0xFFFFFFFF\r\ntemp2 = state[2]\r\nstate[0] = (state[0] + state[1]) \u0026 0xFFFFFFFF\r\nstate[1] = (state[1] + state[2]) \u0026 0xFFFFFFFF\r\nstate[2] = (state[2] + state[3]) \u0026 0xFFFFFFFF\r\nstate[3] = ((state[3] \u003e\u003e 19) ^ state[3] ^ temp0 ^ (temp0 \u003e\u003e 8)) \u0026 0xFFFFFFFF\r\nreturn (((state[3] + temp2) \u0026 0xFFFFFFFF) % (param * 100)) / 100\r\ndef __init__(self, seed, date):\r\narg8 = seed + date.day + (date.year \u003c\u003c 9) + (date.month \u003c\u003c 5)\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 9 of 19\n\nstate = [0] * 4\r\nstate[0] = (arg8 + seed) \u0026 0xFFFFFFFF\r\nstate[1] = ror(state[0] * 2, 4)\r\nstate[2] = ror(bswap(state[1]), 0xE) + seed\r\nstate[3] = ror(state[2] + state[1], 0x12)\r\nfor i in range(16):\r\nnext_byte = self.getbyte(state, 0xFFFFFFFF)\r\ndword_ndx, byte_ndx = i / 4, i % 4\r\nbyte_mask = 0xFF \u003c\u003c (byte_ndx * 8)\r\nstate[dword_ndx] = (state[dword_ndx] \u0026 ~byte_mask) |\r\n((next_byte \u0026 0xFF) \u003c\u003c (byte_ndx * 8))\r\nself.state = state\r\nP2P\r\nFirst of all, few examples why we suspected from the start that there is something else besides DGA:\r\nWe have taken one of our binaries that hadn’t behaved like the payload, unpacked it, deobfuscated and reverse\r\nengineered it. But even without in-depth analysis, we’ve found a lot of hints that P2P may be happening. For\r\nexample we can find strings typical for adding exception to Windows Firewall (and of course – that’s what\r\nmalware did when executed on a real machine).\r\n╰─$ strings decrypted_nymaim | grep -E \"#!#|Firewall\"\r\n#!#*|Action=Allow|#*\r\n#!#*|Action=Block|#*\r\n#!#*|Active=TRUE|#*\r\n#!#*|Active=FALSE|#*\r\n#!#*|Dir=In|#*\r\n#!#*|Dir=Out|#*\r\n#!#*|Profile=Private|#*\r\n#!#*|Profile=Public|#*\r\n#!#*|LPort=#*\r\n#!#*|RPort=#*\r\n\\Registry\\Machine\\SYSTEM\\ControlSet001\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\StandardProfile\\AuthorizedApplications\\List\r\n\\Registry\\Machine\\SYSTEM\\ControlSet001\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\FirewallRules\r\nAnother suspicious behavior is opening ports on a router with help of UPNP. Because of this, infected devices\r\nfrom around the world can connect to it directly.\r\n╰─$ strings decrypted_nymaim | grep -E \"PortMap|upnp\"\r\nDeletePortMapping\r\nurn:schemas-upnp-org:service:WANPPPConnection:1\r\nurn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nGetSpecificPortMappingEntry\r\nupnp:rootdevice\r\nAddPortMapping\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 10 of 19\n\nAddAnyPortMapping\r\nurn:schemas-upnp-org:service:WANIPConnection:1\r\nNewPortMappingDescription\r\nAnd finally something even more outstanding. As we have seen, the malware presents itself as the Nginx in\r\nthe “Server” header. Where does this header come from? Directly from the binary:\r\n╰─$ strings decrypted_nymaim | grep -E \"nginx\" -B 4\r\nHTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: %u\r\nContent-Type: application/octet-stream\r\nServer: nginx/1.9.4\r\nWe implemented tracker for the botnet (more about that later) and with the data we obtained, we concluded\r\nthat this probably is a single botnet, but with geolocated injects (for example Polish and US injects are very\r\nsimilar). Distribution of IPs we found is similar to what other researchers have determined (we have found\r\nmore PL nodes and less US than others, but that’s probably because the botnet is geolocated and we were\r\nmore focused on Poland).\r\n49.9% (~7.5k) of found supernodes were in Poland, 30% (~4.5k) in Germany and 15.7% (~2.2k) in the US.\r\nNetwork protocol\r\nAnd now for something more technical. This is an example of a typical Nymaim request (P2P and C2\r\ncommunication use the same protocol internally):\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 11 of 19\n\nHost header is taken from the static config\r\nRandomized POST variable name and path\r\nPOST variable value = encrypted request (base64 encoded)\r\nUser-Agent and rest of the headers are generated by WinHTTP (so headers are not very unique\r\nand it’s impossible to detect Nymaim network requests by using only them).\r\nTypical response:\r\nThis isn’t really Nginx, just pretending.\r\nEverything except the data section is hardcoded\r\nData = encrypted request\r\nEncrypted messages have very specific format:\r\nA lower nibble of the first byte is equal to a length of the salt and a lower nibble of the second byte is equal to\r\nthe length of the padding. Everything between the salt and the padding is the encrypted message. To decrypt\r\nit, we need to concatenate the key with the salt – and use that password with the rc4 algorithm.\r\nIt can be easily decrypted using Python (but we had to reverse engineer that algorithm first):\r\ndef nymaim_decrypt(key, raw_bytes):\r\nnibble0 = raw_bytes[0] \u0026 0xF\r\nnibble1 = raw_bytes[1] \u0026 0xF\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 12 of 19\n\nsalt = raw_bytes[2:2+nibble0]\r\npassword = key + salt\r\ndata = raw_bytes[2+nibble0:len(raw_bytes)-nibble1]\r\ndecrypted = rc4_decrypt(password, body)\r\ndecrypted_len = struct.unpack('\u0026lt;I', decrypted[:4])[0]\r\nassert decrypted_len == len(decrypted - 4)\r\nreturn decrypted\r\nAfter decrypting a message, we get something with a format very similar to the static config (i.e. a sequence\r\nof consecutive chunks):\r\nEach chunk has its type, length and raw data:\r\nWe can process decrypted message with almost exactly the same code as code for static config:\r\ndef parse_message(blob):\r\ni = 0\r\nwhile i \u003c len(blob):\r\nchunk_type = blob[i:i+4]\r\nchunk_len = from_uint32(blob[i+4:i+8])\r\nchunk_content = blob[i+8:i+8+chunk_len]\r\nprocess_chunk(chunk_type, chunk_content)\r\ni += 8 + chunk_len\r\nAnd this is the basic code used for parsing the message. Each chunk type needs to be processed a bit\r\ndifferently. Interestingly, parsing message is recursive, because some chunk types can contain other lists of\r\nchunks, which in turn can contain other lists of chunks, etc. Unfortunately, important chunks have another\r\nlayer of encryption and compression. At the end of an encrypted chunk we can find special RSA encrypted (or\r\nrather – signed) header. After decryption (unsigning) of the header, we can recover a md5 hash and length of\r\nthe decrypted data and most important of all – a Serpent cipher key used to encrypt the data.\r\nAfter the decryption we will stumble upon another packing method – decrypted data is compressed with\r\nAPLIB32. This structure is very similar to the one used by ISFB – firstly we have magic ‘ARCH’, then length\r\nof compressed data, length of uncompressed data and crc32 – all of them are dwords (4 bytes).\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 13 of 19\n\nAgain, it’s nothing Python can’t deal with. We quickly hacked this function to recover real data hidden\r\nunderneath:\r\ndef inner_decrypt(raw, rsa_key):\r\nencrypted_header, encrypted_data = raw[-0x40:], raw[:-0x40]\r\ndecrypted_data = rsa_decrypt(encrypted_header, rsa_key)\r\nmd5 = decrypted_data[0:16]\r\nblob = decrypted_data[16:32]\r\nlength = from_uint32(decrypted_data[32:36])\r\nserpent_decrypted = crypto.s_decrypt(encrypted_data, blob)[:length]\r\nassert md5 == hashlib.md5(serpent_decrypted).digest()\r\nreturn serpent_decrypted\r\nWith this function we finally managed to hit the jackpot. We decrypted all of the interesting artifacts passed\r\nover the wire, most importantly additional downloaded binaries, web filters and injects.\r\nCommunication\r\nAn example request, after dissection, may look like this:\r\n\u003cd2bf6f4a\u003e \u003e\u003e\u003e [+] [ 62 bytes]:\r\nstate information:\r\ndata field 0: 0x263\r\ndata field 1: 0x23426908\r\ndata field 2: 0x0\r\ndata field 3: 0x0 \u003c- injects version\r\ndata field 4: 0x0\r\ndata field 5: 0x0\r\ndata field 6: 0x0 \u003c- webfilters version\r\ndata field 7: 0x0\r\ndata field 8: 0x0\r\nbody: 846372/573,0,0,0,0/0/0/0/2 \u003c- version of downloaded binaries\r\n\u003cffd5e56e\u003e \u003e\u003e\u003e [+] [ 48 bytes]:\r\nconst_30: 30\r\nconst_90012: 90030\r\nconst_from_memory1: 0x1\r\nconst_from_memory2: 0x1\r\nhash_of_machine_guid: 0x61fa3a8c\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 14 of 19\n\nhash_of_computer_name: 0x9ddad832\r\ncpuid xor (eax^edx^ecx): 0xbfa81e83\r\nhash_of_user_name: 0x1a776b\r\nhash_of_default_user_name: 0x1a776b\r\nCreateTime: 0xb330815e\r\ncrc_of_rsa_key: 0x2c3a27c2\r\nProcessId (TEB[32]): 3196\r\n\u003c014e2be0\u003e \u003e\u003e\u003e [+] [ 48 bytes]:\r\nOS Build Number: 0x1001db1\r\nOS Major Version: 0x6\r\nOS Minor Version: 0x1\r\nIs64BitProcess * 32 + 32: 0x20\r\nbitmask_of_running_processes: 0x0\r\nProcSidSubauthority[0]: 0x2000\r\nIsAdmin: 0x1\r\nSystemTimeAsFileTime/10^7: 1467890012\r\nSystemTimeOfDayInformation/10^7: 1467888755\r\nSystemDefaultUILanguage ID: 2009596937\r\nGetSystemDefaultLCID: 1033\r\nzero: 0\r\n\u003cf77006f9\u003e \u003e\u003e\u003e [+] [ 12 bytes]:\r\nvolume seral number: 0xd49f44a8\r\ncrc32(computer name): 0x33898496\r\ncrc32(volume name name): 0x0\r\n\u003c22451ed7\u003e \u003e\u003e\u003e [+] [ 8 bytes]:\r\ncrc32 from be8ec514: 0xab8c0ad6\r\ncrc32 from 0282aa05: 0xa12e7929\r\n\u003c76fbf55a\u003e \u003e\u003e\u003e [+] [ 314 bytes]:\r\n76fbf55a chunk is null, with length 314\r\nAs we can see, quite a lot of things is passed around here. There are a lot of fingerprinting everywhere and\r\nsome information about current state.\r\nResponses are often more elaborate, but for the sake of presentation, let’s dissect a simple one:\r\n\u003cb84216c7\u003e \u003c\u003c\u003c [+] [ 4 bytes]:\r\nclient ip 195.187.238.160\r\n\u003cbe8ec514\u003e \u003c\u003c\u003c [+] [ 257 bytes]:\r\nuri found: 66.168.203.239:34352\r\nuri found: 98.109.148.2:34352\r\nuri found: 80.147.180.254:34352\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 15 of 19\n\nuri found: 162.222.25.250:34352\r\nuri found: 78.28.51.22:34352\r\nuri found: 75.166.109.79:34352\r\nuri found: 69.132.170.172:34352\r\nuri found: 95.91.6.149:34352\r\nuri found: 76.115.190.186:34352\r\nuri found: 173.14.184.9:34352\r\nuri found: 72.199.113.123:34352\r\nuri found: 73.150.46.222:34352\r\nuri found: ~[mungyshlde.com]\r\n\u003ccb0e30c4\u003e \u003c\u003c\u003c [+] [ 4 bytes]:\r\nnumber of seconds client should sleep: 280\r\n\u003cd2bf6f4a\u003e \u003c\u003c\u003c [+] [ 73 bytes]:\r\nstate information:\r\ndata field 0: 0x1fd\r\ndata field 1: 0xd8798c3e\r\ndata field 2: 0x0\r\ndata field 3: 0x0\r\ndata field 4: 0x0\r\ndata field 5: 0x0\r\ndata field 6: 0x595ef998\r\ndata field 7: 0x0\r\ndata field 8: 0x0\r\nbody: 846372/194,68,48,48,0/329188118/3/0/2\r\n\u003c76fbf55a\u003e \u003c\u003c\u003c [+] [ 268 bytes]:\r\npadding:\r\nI%S07h6LjRjN\u00265*sozG4t!5D%7Zk\u0026FVQelCONWlgKRnuOKZ6HALQxaq73zophDS3#zYYnT*B*al\u0026CZi9o9b5KQOfdwI37A%t@O*K\r\nAn infected machine gets to know its public IP address, IP addresses (and listening ports) of its peers and the\r\nactive domain. Additionally it is usually ordered to sleep for some time (usually 90 seconds when some files\r\nare pending to be transmitted and 280 seconds when nothing special happens).\r\nHere is the list of types of chunks that we can parse and understand:\r\nchunk\r\nhash\r\nshort description\r\nffd5e56e fingerprint 1\r\n014e2be0 fingerprint 2 + timestamps\r\nf77006f9 fingerprint 3\r\n22451ed7 crcs of last received chunks of type be8ec514 and 0282aa05\r\nb873dfe0 probably “enabled” flag (can be only 1 or 0)\r\n0c526e8b\r\nnested chunk (decrypt with nymaim_config_crypt, unpack with aplib, recursively repeat\r\nparsing)\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 16 of 19\n\nchunk\r\nhash\r\nshort description\r\n875c2fbf plain (non-encrypted) executable\r\n08750ec5\r\nnested chunk (decrypt with nymaim_config_crypt, unpack with aplib, recursively repeat\r\nparsing)\r\n1f5e1840 injects (decrypt with serpent, unpack with aplib, parse ISFB binary format)\r\n76daea91 dropper handshake (marker, without data)\r\nbe8ec514 list of peer IPs\r\n138bee04 list of peer IPs\r\n1a701ad9 encrypted binary (decrypt with serpent, unpack with aplib, save)\r\n30f01ee5 encrypted binary (decrypt with serpent, unpack with aplib, save)\r\n3bbc6128 encrypted binary (decrypt with serpent, unpack with aplib, save)\r\n39bc61ae encrypted binary (decrypt with serpent, unpack with aplib, save)\r\n261dc56c encrypted binary (decrypt with serpent, unpack with aplib, save)\r\na01fc56c encrypted binary (decrypt with serpent, unpack with aplib, save)\r\n76fbf55a padding\r\ncae9ea25\r\nnested chunk (decrypt with nymaim_config_crypt, unpack with aplib, recursively repeat\r\nparsing)\r\n0282aa05\r\nnested chunk (decrypt with nymaim_config_crypt, unpack with aplib, recursively repeat\r\nparsing)\r\nd2bf6f4a state informations\r\n41f2e735 web filters\r\n1ec0a948 web filters\r\n18c0a95e web filters\r\n3d717c2e web filters\r\n8de8f7e6 datetime (purpose is unknown, it’s always few days ahead of current date)\r\n3e5a221c list of additional binaries that was downloaded\r\n5babb165 payload handshake (marker, without data)\r\nb84216c7 public IP of infected machine\r\ncb0e30c4 number of seconds to sleep\r\nf31cc18f additional CRC32s of downloaded binaries\r\n920f2f0c injects (decrypt with serpent, unpack with aplib, parse ISFB binary format)\r\n930f2f0c injects (decrypt with serpent, unpack with aplib, parse ISFB binary format)\r\nThis may seem like a lot, but there are a lot of things we didn’t try to understand (we ignored most of dword-sized or always-zero chunks).\r\nAfter extracting everything from communication we can finally look at injects. For example Polish ones:\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 17 of 19\n\n(304 different injects, as of today)\r\nOr US injects:\r\n(393 different injects, as of today)\r\nResources\r\nYara rules:\r\nrule nymaim: trojan\r\n{\r\nmeta:\r\nauthor = \"mak\"\r\nstrings:\r\n$call_obfu_xor = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 33 ?? 08 E9 }\r\n$call_obfu_add = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 03 ?? 08 E9 }\r\n$call_obfu_sub = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 2b ?? 08 E9 }\r\n$nym_get_cnc = {E8 [4] C7 45 ?? [4] C7 45 ?? [4] 83 ??}//3D[4] 01 74 4E E8}\r\n$nym_get_cnc2 ={E8 [4] C7 45 ?? [4] 89 [5] 89 [5] C7 45 ?? [4] 83 ??}\r\n$nym_check_unp = {C7 45 ?? [4] 83 3D [3] 00 01 74 }\r\n$set_cfg_addr = {FF 75 ?? 8F 05 [4] FF 75 08 8F 05 [4] 68 [4] 5? 68 [4] 68 [4] E8}\r\ncondition:\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 18 of 19\n\n(\r\n/* orig */\r\n(2 of ($call_obfu*)) and (\r\n/* old versions */\r\n$nym_check_unp or $nym_get_cnc2 or $nym_get_cnc or\r\n/* new version */\r\n$set_cfg_addr\r\n)\r\n)\r\n}\r\nHashes (md5):\r\nPayload 2016-10-20, 9d6cb537d65240bbe417815243e56461, version 90032\r\nDropper 2016-10-20, a395c8475ad51459aeaf01166e333179, version 80018\r\nPayload 2016-10-05, 744d184bf8ea92270f77c6b2eea28896, version 90019\r\nPayload 2016-10-04, 6b31500ddd7a55a8882ebac03d731a3e, version 90012\r\nDropper 2016-04-12, cb3d058a78196e5c80a8ec83a73c2a79, version 80017\r\nDropper 2016-04-09, 8a9ae9f4c96c2409137cc361fc5740e9, version 80016\r\nRepository with our tools: nymaim-tools\r\nOther research\r\nhttp://blog.talosintel.com/2016/09/goznym.html\r\nhttp://www.seculert.com/blogs/nymaim-deep-technical-dive-adventures-in-evasive-malware\r\nhttps://bitbucket.org/daniel_plohmann/idapatchwork\r\nSource: https://www.cert.pl/en/news/single/nymaim-revisited/\r\nhttps://www.cert.pl/en/news/single/nymaim-revisited/\r\nPage 19 of 19\n\nauthor = \"mak\" strings:    \n$call_obfu_xor = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 33 ?? 08 E9 }\n$call_obfu_add = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 03 ?? 08 E9 }\n$call_obfu_sub = {55 89 E5 5? 8B ?? 04 89 ?? 10 8B ?? 0C 2b ?? 08 E9 }\n$nym_get_cnc = {E8 [4] C7 45 ?? [4] C7 45 ?? [4] 83 ??}//3D[4] 01 74 4E E8}\n$nym_get_cnc2 ={E8 [4] C7 45 ?? [4] 89 [5] 89 [5] C7 45 ?? [4] 83 ??}\n$nym_check_unp = {C7 45 ?? [4] 83 3D [3] 00 01 74 } \n$set_cfg_addr = {FF 75 ?? 8F 05 [4] FF 75 08 8F 05 [4] 68 [4] 5? 68 [4] 68 [4] E8}\ncondition:    \n  Page 18 of 19",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia",
		"ETDA"
	],
	"references": [
		"https://www.cert.pl/en/news/single/nymaim-revisited/"
	],
	"report_names": [
		"nymaim-revisited"
	],
	"threat_actors": [
		{
			"id": "bbf66d2d-3d20-4026-a2b5-56b31eb65de4",
			"created_at": "2025-08-07T02:03:25.123407Z",
			"updated_at": "2026-04-10T02:00:03.668131Z",
			"deleted_at": null,
			"main_name": "ZINC EMERSON",
			"aliases": [
				"Confucius ",
				"Dropping Elephant ",
				"EHDevel ",
				"Manul ",
				"Monsoon ",
				"Operation Hangover ",
				"Patchwork ",
				"TG-4410 ",
				"Viceroy Tiger "
			],
			"source_name": "Secureworks:ZINC EMERSON",
			"tools": [
				"Enlighten Infostealer",
				"Hanove",
				"Mac OS X KitM Spyware",
				"Proyecto2",
				"YTY Backdoor"
			],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "b753c6a8-a83d-47bc-829d-45e56136eb7d",
			"created_at": "2023-01-06T13:46:38.97802Z",
			"updated_at": "2026-04-10T02:00:03.169611Z",
			"deleted_at": null,
			"main_name": "GozNym",
			"aliases": [],
			"source_name": "MISPGALAXY:GozNym",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "7ea1e0de-53b9-4059-802f-485884180701",
			"created_at": "2022-10-25T16:07:24.04846Z",
			"updated_at": "2026-04-10T02:00:04.84985Z",
			"deleted_at": null,
			"main_name": "Patchwork",
			"aliases": [
				"APT-C-09",
				"ATK 11",
				"Capricorn Organisation",
				"Chinastrats",
				"Dropping Elephant",
				"G0040",
				"Maha Grass",
				"Quilted Tiger",
				"TG-4410",
				"Thirsty Gemini",
				"Zinc Emerson"
			],
			"source_name": "ETDA:Patchwork",
			"tools": [
				"AndroRAT",
				"Artra Downloader",
				"ArtraDownloader",
				"AutoIt backdoor",
				"BADNEWS",
				"BIRDDOG",
				"Bahamut",
				"Bozok",
				"Bozok RAT",
				"Brute Ratel",
				"Brute Ratel C4",
				"CinaRAT",
				"Crypta",
				"ForeIT",
				"JakyllHyde",
				"Loki",
				"Loki.Rat",
				"LokiBot",
				"LokiPWS",
				"NDiskMonitor",
				"Nadrac",
				"PGoShell",
				"PowerSploit",
				"PubFantacy",
				"Quasar RAT",
				"QuasarRAT",
				"Ragnatela",
				"Ragnatela RAT",
				"SocksBot",
				"TINYTYPHON",
				"Unknown Logger",
				"WSCSPL",
				"Yggdrasil"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "c81067e0-9dcb-4e3f-abb0-80126519c5b6",
			"created_at": "2022-10-25T15:50:23.285448Z",
			"updated_at": "2026-04-10T02:00:05.282202Z",
			"deleted_at": null,
			"main_name": "Patchwork",
			"aliases": [
				"Hangover Group",
				"Dropping Elephant",
				"Chinastrats",
				"Operation Hangover"
			],
			"source_name": "MITRE:Patchwork",
			"tools": [
				"NDiskMonitor",
				"QuasarRAT",
				"BackConfig",
				"TINYTYPHON",
				"AutoIt backdoor",
				"PowerSploit",
				"BADNEWS",
				"Unknown Logger"
			],
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "bc289ba8-bc61-474c-8462-a3f7179d97bb",
			"created_at": "2022-10-25T16:07:24.450609Z",
			"updated_at": "2026-04-10T02:00:04.996582Z",
			"deleted_at": null,
			"main_name": "Avalanche",
			"aliases": [],
			"source_name": "ETDA:Avalanche",
			"tools": [],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434582,
	"ts_updated_at": 1775792159,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/0501179af18cbaaaa6ebb09fd0f9998a17d726f5.pdf",
		"text": "https://archive.orkl.eu/0501179af18cbaaaa6ebb09fd0f9998a17d726f5.txt",
		"img": "https://archive.orkl.eu/0501179af18cbaaaa6ebb09fd0f9998a17d726f5.jpg"
	}
}