{
	"id": "091ec5b1-16be-48c1-9965-2bff0dce1b28",
	"created_at": "2026-04-06T00:11:04.145276Z",
	"updated_at": "2026-04-10T03:37:04.560059Z",
	"deleted_at": null,
	"sha1_hash": "a89313932e57e6beeb9032c9737a36da817f8ca0",
	"title": "ChillyHell: A Deep Dive into a Modular macOS Backdoor",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1759248,
	"plain_text": "ChillyHell: A Deep Dive into a Modular macOS Backdoor\r\nBy Jamf Threat Labs\r\nArchived: 2026-04-05 14:17:28 UTC\r\nJamf Threat Labs performs a deep dive on the modular malware that has been mysteriously maligning macOS\r\nsince 2021.\r\nAuthors: Ferdous Saljooki, Maggie Zirnhelt\r\nIntroduction\r\nDuring routine sample analysis on VirusTotal, Jamf Threat Labs discovered a file that stood out due to a notable\r\nmethod of process reconnaissance being used. Despite the malware family having been documented in the past, it\r\nremains unflagged by antivirus vendors.\r\nThe sample is developer-signed and successfully passed Apple’s notarization process in 2021. Its notarization\r\nstatus remained active until these recent findings.\r\nBackground\r\nThe malware, known as ChillyHell, was originally unveiled in a private 2023 Mandiant report that loosely tied it\r\nto a threat actor targeting officials in Ukraine. This report documents a 2022 attack in which a threat actor, tracked\r\nby Mandiant as UNC4487, compromised a Ukrainian auto insurance website which government employees were\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 1 of 8\n\nrequired to use for official travel. The compromised site delivered the MATANBUCHUS malware, where\r\nattackers were then able to sell access to the infected systems at a high price.\r\nAlso in its report, Mandiant discovered additional malware dubbed ‘ChillyHell’, while searching for other\r\nsamples matching the signing certificate used to sign the MATANBUCHUS malware.\r\nThe technical details of the ChillyHell malware were not documented, but two macOS hashes were provided in\r\nthe original report:\r\neDrawMaxBeta2023.app/Contents/macOS/eDrawMaxBeta2023\r\nc52e03b9a9625023a255f051f179143c4c5e5636\r\nTEAMID: F645668Q3H\r\nNotarized\r\nchrome_render.app/Contents/MacOS/chrome_render\r\n87dcb891aa324dcb0f4f406deebb1098b8838b96\r\nTEAMID: R868N47FV5\r\nNot notarized\r\nThese two samples are different from each other, despite being assigned the same family name. For example, the\r\none titled eDrawMaxBeta came packaged with an SSH server compiled directly into it, whereas the one titled\r\nchrome_render did not.\r\nJamf Threat labs encountered a new ChillyHell sample uploaded to VirusTotal on May 2nd, 2025.\r\napplet.app/Contents/MacOS/applet\r\n6a144aa70128ddb6be28b39f0c1c3c57d3bf2438\r\nTEAMID: R868N47FV5\r\nNotarized\r\nDespite not making it to VirusTotal until 2025, this sample was also notarized in 2021 and has remained notarized\r\nup until these findings. The teamID matches that of the ChillyHell sample reported by Mandiant, and its\r\nfunctionality appears to be nearly identical. Furthermore, this notarized sample has been publicly hosted on\r\nDropbox since 2021 at https://dropbox[.]com/s/2fncbp2rv134z6y/applet.zip.\r\nTechnical analysis\r\nThe executable applet is a modular C++ backdoor developed for Intel architectures. Although packaged as\r\napplet.app, it does not function as a legitimate macOS applet. In genuine applets, Contents/MacOS/applet is\r\npaired with a compiled AppleScript within Contents/Resources/Scripts/ . This sample omits any scripts,\r\nshowing that both the app bundle and the executable name are used only as a disguise.\r\nOn execution, ChillyHell begins by invoking OS::StartupLogic() , which performs host profiling and attempts\r\nto establish persistence. If successful, it initializes command and control (C2) communication using\r\nTransportInit() and then enters its primary command loop in a function called mainCycle() .\r\nHost profiling\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 2 of 8\n\nThe OS::StartupLogic() function is responsible for preparing the compromised system, both by collecting basic\r\nenvironmental details and establishing persistence.\r\nAs part of its host profiling logic, ChillyHell executes a helper function, Utils::GetUsers() , which queries the\r\nlocal directory service to enumerate all user accounts. This is implemented by shelling out the following command\r\nThis command returns detailed information about all user records on the system in plist format. An additional\r\nfunction obtains the contents of the environment variables $HOME , $PATH and $SHELL . The results of these two\r\nfunctions are used to provide insight into the user’s home directory, available executable paths and preferred shell.\r\nChillyHell uses proc_listpids() to programmatically retrieve all active PIDs. It iterates over each one to collect\r\nstructured metadata about running processes. In addition, it executes the below command calling the function\r\nUtils::ParsePSCommand to capture uid, pid and command-line arguments for every process:\r\nAfter enumerating processes, ChillyHell collects the effective username under which it’s running. It does this by\r\ninstantiating a command object with the string \"whoami\" and executing it via Utils::RunCommand() . The result\r\nis retained if the command succeeds. An Env object is then initialized with default values setting the username to\r\n\"root\" followed by string \" $PATH \".\r\nNext, applet performs another check related to the user context under which it’s executing. It executes a system\r\ncall _getuid() to obtain the current user’s UID, then invokes Utils::FindUserByUID() to map this UID to a\r\nfull user record. If this lookup fails, it falls back to the whoami output collected earlier.\r\nPersistence\r\nAfter resolving the active user, ChillyHell initializes its installation logic.\r\nThree separate persistence mechanisms are supported depending on privilege level and installation state:\r\nLaunchAgent (User Context)\r\nIf running as a regular user, ChillyHell installs itself as a LaunchAgent using the StartupInstall::Install()\r\nmethod. It writes a plist to ~/Library/LaunchAgents/com.apple.qtop.plist and drops its main binary at\r\n~/Library/com.apple.qtop/qtop . The plist ensures execution on login, with RunAtLoad set to true.\r\nLaunchDaemon (Root/System Context)\r\nIf executed with elevated privileges, ChillyHell attempts to install itself as a system LaunchDaemon via\r\nStartupInstall::ReplaceInstallWithElevatedRights() . Upon success, it writes a plist to\r\n/Library/LaunchDaemons/com.apple.qtop.plist and places a copy of the main binary\r\nat/usr/local/bin/qtop . This ensures execution at boot with system privileges.\r\nShell profile injection\r\nAs a fallback persistence mechanism, ChillyHell can modify the user’s shell profile (.zshrc, .bash_profile or\r\n.profile). It uses StartupInstall::GetRcFilePath() to determine the appropriate shell configuration based on\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 3 of 8\n\nthe user’s shell and home directory. The persistence logic is handled by StartupInstall::InstallToShell() ,\r\nwhich calls StartupInstall::InsertLineToShellRCIfNotExist() to inject a launch command into the\r\nconfiguration file – only if the line doesn’t already exist. This ensures the malware is executed on each new\r\nterminal session.\r\nWhen ChillyHell is manually executed it daemonizes itself using OS::ForkyDaemon() . This function creates a\r\ndaemon using the old school Unix approach by performing a double-fork: creating a new session and redirecting\r\nall standard I/O to /dev/null to fully detach from the parent process. It also opens a decoy URL\r\n(https://google.com) in the default web browser for reasons not fully known at this time, although the current\r\nbelief is to minimize user suspicion.\r\nTimestomping\r\nAfter ChillyHell has created artifacts on the infected system, it replaces their associated timestamps to reduce\r\nsuspicion. A call to _utime() updates the creation and modification times of any created directories, plists and\r\nbinaries. If it does not have sufficient permission to update the timestamps by means of a direct system call, it will\r\nfall back to using shell commands touch -c -a -t and touch -c -m -t respectively, each with a formatted\r\nstring representing a date from the past as an argument included at the end of the command. The -a in the first\r\ncommand refers to access time and the -m in the second command refers to modification time.\r\nDue to the way macOS and the APFS file system work, adjusting the modified timestamp could inadvertently\r\nresult in a backdated birth timestamp. Meanwhile, the \"change\" timestamp ends up reflecting the time that the\r\nmanipulation took place.\r\nC2 initialization and communication setup\r\nThe TransportInit() function, which comes after the initial function OS::StartupLogic() , is used to establish\r\ncommunication with the C2.\r\nChillyHell performs a network reachability check to determine if it has access to the internet. By calling a function\r\nnamed WaitForNetworkReachability() which leverages Apple's System Configuration\r\nframework, applet attempts to reach Google's DNS server at 8.8.8.8. Specifically, it waits for the\r\nSCNetworkReachabilityFlags to be set to 0x2 indicating the host is reachable. Between requests it sleeps for 60\r\nto 120 seconds until the system is online.\r\nOnce an internet connection is confirmed, ChillyHell calls GateServersInit() to populate its internal list of C2\r\nservers. Two hardcoded IP addresses are used: 93[.]88.75.252 and 148[.]72.172.53 . Each address is paired\r\nwith multiple ports (53, 80, 1001, and 8080) and a transport type indicator (1 for DNS; 2 for HTTP). These\r\ncombinations are stored in a global list for use in later C2 communications, such as retrieving attacker-issued\r\ntasks.\r\nMain execution loop and tasking\r\nAfter establishing a network connection, ChillyHell runs its mainCycle() loop, handling tasks and carrying out\r\ncommands from the attacker-controlled C2 infrastructure.\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 4 of 8\n\nThe loop within the mainCycle() function does the following:\r\n1. Retrieve tasks\r\nVia the function tasks::getTasks() , ChillyHell fetches a list of task descriptors from the C2 server and\r\npopulates them into a local vector.\r\n2. Deduplicate\r\nVia the function Utils::pidExists() , applet compares the contents of running tasks to the local record of\r\nalready processed tasks. If the current task to execute does not duplicate work that is in progress or work that has\r\nbeen done already, then the task will be executed.\r\n3. Execute\r\nThe function tasks::execTask() is called when a task is deemed new. It dynamically instantiates the appropriate\r\nmodule class (ModuleLoader, ModuleUpdater, ModuleBackconnectShell or ModuleSUBF), and invokes its\r\nExecute() method. Each module receives the original task string, which is first base64-decoded. Modules then\r\nparse the decoded content for parameters. Some modules also report status updates to the C2. Successfully\r\nexecuted tasks are tracked by storing their process ID and payload in memory to prevent duplicate execution in\r\nfuture polling cycles.\r\n4. Sleep\r\nAt the end of each cycle, randoms::doSleep() is invoked with a randomized delay between 60 and 120 seconds\r\nto introduce variability in C2 polling.\r\nA diagram describing the steps taken during each iteration of the loop within mainCycle().\r\nNow that the outermost loop of the malware has been described, we can take a closer look at the elements that\r\nmake up the most interesting parts of the cycle.\r\nTask retrieval\r\nAs identified in the first step of the main execution loop, ChillyHell uses the tasks::getTasks() function to\r\nfetch new commands. This method constructs a C2 query string, sends the request and parses the response for task\r\npayloads.\r\nBuild the query\r\nThe function first calls tasks::getPrefix() , which constructs a prefix string composed of host-specific\r\nmetadata in the following order: OS identifier (3), group label (shadmins), hardware UUID (lowercased and\r\ndashless), a static literal (0) and a static version marker (114). This full prefix is appended to the C2’s base domain\r\nand used in a DNS TXT query to retrieve tasking commands.\r\nPoll for a response\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 5 of 8\n\nThe function next repeatedly queries the C2 by invoking Query() until a response is received. Between attempts,\r\nit sleeps for a randomized interval between 60 and 120 seconds. The final response is stored in a local string\r\nbuffer.\r\nParse response\r\nChillyHell iterates through each returned string, scanning for the marker TASK:. If found, it extracts the portion of\r\nthe string following this marker as a task payload which is queued for execution. Each new task is dynamically\r\nexecuted by instantiating a corresponding module and invoking its Execute() method.\r\nAdd to task list\r\nEach valid task is copied into a new string and appended to a vector passed into the function. This vector\r\naccumulates all discovered tasks during the current polling cycle is used in the main execution cycle to prevent\r\ntask duplication.\r\nBelow is an example DNS query that contains host-derived metadata used for tasking.\r\nC2 transport\r\nChillyHell communicates with its C2 infrastructure using the Query() function, which loops through a list of\r\npredefined gate servers, each associated with a transport type.\r\nEach gate entry includes both an address and a transport type. If a previously successful (“stable”) gate exists, it is\r\ntried first. Otherwise, the malware loops through all available gates until one returns a valid response with the\r\nmarker \"[+] Ok\". That gate is then saved as the new stable gate for future use.\r\nThe QueryGate() function dispatches based on the gate’s transport type:\r\nHTTP transport (type 2): Calls QueryHTTP() to fetch tasks from a remote server over standard HTTP(S).\r\nDNS transport (type 1): Calls QueryTXTRecords() to retrieve encoded task data via DNS TXT record\r\nlookups.\r\nIf the gate returns a valid response, it is parsed and stored into the string vector passed to getTasks() . If all gates\r\nfail, the stable gate is reset, and the polling logic will be retried in the next cycle.\r\nTask execution and Module dispatch\r\nOnce a task payload is retrieved and parsed, ChillyHell calls tasks::execTask() to execute it. This function\r\ndetermines the task type and uses a switch statement to instantiate the corresponding module:\r\nType 0 - ModuleBackconnectShell: Connects to a C2 IP and port, spawns a pseudo-terminal using forkpty()\r\nand relays input/output over the socket to maintain an interactive reverse shell. We noticed it uses the banner \"\\r\\n-\r\n--===Welcome to Paradise!===---\\r\\n\".\r\nType 1 - ModuleUpdater: Downloads a new version of the malware from the C2 server, replaces the current\r\nbinary and restarts itself calling ModuleUpdater::restartProcess() .\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 6 of 8\n\nType 2 - ModuleLoader: Downloads a payload from the C2 server, writes it to /tmp/kworker and then attempts\r\nto execute it using startProcess() . If execution is successful, it waits five seconds before deleting the file.\r\nType 4 - ModuleSUBF: Enumerates user accounts and performs password cracking. We were unable to retrieve\r\nthis tool from the server at the time of analysis and thus unable to guarantee its purpose. Despite this, we believe\r\nthis module targets Kerberos-based authentication because of the observed filenames, downloaded wordlists and\r\nbrute-force attempts.\r\nEach module extends a shared AbstractModule base class and implements its own Execute() method. The first\r\nthree module types, ModuleBackconnectShell, ModuleUpdater and ModuleLoadereach have their functionality\r\ndescribed effectively in the statements above. ModuleSUBF, however, contains more complex logic and warrants\r\nadditional details.\r\nModuleSUBF - Brute Forcing\r\nThis module (Type 4) performs local password cracking against user accounts. It consists of the following steps:\r\nRetrieves usernames\r\nTo begin, ModuleSUBF uses the method getUsernames() to parse /etc/passwd and extract local usernames,\r\nwhich are then stored for use in subsequent password brute-force attempts.\r\nDownloads tooling and wordlist\r\nThis module then calls downloadModule() to retrieve a brute-force tool from the C2 named ./kerberos. After\r\ndownloading the tool, ChillyHell invokes downloadWordlist() to fetch a password wordlist used in the attack.\r\nLaunches credential attacks\r\nThe module calls tryUser() for each discovered username. This function begins by constructing configuration\r\nstrings by replacing placeholders {USERNAME} and {WORDLIST_PATH}. The downloaded ./kerberos binary\r\nis then executed in a forked child process. Each process attempts to crack the user’s password by brute-forcing it\r\nwith the supplied username and wordlist. Successful guesses are likely written by the ./kerberos binary to a file\r\nnamed good.txt.\r\nValidates and extracts results\r\nThe module calls getFound() to parse the brute-force output and extract valid credential pairs. If a match is\r\nfound, applet verifies whether the associated cracking process is still running using Utils::pidExists() . If the\r\nPID is inactive, the module issues a SIGTERM to ensure cleanup and then removes the corresponding entry from\r\nmemory.\r\nThe above is a pseudocode representation of the Execute() function of ModuleSUBF.\r\nConclusion\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 7 of 8\n\nBetween its multiple persistence mechanisms, ability to communicate over different protocols and modular\r\nstructure, ChillyHell is extraordinarily flexible. Capabilities such as timestomping and password cracking make\r\nthis sample an unusual find in the current macOS threat landscape. Notably, ChillyHell was notarized and serves\r\nas an important reminder that not all malicious code comes unsigned.\r\nThe Jamf Threat Labs team would like to thank Google Threat Intelligence for sharing details regarding the\r\noriginal ChillyHell discovery, and Apple for working with us to quickly revoke the developer certificates\r\nassociated with this malware.\r\nIndicators of Compromise (IoCs)\r\nSubscribe to the Jamf Blog\r\nHave market trends, Apple updates and Jamf news delivered directly to your inbox.\r\nTo learn more about how we collect, use, disclose, transfer, and store your information, please visit our Privacy\r\nPolicy.\r\nSource: https://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nhttps://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/\r\nPage 8 of 8",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MISPGALAXY",
		"Malpedia"
	],
	"references": [
		"https://www.jamf.com/blog/chillyhell-a-modular-macos-backdoor/"
	],
	"report_names": [
		"chillyhell-a-modular-macos-backdoor"
	],
	"threat_actors": [
		{
			"id": "d9b39228-0d9d-4c1e-8e39-2de986120060",
			"created_at": "2023-01-06T13:46:39.293127Z",
			"updated_at": "2026-04-10T02:00:03.277123Z",
			"deleted_at": null,
			"main_name": "BelialDemon",
			"aliases": [
				"Matanbuchus"
			],
			"source_name": "MISPGALAXY:BelialDemon",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "5babb972-ee1e-4518-950b-54c252c38b49",
			"created_at": "2026-01-20T02:00:03.659017Z",
			"updated_at": "2026-04-10T02:00:03.911237Z",
			"deleted_at": null,
			"main_name": "UNC4487",
			"aliases": [],
			"source_name": "MISPGALAXY:UNC4487",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434264,
	"ts_updated_at": 1775792224,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/a89313932e57e6beeb9032c9737a36da817f8ca0.pdf",
		"text": "https://archive.orkl.eu/a89313932e57e6beeb9032c9737a36da817f8ca0.txt",
		"img": "https://archive.orkl.eu/a89313932e57e6beeb9032c9737a36da817f8ca0.jpg"
	}
}