Skip to main content

Dylib Hijacking

Another way of injecting code is by performing dylib hijacking or a dylib proxying attack, which is similar to dll hijacking on Windows. 

 

 

LC_RPATH and LC_LOAD_DYLIB Load Commands 

LC_RPATH: Contain paths to directories 

LC_LOAD_DYLIB: Contain paths to specific dylibs to be loaded. The dylib path might be prefixed with @rpath, a variable that will be resolved during execution using the paths within the LC_RPATH commands. 

 

An example: 

 

Multiple LC_RPATH commands pointing to different directories, as well as LC_LOAD_DYLIB commands with an @rpath prefix. LC_LOAD_DYLIB always point to specific binaries. 

 

 

ImageLoader::recursiveLoader: Obtain the various run paths of the dylibs that should be loaded and attempt to load them. 

ImageLoaderMachO::doGetDependentLibraries: Find the  dylibs to be loaded. It iterate through the mach-o LOAD commands and build a list of dependent shared libraries. The 4 most important commands for dylib hijacking: 

LC_LOAD_DYLIB: The generic command to load a dylib. 

LC_LOAD_WEAK_DYLIB: Works the same, but if the dylib is not found, execution continues without error. 

LC_REEXPORT_DYLIB: Proxy/re-export the symbols from a different library. 

LC_LOAD_UPWARD_DYLIB: Used when two libraries depend on each other(upward dependency) 

If any of the four are found, it will add the specified library to the list of dylibs to be loaded later. 

For LC_LOAD_UPWARD_DYLIB, if LC_LOAD_WEAK_DYLIB is being used, the required flag is set to FLASE, which means that a process can start without crashing even if a weak dylib is not found. 

 

Then, rpath variables will be resolved by recursiveLoadLibraries. 

These run-time dependent search paths can be specified with the LC_RPATH command, typically in a form similar to @rpath/libssl.1.0.0.dylib. If the runtime needs to find the dylib's location dynamically upon load, the linker will rely on these search paths. 

 

When LC_RPATH is found, some checks are performed. If the command starts with the @loader_path string, which is the directory the binary is located. context.allowAtPaths was set depending on whether the process was considered restrict or not. In the case of restricted binaries, the command will be ignored while processing the main binary, the rpath will be resolved by the realpath function and added to a list. 

recursiveLoadLibraries iterate through each dylib it found and try to load them within a try-catch block. If any error occur, an exception is thrown. In the catch block, the exception will be sent upwards, unless the required is set to FALSE, and we recall that required was set for the LC_LOAD_WEAK_DYLIB load command, it means that if the load command is used and an error occurs, the app won'r error out and will continue execution. 

 

@rpath variable will be replaced by each run path-dependent search path that was found when parsing the LC_RPATH command, these locations are searched sequentially, and the first found will be loaded. 

 

 

Assume we encounter a LC_LOAD_DYLIB command with the value of @rpath/example.dylib, as well as two LC_RPATH commands with the values /Application/Example.app/Contents/OldDylibs/ and /Application/Example.app/Contents/Dylibs, two possible paths: 

/Application/Example.app/Contents/OldDylibs/example.dylib 

/Application/Example.app/Contents/Dylibs/example.dylib 

 

Two possible hijickable scenarios: 

  1. The application uses the LC_LOAD_WEAK_DYLIB command, but the actual dylib does not exist. 

  2. @rpath search path order points to folders where the dylib is actually not found. For example, /Application/Example.app/Contents/OldDylibs/example.dylib does not exist, we can place our dylib in the first location. 

  3. dylib proxying: not a real hijack, we need to tamper with the app. If we have write access to the dylib file, we can swap the intended dylib with our own dylib by naming the original dylib pointing our dylib to the real one, re-exporting all of its offered functions. However, if an app is compiled with hardened runtime or library validation enabled, and does not have the com.app.security.cs.disable-library-validation entitlement set, dyld won't load libraries that were signed with different team IDs. 

 

 

Discover: 

Use Dylib Hijack Scanner(DHS) tool. 

Manual:  

  1. Use otool to display all load commands of the application 

 

  1. The library does not exist 

  1. Check the code-signing properties of the lib, it has lib validation disabled 

  1. But the location is only writable as root. Hunt for rpath-based hijacking.  

@loader_path points to the directory containing the binary that includes the load command. For airhost, the loader_path is /Application/zoom.us.app/Contents/Frameworks/airhost.app/Contents/MacOS, therefore the run time-dependent paths will be resolved as: /Application/zoom.us.app/Contents/Frameworks/airhost.app/Contents/Framework, and /Application/zoom.us.app/Contents/Frameworks/. 

 

  1. We have a list of the paths, retrieve the related dylibs that will be resolved. 

  1. Check if any of them are absent. libcrypto.dyliblibssl.dylib can be found only in the second location. 

  1. However, the app is hardened, Library validation is not disabled. 

 

 

 

Another attempt 

  1. Examine burpsuite 

  1. Check for any dylibs using the @rpath prefix. Note that the version is 1.0.0. 

 

  1. No dylibs in the executable's directory. 

  1. Verify th entitlements, dylib injection is possible. 

  1. We need to ensure the version is matched, and the dylib export everything expected by the app to avoid crashing. The original lib can be found at /Applications/Burp Suite Community Edition.app/Contents/PlugIns/jre.bundle/Contents/Home/lib/libjli.dylib. 

  2. Create a simple POC: 

  1. Compile the dylib: gcc -dynamiclib -current_version 1.0 -compatibility_version 1.0 -framework Foundation hijack.m -Wl,-reexport_library,"/Applications/Burp Suite Community Edition.app/Contents/PlugIns/jre.bundle/Contents/Home/lib/libjli.dylib" -o hijack.dylib. -current_version 1.0 -compatibility_version 1.0 specifies the version,  -Wl,-reexport_library,"/Applications/Burp Suite Community Edition.app/Contents/PlugIns/jre.bundle/Contents/Home/lib/libjli.dylib instructs gcc which dylib to re-export. 

  2. LC_REEXPORT_DYLIB load command uses the @rpath variable to find the original dylib. We want to avoid self-reference, we need to specify the exact path location using install_name_tool: install_name_tool -change @rpath/libjli.dylib "/Applications/Burp Suite Community Edition.app/Contents/PlugIns/jre.bundle/Contents/Home/lib/libjli.dylib" hijack.dylib 

  3. It works. 

  1. Copy the dylib and run the app. 

 

 

Dlopen 

It occurs when an app tries to load a dylib with the dlopen function without specifying the full path. Then, dyld will search through different paths. In summary, dlopen will search the paths set by various environment variables, followed by the local directory. If the environment variables are not set, the search path will default to the following (as noted at DYLD_FALLBACK_LIBRARY_PATH): 

  1. $HOME/lib 

  2. /usr/local/lib 

  3. /usr/lib 

  4. current directory 

 

However, if the app is set SUID/GUID, environment variables are ignored, only the /usr/lib directory will be searched, and the location is protected by SIP, making it impossible for us to hijack a restricted binary.