{
	"id": "227fb818-fe21-4889-91dd-d5c428a0a534",
	"created_at": "2026-04-06T00:17:36.459053Z",
	"updated_at": "2026-04-10T03:24:34.028913Z",
	"deleted_at": null,
	"sha1_hash": "f80f01b9908359c52c3af60fa5e4bb9a202acab3",
	"title": "KrustyLoader - Rust malware linked to Ivanti ConnectSecure compromises",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 463112,
	"plain_text": "KrustyLoader - Rust malware linked to Ivanti ConnectSecure\r\ncompromises\r\nBy Théo Letailleur\r\nArchived: 2026-04-05 16:06:58 UTC\r\nOn 10th January 2024, Ivanti disclosed two zero-day critical vulnerabilities affecting Connect Secure VPN\r\nproduct: CVE-2024-21887 and CVE-2023-46805 allowing unauthenticated remote code execution. Volexity and\r\nMandiant published articles reporting how these vulnerabilities were actively exploited by a threat actor. On 18th\r\nJanuary, Volexity published new observations including hashes of Rust payloads downloaded on compromised\r\nIvanti Connect Secure instances. This article presents a malware analysis of these unidentified Rust payloads that I\r\nlabelled as KrustyLoader.\r\nVous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus\r\nIntroduction\r\nOn 10th January 2024, Ivanti disclosed two zero-day critical vulnerabilities affecting Connect Secure VPN\r\nproduct: CVE-2024-21887 and CVE-2023-468051 allowing unauthenticated remote code execution. Volexity2\r\nand Mandiant3 published several articles showing how these vulnerabilities were actively exploited by a threat\r\nactor, tracked by Volexity as UTA0178 and by Mandiant as UNC5221.\r\nOn 18th January, Volexity published new indicators of compromise4 including Rust payloads downloaded on\r\ncompromised Ivanti Connect Secure appliances. Then on 21st and 24th of January, I published two posts on X5 6\r\nsummarizing the behaviour of those 12 Rust payloads. They share almost 100% code similarity and their main\r\npurpose is to download and execute a Sliver backdoor. I personally labelled this piece of malware as\r\nKrustyLoader.\r\nTherefore, the purpose of this article is to provide more insights on this malware, reversing tips, as well as a script\r\nthat automatically extracts the encrypted URL from any similar sample.\r\nBasic information\r\nKrustyLoader basic information\r\nSHA256\r\n47ff0ae9220a09bfad2a2fb1e2fa2c8ffe5e9cb0466646e2a940ac2e0cf55d04\r\n816754f6eaf72d2e9c69fe09dcbe50576f7a052a1a450c2a19f01f57a6e13c17\r\nc26da19e17423ce4cb4c8c47ebc61d009e77fc1ac4e87ce548cf25b8e4f4dc28\r\nc7ddd58dcb7d9e752157302d516de5492a70be30099c2f806cb15db49d466026\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 1 of 8\n\nd14122fa7883b89747f273c44b1f71b81669a088764e97256f97b4b20d945ed0\r\n6f684f3a8841d5665d083dcf62e67b19e141d845f6c13ee8ba0b6ccdec591a01\r\na4e1b07bb8d6685755feca89899d9ead490efa9a6b6ccc00af6aaea071549960\r\nef792687b8bcd3c03bed4b09c4722bba921536802afe01f7cdb01cc7c3c60815\r\n76902d101997df43cd6d3ac10470314a82cb73fa91d212b97c8f210d1fa8271f\r\ne47b86b8df43c8c1898abef15b8b7feffe533ae4e1a09e7294dd95f752b0fbb2\r\n73657c062a7cc50a3d51853ec4df904bcb291fdc9cdd08eecaecb78826eb49b6\r\n030eb56e155fb01d7b190866aaa8b3128f935afd0b7a7b2178dc8e2eb84228b0\r\nFile type ELF 64-bit LSB pie executable x86_64 stripped, static-pie linked\r\nFile size 878824 bytes\r\nThreat Linux Rust downloader\r\nScreenshots and extracts on this article are based on sample 030eb56e1[...]84228b0 (the highlighted hash above),\r\nbut – as they are similar – the logic is the same for the other payloads.\r\nCode analysis approach\r\nYou will not find a deep analysis into assembly code with tons of IDA screenshots, because it does not bring much\r\nvalue in this context. However, I find more interesting to explain what is my approach to quickly spot the useful\r\nparts of the code and get a general idea of its behaviour.\r\nUsually we would start from the entry point and determine the flow of execution, symbols, and API functions.\r\nHowever, there are several difficulties to consider when reversing a Rust-based executable:\r\nThe executable is statically linked, meaning that libraries are embedded into the executable, including Rust\r\ncrates and the libc: it adds lots of functions that are not important to spend time during malware analysis.\r\nSince Rust is a high-level programming language, its abstractions tend to bring a “natural” obfuscation to\r\nthe program code with lots of additional checks, temporary variables and built-in structures.\r\nMoreover, this sample is stripped, meaning that symbols and debug information are removed from the\r\nexecutable. In practice, it means that the disassembler will not be able to retrieve functions names of the\r\nprogram – and of the libraries – as well as structures, variable and constant names, etc.\r\nAs a result, with more than 2000 unnamed functions, it becomes quite tedious to determine what is the actual code\r\nof the developer, and what is not.\r\nTherefore, I first executed the sample in a controlled environment (a Linux Debian-based virtual machine), to\r\nmonitor any system and network activity.\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 2 of 8\n\n$ strace ./030eb56e155fb01._bad_elf\r\nexecve(\"./030eb56e155fb01._bad_elf\", [\"./030eb56e155fb01._bad_elf\"], 0x7ffc20b137a0 /* 50 vars */) = 0\r\n[...]\r\nreadlink(\"/proc/self/exe\", \"/home/user/iv/030eb56/030eb56e\"..., 256) = 48\r\nopen(\"/home/user/iv/030eb56/030eb56e155fb01._bad_elf\", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_PATH) = 6\r\nreadlink(\"/proc/self/fd/6\", \"/home/user/iv/030eb56/030eb56e\"..., 4095) = 48\r\nfstat(6, {st_mode=S_IFREG|0755, st_size=878824, ...}) = 0\r\nstat(\"/home/user/iv/030eb56/030eb56e155fb01._bad_elf\", {st_mode=S_IFREG|0755, st_size=878824, ...}) = 0\r\nclose(6) = 0\r\nunlink(\"/home/user/iv/030eb56/030eb56e155fb01._bad_elf\") = 0\r\ngetppid() = 3033\r\nreadlink(\"/proc/self/exe\", \"/home/user/iv/030eb56/030eb56e\"..., 256) = 58\r\nreadlink(\"/proc/self/exe\", \"/home/user/iv/030eb56/030eb56e\"..., 256) = 58\r\nstat(\"/tmp/0\", 0x7fffe54a8700) = -1 ENOENT (No such file or directory)\r\n[...]\r\nexit_group(0) = ?\r\nI was first disappointed because the process exited instantaneously with no network activity and no impact on the\r\nfilesystem. But there was a few interesting system calls executed:\r\nreadlink(\"/proc/self/exe\"...) : reads the value (the path) pointed by the symbolic link\r\n/proc/self/exe , meaning its executable (here /home/user/iv/030eb56/030eb56e155fb01._bad_elf );\r\nThen it opens its executable with open syscall, checks its file status with fstats (not sure why) and\r\ncloses it;\r\nunlink(\"/home/user/iv/030eb56/030eb56e155fb01._bad_elf\") : deletes itself;\r\nstat(\"/tmp/0\", ...) : tests the existence of /tmp/0 file, in this running context you can see the error\r\nexplaining that it does not exist;\r\nExits.\r\nWe can use this information to find the beginning of the main useful function by searching any references to\r\nreadlink and unlink system calls, as well as /proc/self/exe and /tmp/0 strings. However, those two\r\nstrings did not bring interesting results (as I discovered later, they are stack strings so no reference!). But /tmp/\r\nand the two mentioned system calls were directly referenced from a big function that I determined as the main\r\nroutine.\r\nThe main routine is called by another big function that I identified as a Tokio worker thread, responsible for\r\nrunning asynchronous tasks. Tokio7 is a famous Rust crate, very handy when building asynchronous network\r\napplications. I quickly identified the purpose of this function thanks to a reference to TOKIO_WORKER_THREADS\r\nstring, which allowed me to completely skip its code flow and go straight to the main routine.\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 3 of 8\n\nKrustyLoader main routine\r\nOnce we identify the exception/error handling code inside the function, the execution flow becomes more\r\nobvious. To help with the reverse engineering, I debugged the program alongside with GDB. Since it is a stripped\r\nPIE (Position Independent Executable) binary – simply put, code segment base address is randomized – we can\r\nneither break on function names nor predictable addresses. The start GDB command is not able to break on the\r\nmain function in this configuration. Thankfully, GDB has another command called starti8 that sets a temporary\r\nbreakpoint at the very first instruction of a program’s execution and then invokes the ‘run’ command. This\r\ncommand allows us to start the process, break instantaneously, and get the base address of the code segment\r\nloaded in memory.\r\n$ gdb 030eb56e155fb01._bad_elf\r\n[...]\r\nReading symbols from 030eb56e155fb01._bad_elf...\r\n(No debugging symbols found in 030eb56e155fb01._bad_elf)\r\ngef➤ starti\r\nStarting program: /home/user/iv/030eb56/030eb56e155fb01._bad_elf\r\n[*] Failed to find objfile or not a valid file format: [Errno 2] No such file or directory: 'system-supplied DSO\r\nProgram stopped.\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 4 of 8\n\n0x00007ffff7d364db in ?? ()\r\n[...]\r\n────────────────────────────────────────────────────────\r\n 0x7ffff7d364cd call 0x7ffff7dc9bc4\r\n 0x7ffff7d364d2 mov edi, DWORD PTR [rsp+0xc]\r\n 0x7ffff7d364d6 call 0x7ffff7dc742e\r\n → 0x7ffff7d364db xor rbp, rbp\r\n 0x7ffff7d364de mov rdi, rsp\r\n 0x7ffff7d364e1 lea rsi, [rip+0x2c6570] # 0x7ffff7ffca58\r\n 0x7ffff7d364e8 and rsp, 0xfffffffffffffff0\r\n 0x7ffff7d364ec call 0x7ffff7d364f1\r\n 0x7ffff7d364f1 sub rsp, 0x190\r\n────────────────────────────────────────────────────────\r\n[#0] Id 1, Name: \"030eb56e155fb01\", stopped 0x7ffff7d364db in ?? (), reason: STOPPED\r\n────────────────────────────────────────────────────────\r\n[#0] 0x7ffff7d364db → xor rbp, rbp\r\n────────────────────────────────────────────────────────\r\ngef➤\r\nIDA disassembly view - KrustytLoader's first instruction\r\nGDB breaks at the first instruction at address 0x7ffff7d364db in our example. IDA disassembly view shows that\r\nthe first instruction of the program (pointed by start symbol) is at 0xF4DB . Then, using a subtle mathematical\r\noperation, we can retrieve the base address and determine the address of the main routine: 0x7ffff7d364db -\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 5 of 8\n\n0xF4DB + 2E70B (offset of the main routine) = 0x7ffff7d5570b . We can now break at  0x7ffff7d5570b and\r\nfinally start debugging the main routine normally.\r\nThe next section describes the results of my analysis based on this approach. I also used Sysdig9 to monitor the\r\nsystem calls and general activity on the virtual machine. This is a great system monitoring tool that would deserve\r\nits own article!\r\nKrustyLoader Behaviour\r\nBased on reverse engineering and dynamic analysis, the behaviour of KrustyLoader can be summarized in the\r\nfollowing main points:\r\nThe malware reads /proc/self/exe to gets its path (readlink) and deletes itself (unlink)\r\nThen the following checks must be validated else the program exits:\r\nIt gets the process parent ID (PPID) using getppid syscall and exits if PPID is 1.\r\nAnti-debug checks: it reads /proc/self/exe again (now the value suffixed with \" (deleted)\" )\r\nand exits if it contains gdb or lldb (both debuggers) strings.\r\nIt checks the existence of /tmp/0 and exits if it does not.\r\nIt checks if its executable (pointed by /proc/self/exe ) is located in /tmp/ directory. If it's not in\r\n/tmp/ directory, it exits.\r\nOnce all the checks successfully passed, the malware starts doing interesting stuff:\r\nIt creates in /tmp directory a new file with a filename made of 10 random alphanumeric\r\ncharacters.\r\nIt decrypts a hardcoded URL, and sends a GET HTTP request to that URL.\r\nIn result, it receives an encrypted response from the remote server.\r\nThe content is decrypted and written to the random file.\r\nIt makes the random file executable using system command chmod +x /tmp/randomfile .\r\nFinally, it tries to execute the newly created executable and exits.\r\nAs a general point, there is a bit of obfuscation: most symbols are XOR-encrypted stack strings.\r\nThe process of decryption used by the malware to retrieve the URL has three steps:\r\n1. It hex-decodes (the equivalent of bytes.fromhex() in Python) the encrypted URL;\r\n2. XOR each byte with a 1-byte key;\r\n3. And uses AES-128 CFB-1 mode10 with hardcoded key and initialization vector to decrypt and get the\r\nURL.\r\nAES-128 CFB is also used to decrypt the payload sent by the remote HTTP server.\r\nWhat about the executed payloads? Based on my observations, all the samples download a Sliver (Golang)\r\nbackdoor, though from different URLs. The Sliver backdoors contact their C2 server using HTTP/HTTPS\r\ncommunication. Sliver11 is an open-source adversary simulation tool that is gaining popularity amongst threat\r\nactors, since it provides a practical command and control framework.\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 6 of 8\n\nThe list of domains and URLs can be found in this GitHub repository: https://github.com/synacktiv/krustyloader-analysis.\r\nExtraction and detection\r\nExtraction of the URL\r\nI developed a simple script to statically retrieve and decrypt the URL used by KrustyLoader to get the Sliver\r\nbackdoor. It allows extracting the pieces of information we only need without executing the malware. The script is\r\navailable here: https://github.com/synacktiv/krustyloader-analysis/blob/main/krusty_extractor.py. It requires\r\npycryptodome Python package and a decent Python version to run. It automatically extracts the XOR key, the\r\nAES key, the AES initialization vector and the encrypted URL.\r\n$ python krusty_extractor.py 030eb56/030eb56e155fb01._bad_elf\r\nSample SHA256sum: 030eb56e155fb01d7b190866aaa8b3128f935afd0b7a7b2178dc8e2eb84228b0\r\nXOR KEY: 0x81\r\nAES-128 CFB KEY: b1e228b4b5723d41a575d993b70c906b\r\nAES-128 CFB IV: 27bb7db8021cd9ade3520a6e67f43ac5\r\nDecrypted Stage Hoster URL: http://bringthenoiseappnew.s3.amazonaws.com/iEgJ4J7Uc9YgC\r\n$ python krusty_extractor.py a4e1b07/a4e1b0._bad_elf\r\nSample SHA256sum: a4e1b07bb8d6685755feca89899d9ead490efa9a6b6ccc00af6aaea071549960\r\nXOR KEY: 0x81\r\nAES-128 CFB KEY: b1e228b4b5723d41a575d993b70c906b\r\nAES-128 CFB IV: 27bb7db8021cd9ade3520a6e67f43ac5\r\nDecrypted Stage Hoster URL: http://bbr-promo.s3.amazonaws.com/NWEUW983Ve4g1\r\nAs you can observe in the extract above, it successfully decrypts the URL of both samples (and it works for all 12\r\nsamples). When I first ran the script on all samples, I was quite disappointed to notice that they also share the\r\nexact same cryptographic parameters. 😄 At least it sped up my analysis, and it could still be handy in case there\r\nare new variants with different XOR key or AES parameters.\r\nDetection\r\nYou can find a Yara rule here to detect similar KrustyLoader samples: https://github.com/synacktiv/krustyloader-analysis/blob/main/KrustyLoader.yar. It searches specific strings I mentioned and some AES routines.\r\nConclusion\r\nRust payloads detected by Volexity team turn out to be pretty interesting Sliver downloaders as they were\r\nexecuted on Ivanti Connect Secure VPN after the exploitation of CVE-2024-21887 and CVE-2023-46805.\r\nKrustyLoader – as I dubbed it – performs specific checks in order to run only if conditions are met. The fact that\r\nKrustyLoader was developed in Rust brings additional difficulties to obtain a good overview of its behaviour. A\r\nscript as well as a Yara rule are publicly available to help detection and extraction of indicators.\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 7 of 8\n\nIf any organization needs assistance in doubt removal or responding to a compromise, please feel free to contact\r\nSynacktiv.\r\nSource: https://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nhttps://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises\r\nPage 8 of 8",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.synacktiv.com/publications/krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises"
	],
	"report_names": [
		"krustyloader-rust-malware-linked-to-ivanti-connectsecure-compromises"
	],
	"threat_actors": [
		{
			"id": "b2e48aa5-0dea-4145-a7e5-9a0f39d786d8",
			"created_at": "2024-01-18T02:02:34.643994Z",
			"updated_at": "2026-04-10T02:00:04.959645Z",
			"deleted_at": null,
			"main_name": "UNC5221",
			"aliases": [
				"UNC5221",
				"UTA0178"
			],
			"source_name": "ETDA:UNC5221",
			"tools": [
				"BRICKSTORM",
				"GIFTEDVISITOR",
				"GLASSTOKEN",
				"LIGHTWIRE",
				"PySoxy",
				"THINSPOOL",
				"WARPWIRE",
				"WIREFIRE",
				"ZIPLINE"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "6ce34ba9-7321-4caa-87be-36fa99dfe9c9",
			"created_at": "2024-01-12T02:00:04.33082Z",
			"updated_at": "2026-04-10T02:00:03.517264Z",
			"deleted_at": null,
			"main_name": "UTA0178",
			"aliases": [
				"UNC5221",
				"Red Dev 61"
			],
			"source_name": "MISPGALAXY:UTA0178",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434656,
	"ts_updated_at": 1775791474,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/f80f01b9908359c52c3af60fa5e4bb9a202acab3.pdf",
		"text": "https://archive.orkl.eu/f80f01b9908359c52c3af60fa5e4bb9a202acab3.txt",
		"img": "https://archive.orkl.eu/f80f01b9908359c52c3af60fa5e4bb9a202acab3.jpg"
	}
}