Dynamic linker tricks you can try at home

The environment variables DYLD_INSERT_LIBRARIES (macOS) and LD_PRELOAD (Linux) let you instruct the dynamic linker to load dynamic libraries that are not normally part of an application. Furthermore, the libraries specified are loaded before any other libraries. This technique, which I will hereafter refer to as “library injection”, provides a lot of creative ways to modify and exploit software.

Hijacking dynamically linked functions

First and foremost, library injection allows you to override the implementation of dynamically linked functions. Sometimes larger (typically cross-platform) applications extract most of their business logic into a separate library. This extraction opens up an attack surface in which we can reimplement functions at our convenience.

Assume we are targeting a commercial application with the intent of extending the trial period by one week. From our analysis we know the application calls a (dynamically-linked) function, days_left, to get the number of days left in the trial. The code below shows how we can reimplement this function while also preserving the implementation of the old one.

#include <dlfcn.h>
#include <unistd.h>

int (*old_days_left)(void) = NULL;

int days_left(void) {
  if (!old_days_left) {
    old_days_left = dlsym(RTLD_NEXT, "days_left");
  }

  return old_days_left() + 7;
}

Simply compile this code as a shared library and use the environment variables mentioned earlier to load it. The steps to do this on macOS look like this:

$ clang -shared extend.c -o libextend.dylib
$ DYLD_INSERT_LIBRARIES=./libextend.dylib ./example_application

When our library is injected into the application, our implementation will be called instead, and we can enjoy a longer trial period.

See the man page for dlsym to better understand how the code above works. On macOS you may need to use DYLD_FORCE_FLAT_NAMESPACE=1 to ensure your implementation is used, and I’ve heard you may need to disable System Integrity Protection as well.

Persistent injection on macOS

Should we want to make our changes from the previous examples permanent, we could replace the app’s main executable with a script which launches the application with our library injected.

To do this, rename the main executable (you can just put an underscore in front if it), and create an empty text file with the main executable’s old name. Open the file in a text editor and add the following:

#!/bin/sh

CONTENTS=$(dirname "${0}")
CONTENTS=$(cd "${CONTENTS}/.."; pwd)

# Here is where you would configure your injection. Copy your compiled library
# to somewhere inside the app bundle, such as the Frameworks directory.
export DYLD_INSERT_LIBRARIES="$CONTENTS/Frameworks/libextendedtrial.dylib"

# If your application was called "HelloWorld", make that this script's name.
"$CONTENTS/MacOS/_HelloWorld" "$@"

Finally, make the script executable with chmod +x HelloWorld. Now you should be able to open the application normally, via the Dock, Spotlight, etc. and your library will always be loaded. Note that some applications manually verify their code signature, etc. and this method will not work in some of those cases.