History

I think everyone remembers the scandal (around November 2019) about an app in which the camera was activated in the “background” due to a bug. If you missed it, quickly Google it. The online media was full of these kinds of articles at that time.

Then my mind did not let me sleep…

Camera Permission on iOS

Description from Apple:

In iOS, the user must explicitly grant permission for each app to access cameras and microphones. Before your app can use the capture system for the first time, iOS shows an alert asking the user to grant your app access to the camera, as shown below. iOS remembers the user’s response to this alert, so subsequent uses of the capture system don’t cause it to appear again. The user can change permission settings for your app in Settings > Privacy.

Camera API

The Camera API usage is pretty straightforward. You define which camera hardware you would like to use, how to show a preview, and where to save the files.

The Problem

As you see from the documentation above, once the user grants the Camera Permission for an app, it is granted. Forever.

Anyone can make an application that asks for Camera Permission. e.g. taking a photo, uploading an image, setting profile picture, “scanning” document, etc. You can find many use-cases that can justify camera usage. The application can ask it for a valid reason, but after that, you don't know what can happen.

And do you know if the app is still using your camera while you are in the app? Probably not.

Let your imagination fly a bit and think about what you could do if you could have access to the user's camera anytime in the app.

You have a home decor online shop. While the user is searching for a certain item in your app you could look around in his/her house, see the colors, furniture, and your app could start suggesting extra items in matching style.

Or:

Your main revenue stream is displaying ads in your app. What if you could monitor the user's reaction to an ad? Then you could offer to your advertiser more insights about what kind of ads a certain type of user likes.

This information would be pretty valuable, right? Ethical? I don't think so.

Proof of Concept

The Good

Note: If you would like to go straight to the code, here you are: HiddenCamera app on GitHub.

As a first step, you have to define the NSCameraUsageDescription key Info.plist file of your application. You have to define a purpose string for this key why your app needs this permission. Obviously, taking pictures… right?

This description will be part of the alert message that will be shown to the users the first time when they use your app.

So, let's take a look at our CameraViewController.

We need to initialize an AVCaptureSession.

let session = AVCaptureSession()

This session is the base of everything. Here we can attach our input/output elements.

var deviceInput: AVCaptureDeviceInput!

session.sessionPreset = AVCaptureSession.Preset.vga640x480

// acquire the camera device
guard let device = AVCaptureDevice
    .default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,
             for: .video,
             position: AVCaptureDevice.Position.front) else {
                return
}

do {
    deviceInput = try AVCaptureDeviceInput(device: device)
    guard deviceInput != nil else {
        print("error: can't get deviceInput")
        return
    }
    
    // add the Input device to the session
    if self.session.canAddInput(deviceInput){
        self.session.addInput(deviceInput)
    }
    
    // define a Video output device to capture video frames from the camera
    videoDataOutput = AVCaptureVideoDataOutput()
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    videoDataOutputQueue = DispatchQueue(label: "VideoDataOutputQueue")
    videoDataOutput.setSampleBufferDelegate(self, queue:self.videoDataOutputQueue)
    
    if session.canAddOutput(self.videoDataOutput){
        session.addOutput(self.videoDataOutput)
    }
    
    //define a Picture output device to capture still images from the camera
    stillImageOutput = AVCapturePhotoOutput()
    
    if session.canAddOutput(stillImageOutput) {
        session.addOutput(self.stillImageOutput)
    }
    
    videoDataOutput.connection(with: .video)?.isEnabled = true
    
    // define the camera preview view
    previewLayer = AVCaptureVideoPreviewLayer(session: self.session)
    previewLayer.videoGravity = AVLayerVideoGravity.resizeAspect
    
  	// ...
    
    // and start running the capture session
    session.startRunning()
} catch let error as NSError {
    deviceInput = nil
    print("error: \(error.localizedDescription)")
}

The startRunning() method kicks in the whole capturing process. (This will be important later!)

Now we can listen to the video delegate callbacks:

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    // do stuff here with the video
}

Then to the photo delegate callbacks:

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    //process single image
    guard let imageData = photo.fileDataRepresentation()
        else { return }
    
    let captureImageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
    let image = UIImage(data: imageData)
    captureImageView.image = image
    
    //show the captured image
    let centerView = UIView(frame: CGRect(x: UIScreen.main.bounds.size.width / 2 - 100,
                                          y: UIScreen.main.bounds.size.height / 2 - 100,
                                          width: 200,
                                          height: 200))
    centerView.backgroundColor = UIColor.red
    centerView.addSubview(captureImageView)
    
    self.view.addSubview(centerView)
    
}

As you can see, nothing evil. We ask for permission, the user grants it, we show the video preview and the captured photo.

Let's see how it works:

The Bad

We can take a look on the CameraViewControllerHidden class. Same thing as above, but unfortunately by mistake we forgot to attach our video preview to the view.

// ...
do {
    deviceInput = try AVCaptureDeviceInput(device: captureDevice)
    guard deviceInput != nil else {
        print("error: can't get deviceInput")
        return
    }
    
    if self.session.canAddInput(deviceInput){
        self.session.addInput(deviceInput)
    }
    
    stillImageOutput = AVCapturePhotoOutput()
    
    if session.canAddOutput(stillImageOutput) {
        session.addOutput(self.stillImageOutput)
    }
    
    // we can also attach videoDataOutput from the previous example to get all the video frames
    // but for now image capturing is enough

    // and whoopsie, we forgot the preview :(
    
    session.startRunning()
}
// ...

What do we see now?

No permission was asked (previously it was granted forever), no video preview. So we think everything is alright, we are browsing our feed, watching ads, liking/disliking photos… meanwhile someone is spying on us:

