{
	"id": "dddf0e39-bc18-40b0-8993-d4d410b22b44",
	"created_at": "2026-04-06T00:14:24.989623Z",
	"updated_at": "2026-04-10T13:12:15.333737Z",
	"deleted_at": null,
	"sha1_hash": "dd7fd8dbbb294353765aa2823bb77329c37b13db",
	"title": "Melting Ice – Tracking IcedID Servers with a few simple steps",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 90046,
	"plain_text": "Melting Ice – Tracking IcedID Servers with a few simple steps\r\nBy Alex Ilgayev\r\nPublished: 2021-05-26 · Archived: 2026-04-05 20:32:59 UTC\r\nResearch by: Alex Ilgayev\r\nIntroduction\r\nTracking botnets usually demands a significant amount of effort, time, and threat intelligence know-how. The\r\nbarrier to entry grows even larger in cases of multi-staged complex malware families such as IcedID, Emotet, and\r\nQakBot. Therefore, as malware analysts, we tend to look for ways to automate the process as much as we can —\r\ncollecting a large scale of samples, identifying them, extracting their configurations, and then having these yield\r\nvalue we are interested in, such as a clearer threat intel picture or a more up-to-date domain reputation engine. \r\nWhile the malware analysis life is often as difficult as described above, it doesn’t have to be. Sometimes, if we are\r\nclever, 10% of the work will get us 90% of the result we are interested in. In this article, we demonstrate such a\r\nsleight-of-hand and show how to hunt IcedID C\u0026C servers quickly and without tracking or analyzing any\r\nsamples.\r\nIcedID Background\r\nThe IcedID banker malware first emerged in September 2017 and has made significant progress since then. This\r\nthreat, also known as BokBot, has constantly been growing in the past year and boasts a wide range of malicious\r\ncapabilities such as browser hooking, credential theft, MiTM proxy setup and a VNC module, among others.\r\nThe bot’s internal capabilities, including infection chain and malicious components, were described thoroughly by\r\nMalwarebytes, Group-IB, and Binary Defense past publications.\r\nAccording to the latest infection chain presented by Binary Defense researchers, recent iterations of IcedID\r\ncontain three malicious components – The entry-point DOC/XLS containing malicious macros; a first stage\r\npayload; and finally a second-stage payload, which itself consists of two sub-parts – A 64bit DLL loader, and the\r\nencrypted actual bot disguised as a “.dat” file.\r\nEach second-stage payload usually contains 2-4 unique embedded domains, which all resolve to the same IP\r\naddress. Suppose that we set an objective to track these domains/IPs and block them; naturally, we are interested\r\nin the quickest and painless way to do it.\r\nThe communication protocol for the victims is HTTPS, so we inspected the bundled TLS certificate. As we can\r\nobserve below, the IcedID people care more about the hard, concrete guarantees provided by RSA-2048 and less\r\nabout following security guidelines; the certificate is issued to the defaultly-named “Internet Widgits Pty, Ltd”\r\nwhich resides in “Some State”. \r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 1 of 12\n\nFigure 1 – Sample IcedID C\u0026C certificate\r\nIf we compare this IcedID certificate with a default self-signed certificate, the most notable difference would be\r\nthe common-name field. Public servers should have their FQDN (e.g. checkpoint.com ) in this field and not\r\nlocalhost like in this case.\r\nYou probably understand where this is going; we promptly got to scanning the wide web and looking for other\r\nservers which present this certificate.\r\nEnumerating Servers\r\nLoath to reinvent the wheel, we opted to use the popular internet scanning platform Censys to create a list of\r\npotential C\u0026C candidates. Additional internet scanning platforms, like Shodan, could be fit for this purpose as\r\nwell.\r\nThe Censys engine gave us a great amount of control for certificate querying. After some tweaking and fiddling,\r\nwe constructed the following query:\r\n443.https.tls.certificate.parsed.issuer_dn: \"CN=localhost, C=AU, ST=Some-State, O=Internet Widgits\r\nPty Ltd\" and 443.https.tls.certificate.parsed.subject_dn: \"CN=localhost, C=AU, ST=Some-State,\r\nO=Internet Widgits Pty Ltd\"\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 2 of 12\n\nFigure 2 – One of Censys results with the certificate\r\nIf you doubt the uniqueness of having localhost as a common name in a certificate, a short experiment proves it\r\nhandily: the dozens of servers our search yielded may seem like much, but if you omit the requirement\r\nCN=localhost from the search query, the results explode in size and number in the ten of thousands.\r\nAt the moment, we cannot assume that each of these servers is an active IcedID malicious C\u0026C, so we need to\r\nvalidate them. Luckily, this is possible due to another unique property we discovered during the research.\r\nValidating The Servers\r\nWhile we were reverse-engineering the bot functionality, we noticed the bot makes a pretty interesting test before\r\ncommunicating with the C\u0026C server. The following code is part of a callback function of WinHttpSendRequest\r\nand is called before contacting the C\u0026C.\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 3 of 12\n\nFigure 3 – Certificate verification code\r\nIn simple words, this code runs Fowler–Noll–Vo 32 bit hash function over the certificate’s public key and\r\ncompares it with the assigned serial number of the certificate. The communication proceeds only when that\r\ncomparison matches (Or with an XOR-ed value 0x384A2414 ). \r\nThe Certificate Serial Number field is assigned by the Certificate Authority and provides a unique identifier for\r\neach generated certificate. The Certificate Issuer must ensure that no two distinct certificates with the same\r\nCertificate Issuer DN contain the same serial number, but no one can guarantee that.\r\nIn our case, the certificate is self-signed, and the malware operators assign its C\u0026C serial number field uniquely\r\naccording to the above logic. Using this verification algorithm ourselves, against a suspected server, we can make\r\nsure it is part of the IcedID infrastructure. We provided a simple python script to verify that logic against a\r\nsupplied remote server in Appendix A.\r\nApplying that method, we discovered that most of the previous potential results were IcedID related and narrowed\r\nthe list down to 52 servers. Given the unique certificate property paired with the uncommon hash function, we\r\ncould safely deduce that these servers are components in IcedID C\u0026C infrastructure. With this list in hand, we\r\nwent to take a closer look at the servers. \r\nServer Analysis\r\nBy analyzing their internet-facing banners, we could find several similar properties for most of the deployed\r\nservers. The most common properties were open ports, operating system, and web-server:\r\nOpen Ports – All servers had port 443 available, 94% also listened on port 80 and 77% on SSH port (22).\r\nOperating System – 77% of the servers were running a Debian OS.\r\nWeb Server – 94% of the servers were running nginx web-server.\r\nWe could also see that most of the servers reside in Romania, United States, and Germany:\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 4 of 12\n\nFigure 4 – IcedID servers locations\r\nAnother angle of analysis is via a passive DNS service such as RiskIQ. Using this service allows us to find\r\nassociated domains for each IP address we have and expand our threat intel picture. These extra domains must\r\nlogically be embedded in some samples that we do not have direct access to. For example, one of the addresses we\r\nfound was 152[.]89.247.60 , which unresolved to the following IcedID domains:\r\nformgotobig[.]top\r\nponduroviga[.]top\r\ntranmigrust[.]club\r\naswenedo[.]space\r\nSummary\r\nIt is a sad fact of life that doing the “right thing” can backfire. Such is the case for IcedID campaign maintainers\r\nhere; they produced a self-signed certificate and had their malicious operation support HTTPS, a laudable effort\r\nthat the vast majority of malware cannot be bothered with, and even some long-running legitimate websites could\r\nnot be bothered with for the longest time. By doing this, they made it much more challenging to instigate a hostile\r\ntakeover of their network — but they also made all their servers basically respond to standard scanning services\r\nwith a cheerful “Hi, I’m a malicious C\u0026C”.\r\nOnce these servers were exposed, we were able to continuously track them and analyze their behavior without\r\never running a regular expression, never mind launching a debugger or disassembler. For someone running a\r\nmalicious campaign, this is a nightmare scenario; they aim to place the “prize” of the continuously updating C\u0026C\r\nserver list just out of reach, at the top of the tier list. Instead, due to this too-clever-by-half TLS business,\r\ncollecting the servers (and obtaining a bunch of juicy information, which is out of scope for this article) became\r\nwithin the analyst’s capabilities with a week of experience. As a wise person once said, think before using a\r\ntechnique, or your opponent will use it against you.  \r\nIOC\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 5 of 12\n\nIcedID C\u0026C servers:\r\n83[.]97.20.249\r\n83[.]97.20.174\r\n194[.]5.249.52\r\n45[.]153.240.135\r\n194[.]5.250.104\r\n152[.]89.247.60\r\n91[.]193.19.97\r\n194[.]5.249.81\r\n83[.]97.20.73\r\n194[.]5.250.35\r\n83[.]97.20.176\r\n194[.]5.250.46\r\n212[.]114.52.186\r\n91[.]193.19.37\r\n45[.]147.231.113\r\n188[.]119.148.75\r\n185[.]38.185.90\r\n45[.]138.172.179\r\n91[.]193.19.51\r\n83[.]97.20.122\r\n194[.]5.249.103\r\n139[.]60.161.63\r\n194[.]5.249.86\r\n139[.]60.161.48\r\n185[.]33.85.35\r\n194[.]5.249.97\r\n79[.]141.164.241\r\n194[.]5.249.90\r\n193[.]109.69.52\r\n194[.]5.249.54\r\n185[.]70.184.87\r\n79[.]141.166.39\r\n146[.]0.77.92\r\n31[.]184.199.11\r\n45[.]129.99.241\r\n194[.]5.249.46\r\n185[.]123.53.202\r\n146[.]0.77.18\r\n31[.]24.228.170\r\n45[.]147.230.88\r\n83[.]97.20.254\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 6 of 12\n\n45[.]147.230.82\r\n194[.]5.249.72\r\n46[.]17.98.191\r\n45[.]153.241.115\r\n185[.]70.184.41\r\n139[.]60.161.50\r\n194[.]5.249.143\r\n79[.]141.161.176\r\n5[.]149.252.179\r\n45[.]147.228.198\r\n91[.]193.19.251\r\nAppendix A\r\nTesting a server for IcedID certificate:\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nimport idna\r\nfrom socket import socket\r\nfrom OpenSSL import SSL\r\nfrom cryptography.hazmat.primitives.serialization import Encoding\r\nfrom cryptography.hazmat.primitives.serialization import PublicFormat\r\ndef fnv1a_32(data: bytes) -\u003e int:\r\n\"\"\"Fowler–Noll–Vo hash function variation.\r\nArgs:\r\ndata (bytes): Input data\r\nReturns:\r\nint: Output 32 bit hash\r\n\"\"\"\r\nhval_init = 0x811c9dc5\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 7 of 12\n\nfnv_prime = 0x01000193\r\nfnv_size = 2 ** 32\r\nhval = hval_init\r\nfor byte in data:\r\nhval = hval ^ byte\r\nhval = (hval * fnv_prime) % fnv_size\r\nreturn hval\r\ndef get_certificate(hostname: str, port: int):\r\n\"\"\"Connects to the remote server,\r\nand retrieves the certificate.\r\nArgs:\r\nhostname (str): Remote hostname\r\nport (int): Remote port (usually 443)\r\nReturns:\r\nCertificate object\r\n\"\"\"\r\nhostname_idna = idna.encode(hostname)\r\n# We are building a SSL context on top of a raw socket.\r\nsock = socket()\r\nsock.connect((hostname, port))\r\nctx = SSL.Context(SSL.SSLv23_METHOD)\r\nctx.check_hostname = False\r\n# the cert is self-signed so we don't want to verify it\r\nctx.verify_mode = SSL.VERIFY_NONE\r\n# making SSL handshake\r\nsock_ssl = SSL.Connection(ctx, sock)\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 8 of 12\n\nsock_ssl.set_connect_state()\r\nsock_ssl.set_tlsext_host_name(hostname_idna)\r\nsock_ssl.do_handshake()\r\n# retrieving certificate and converting it to an cryptography object\r\ncert = sock_ssl.get_peer_certificate()\r\ncrypto_cert = cert.to_cryptography()\r\nsock_ssl.close()\r\nsock.close()\r\nreturn crypto_cert\r\ndef test_is_icedid_c2(hostname: str, port: int) -\u003e bool:\r\n\"\"\"Testing whether a remote server is part of IcedID\r\nC\u0026C infrastructure.\r\nArgs:\r\nhostname (str): Remote hostname\r\nport (int): Remote port (usually 443)\r\nReturns:\r\nbool: True if the server is IcedID verified, or False otherwise.\r\n\"\"\"\r\ntry:\r\n# We query the server and retrieve its certificate.\r\ncert = get_certificate(hostname, port)\r\nserial_number = cert.serial_number\r\n# Getting the public key, and hashing it.\r\npublic_key = cert.public_key().public_bytes(Encoding.DER, PublicFormat.PKCS1)\r\nfnv_hash = fnv1a_32(public_key) \u0026 0x7fffffff\r\n# Finally comparing the hash to the serial number.\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 9 of 12\n\nif serial_number == fnv_hash or serial_number == fnv_hash ^ 0x384A2414:\r\nreturn True\r\nexcept Exception as e:\r\nreturn False\r\nreturn False\r\nimport idna from socket import socket from OpenSSL import SSL from\r\ncryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization\r\nimport PublicFormat def fnv1a_32(data: bytes) -\u003e int: \"\"\"Fowler–Noll–Vo hash function variation. Args: data\r\n(bytes): Input data Returns: int: Output 32 bit hash \"\"\" hval_init = 0x811c9dc5 fnv_prime = 0x01000193 fnv_size\r\n= 2 ** 32 hval = hval_init for byte in data: hval = hval ^ byte hval = (hval * fnv_prime) % fnv_size return hval def\r\nget_certificate(hostname: str, port: int): \"\"\"Connects to the remote server, and retrieves the certificate. Args:\r\nhostname (str): Remote hostname port (int): Remote port (usually 443) Returns: Certificate object \"\"\"\r\nhostname_idna = idna.encode(hostname) # We are building a SSL context on top of a raw socket. sock = socket()\r\nsock.connect((hostname, port)) ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.check_hostname = False # the\r\ncert is self-signed so we don't want to verify it ctx.verify_mode = SSL.VERIFY_NONE # making SSL handshake\r\nsock_ssl = SSL.Connection(ctx, sock) sock_ssl.set_connect_state()\r\nsock_ssl.set_tlsext_host_name(hostname_idna) sock_ssl.do_handshake() # retrieving certificate and converting it\r\nto an cryptography object cert = sock_ssl.get_peer_certificate() crypto_cert = cert.to_cryptography()\r\nsock_ssl.close() sock.close() return crypto_cert def test_is_icedid_c2(hostname: str, port: int) -\u003e bool: \"\"\"Testing\r\nwhether a remote server is part of IcedID C\u0026C infrastructure. Args: hostname (str): Remote hostname port (int):\r\nRemote port (usually 443) Returns: bool: True if the server is IcedID verified, or False otherwise. \"\"\" try: # We\r\nquery the server and retrieve its certificate. cert = get_certificate(hostname, port) serial_number =\r\ncert.serial_number # Getting the public key, and hashing it. public_key =\r\ncert.public_key().public_bytes(Encoding.DER, PublicFormat.PKCS1) fnv_hash = fnv1a_32(public_key) \u0026\r\n0x7fffffff # Finally comparing the hash to the serial number. if serial_number == fnv_hash or serial_number ==\r\nfnv_hash ^ 0x384A2414: return True except Exception as e: return False return False\r\nimport idna\r\nfrom socket import socket\r\nfrom OpenSSL import SSL\r\nfrom cryptography.hazmat.primitives.serialization import Encoding\r\nfrom cryptography.hazmat.primitives.serialization import PublicFormat\r\ndef fnv1a_32(data: bytes) -\u003e int:\r\n \"\"\"Fowler–Noll–Vo hash function variation.\r\n Args:\r\n data (bytes): Input data\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 10 of 12\n\nReturns:\r\n int: Output 32 bit hash\r\n \"\"\"\r\n hval_init = 0x811c9dc5\r\n fnv_prime = 0x01000193\r\n fnv_size = 2 ** 32\r\n hval = hval_init\r\n for byte in data:\r\n hval = hval ^ byte\r\n hval = (hval * fnv_prime) % fnv_size\r\n return hval\r\ndef get_certificate(hostname: str, port: int):\r\n \"\"\"Connects to the remote server,\r\n and retrieves the certificate.\r\n Args:\r\n hostname (str): Remote hostname\r\n port (int): Remote port (usually 443)\r\n Returns:\r\n Certificate object\r\n \"\"\"\r\n hostname_idna = idna.encode(hostname)\r\n # We are building a SSL context on top of a raw socket.\r\n sock = socket()\r\n sock.connect((hostname, port))\r\n ctx = SSL.Context(SSL.SSLv23_METHOD)\r\n ctx.check_hostname = False\r\n # the cert is self-signed so we don't want to verify it\r\n ctx.verify_mode = SSL.VERIFY_NONE\r\n # making SSL handshake\r\n sock_ssl = SSL.Connection(ctx, sock)\r\n sock_ssl.set_connect_state()\r\n sock_ssl.set_tlsext_host_name(hostname_idna)\r\n sock_ssl.do_handshake()\r\n # retrieving certificate and converting it to an cryptography object\r\n cert = sock_ssl.get_peer_certificate()\r\n crypto_cert = cert.to_cryptography()\r\n sock_ssl.close()\r\n sock.close()\r\n return crypto_cert\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 11 of 12\n\ndef test_is_icedid_c2(hostname: str, port: int) -\u003e bool:\r\n \"\"\"Testing whether a remote server is part of IcedID\r\n C\u0026C infrastructure.\r\n Args:\r\n hostname (str): Remote hostname\r\n port (int): Remote port (usually 443)\r\n Returns:\r\n bool: True if the server is IcedID verified, or False otherwise.\r\n \"\"\"\r\n try:\r\n # We query the server and retrieve its certificate.\r\n cert = get_certificate(hostname, port)\r\n serial_number = cert.serial_number\r\n # Getting the public key, and hashing it.\r\n public_key = cert.public_key().public_bytes(Encoding.DER, PublicFormat.PKCS1)\r\n fnv_hash = fnv1a_32(public_key) \u0026 0x7fffffff\r\n # Finally comparing the hash to the serial number.\r\n if serial_number == fnv_hash or serial_number == fnv_hash ^ 0x384A2414:\r\n return True\r\n except Exception as e:\r\n return False\r\n return False\r\nSource: https://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nhttps://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/\r\nPage 12 of 12",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://research.checkpoint.com/2021/melting-ice-tracking-icedid-servers-with-a-few-simple-steps/"
	],
	"report_names": [
		"melting-ice-tracking-icedid-servers-with-a-few-simple-steps"
	],
	"threat_actors": [],
	"ts_created_at": 1775434464,
	"ts_updated_at": 1775826735,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/dd7fd8dbbb294353765aa2823bb77329c37b13db.pdf",
		"text": "https://archive.orkl.eu/dd7fd8dbbb294353765aa2823bb77329c37b13db.txt",
		"img": "https://archive.orkl.eu/dd7fd8dbbb294353765aa2823bb77329c37b13db.jpg"
	}
}