Just to give you a little peace of mind: at least the camera stream is blocked while the app is in the background.

Jailbreak to the Rescue

What is Jailbreaking?

Jailbreaking is the privilege escalation of an Apple device for the purpose of removing software restrictions imposed by Apple on iOS, iPadOS, tvOS and watchOS operating systems. This is typically done by using a series of kernel patches. Jailbreaking permits root access in Apple's mobile operating system, allowing the installation of software that is unavailable through the official Apple App Store. Many types of jailbreaking are available, for different versions.

Apple publicly disapproves of jailbreaking.

There are a lot of pros and cons of jailbreaking. You can find some of these on the above-mentioned links.

Jailbreaking is for users who know what they are doing. And these people like to jailbreak for tweaking their devices. Just take a look at all the magic you can do on your iOS device once it is jailbroken.

The Tweak

The tweaks are iOS applications, however

…many of them are not typical self-contained apps but instead are extensions and customization options for iOS and its features and other apps (commonly called tweaks). Users install these programs for purposes including personalization and customization of the interface by tweaks developed by developers and designers, adding desired features and fixing annoyances…

PrivacyGuard - iOS

Note: This post is not elaborating how to develop a Jailbreak tweak. That's for a future post. If you want to hear more about it, please subscribe to the content notifications.

As you read above, most of the Jailbreak tweaks are extending the functionality of already existing apps. How? In a nutshell, they hook into existing methods in the app and react when the method is called. They can completely override it, alter its behavior, or just trigger a jailbreak code and call the original method after it.

This is exactly what we will do.

Note: If you would like to go straight to the code, here you are: PrivacyGuard-iOS tweak on GitHub.

As we saw above in the camera app examples, everything starts with the startRunning() method on AVCaptureSession.

It's time to hook!

#import <UIKit/UIKit.h>

%hook SpringBoard
-(void) applicationDidFinishLaunching:(id)arg {
	%orig(arg);
	UIAlertView *lookWhatWorks = [[UIAlertView alloc] initWithTitle:@"PrivacyGuard Tweak"
		message:@"Your privacy guard is running 😎"
		delegate:self
		cancelButtonTitle:@"OK"
		otherButtonTitles:nil];
	[lookWhatWorks show];
}
%end

%hook AVCaptureSession

// Hooking an instance method with no arguments.
-(void) startRunning {
	NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
	NSLog(@"PrivacyGuard startRunning: %@", appName);

	UIView *statusBar = [[UIApplication sharedApplication] valueForKey:@"statusBar"];
	statusBar.backgroundColor = [UIColor greenColor];

	%orig;

}

-(void) stopRunning {
	NSLog(@"PrivacyGuard stopRunning");

	UIView *statusBar = [[UIApplication sharedApplication] valueForKey:@"statusBar"];
	statusBar.backgroundColor = [UIColor clearColor]; // or we could save and restore the original one

	%orig;
}

// Always make sure you clean up after yourself; Not doing so could have grave consequences!
%end

Easy (for now) and beautiful!

(The first part, hooking into Springboard, just a helper notification to show the tweak is initialized.)

We hook directly into AVCaptureSession's startRunning() method. Whenever this method is called we log to the console the current app that is using the camera (initiated the capturing) and change the status bar color to green to visually notify ourselves. After this is done we just call the original (%orig) method and let the flow continue. Hooking into the stopRunning() method is useful to revert our changes after the camera is not in use anymore.

How does it look while using the example apps above?

When you have the preview visible:

And when we are in evil mode:

Also, in the logs we can see the relevant app info:

And no hidden surprises anymore.

You can go further. Showing a popup, notification, or a little icon in the status bar while the camera is in use. It is all up to you!

Note: PrivacyGuard-iOS is not available in any Jailbreak repository (yet). If you have the urge to try it, please let me know. But also feel free to compile it and tweak it for yourself.

Note: If there are other ways of accessing the camera stream those were not part of this investigation. Probably there you can use the same approach.

Conclusions

Jailbreak Tweaks

The tweaks made by the Jailbreak community are amazing. Lot of smart ideas, solutions for daily annoyances.

Having the possibility to use a jailbroken device and develop your own tweaks for your development work can be really interesting. It opens your possibilities, you have more access to the system, you can debug more things, private APIs.

It is brilliant.

Camera API

I think you can draw your own conclusion.

Be aware of your apps and used services. Maybe you don't even need those apps, just try to use the web version of those services. Check the granted permissions of your apps, maybe for your use-case it does not need extra permission.

Sensitive permissions should not be granted forever. Hopefully, Apple will fix this issue soon like Google is doing it on Android 11 now.

Disclaimer

I don't say that any of these things are happening in any apps on the market and I don't encourage you to jailbreak your iOS device.

But better to be prepared and be more privacy-conscious. Having these kinds of data collected about a user/content/ad could be valuable for certain parties.

Support

Did you enjoy my story? There is more in the pipeline… 😉

Do you want to know more insights? Would you like to discuss the used techniques, or would you like to see some part of the code? Then consider becoming a monthly supporter on Patreon via my Tweaked.Tech initiative.

Become a Patron Become a Patron

Why?

  • The articles are published on Patreon first.
  • There is a follow-up article that shares insights, used techniques, architecture, infrastructure.
  • You can get early access to projects in the Beta phase.
  • We can discuss your ideas, your doubts, and the current issues you face in your project.
  • You keep me going, it is a big boost to my motivation!

I really appreciate a one time support too, you can do it via Buy Me A Coffee.

Buy me a coffee Buy me a coffee

Thanks for reading my story, I am grateful that you were here until the end.
Stay tuned for the next one!