"Screen leakage"
Background
Backing to 2015, Google introduced the MediaProjectionManager API in Android Lollipop, gave applications the ability to record the device’s screen. While this feature is incredibly powerful, it also raises some privacy and security concerns. Therefore, Android handle screen cast with caution, for example
- SystemUI displays a confirm dialog to warn user while initiating the cast recording session,
- a casting indicator is always on the StatusBar to notify user that they are been watched,
- a togglable Screen Cast Tile would show up in the Quick Settings panel, along with its current status and the name of the casting application, clicking the tile leads SystemUI to immediately terminate the casting sessions
Assuming that an application called CaptureCat is going to implement screen casting functionality, the following is an overview of the process.
Start casting
Spare the hassle process of user grants, permission verification and condition validation, start casting mainly involves three procedures
-
Request
MediaProjectiontokenOnce the user agreed the confirm dialog, the SystemUI will request a new
MediaProjectionfromMediaProjectionManagerServiceforCaptureCatand redirect theMediaProjectiontoken back toCaptureCat -
Set current casting session
CaptureCatcallsIMediaProjection.startto notifyMediaProjectionManagerServicethat it will start casting. Upon receiving the call,MediaProjectionManagerServicewill- terminate current ongoing cast session (if there is one)
- notify the SystemUI to display the cast indicator, set cast tile state to “on”, and display
CaptureCat’s name on the tile, - set the
MediaProjectionManagerService.mProjectionGrantfield to theMediaProjectionprovided byCaptureCat, which acknowledge thatCaptureCatis the app who’s carrying out the screen casting
-
Create a privileged
VirtualDisplayDevicewhich featuresVIRTUAL_DISPLAY_FLAG_AUTO_MIRRORflagAlthough
CaptureCatis unanimous the casting app, it has not received any screen frames so far. To receive these frames,CaptureCatmust create a privilegedVirtualDisplayDeviceand associate it with the surface on whichCaptureCatwants to render the frames. To achieve this,CaptureCatcallIDisplayManager.createVirtualDevicewith the following parameterssurface, the surface to which CaptureCat wants to render frame contents. This surface can be obtained using ImageReader, MediaRecorder, or SurfaceView.callback, an instance ofVirtualDisplayCallback, which is a binder that will be called back whenever there is a state change in the created display.flag, i.eVIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,DisplayManagerServiceposed some restriction on calling app if you provide this flag, one of which is that the calling app must be the ongoing screen casting app. Fortunately,CaptureCathappens to satisfy
Once the call reaches the
DisplayManagerService, it will delegate theVirtualDisplayAdapterto create the privilegedVirtualDisplayDevicefor theCaptureCat, Internally, theVirtualDisplayAdapterstores the createdVirtualDisplayDevicein itsmVirtualDisplayDevicesfield (an ArrayMap), using thecallbackparameter provided byCaptureCatas the key. Screen frames are then delivered continuously to the surface.
Terminate casting
What happens when a user decides to terminate the current screen casting by toggling the casting tile?
- The
SystemUIcallsMediaProjectionManager.stopActiveProjection. - The
MediaProjectionManagerServicereceives the IPC call and looks for the current ongoing casting session in itsmProjectionGrantfield. - The
mProjectionGrantfield triggers theIMediaProjectionCallbackcallback thatDisplayManagerServiceinstalled in it. - The
DisplayManagerServiceretrieves theappTokeninside thatcallback, looks for the correspondingVirtualDisplayDeviceby searching themVirtualDisplayDevicesArrayMap with theappTokenas the key. - Once the
VirtualDisplayDeviceis found,DisplayManagerServiceclears thesurfacefield of thatVirtualDisplayDeviceand sets itsmStoppedfield to true, thus terminating the screen casting.
Exploit
As described above, mVirtualDisplayDevices is an ArrayMap, and the key is provided by the CaptureCat app (the callback parameter). If the CaptureCat app inserts another non-privileged VirtualDisplayDevice with the same key into the ArrayMap during casting, the value will be overridden. This can be achieved by simply call IDisplayManager.createVirtualDevice without set the VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR flag but with the same callback as the first call.
Then when user decide to terminate ongoing casting, the non-privileged VirtualDisplayDevice will be found, clear its surface and set its mStopped states. However, this will not terminate the privileged VirtualDisplayDevice, then the SystemUI will dismiss the casting indicator, set the casting tile state to “off,” and remove the current casting application name from the tile. This could mislead the user into believe that no casting is currently taking place, causing them to input sensitive information (such as a banking password) without realizing that their screen is still being silently recorded by CaptureCat
I came across this vulnerability and reported it to Google on June 28. However, after approximately a month (July 29), they responded by rejecting the report and categorizing it as “Won’t fix.” Their reasoning was that “the user initially granted access to the malicious app to record the screen”. Given this outcome, I have decided to disclose the vulnerability here, with the hope that it will raise awareness among Android users. It is essential to understand that even if you explicitly close the screen casting, there is no casting indicator, and the “Screen casting” tile suggests no ongoing recording, you may still be under surveillance. Therefore, it is crucial to exercise caution in all your activities.
Windfall
After discussing this vulnerability with Mishaal Rahman (BTW his twitter is a goldmine for Android enthusiasts), he pointed towards a relevant compatibility change in the Android 14 beta 4, After exploring it briefly, I found some intriguing aspects, yet it falls short in mitigating the aforementioned vulnerabilities.
Though not explicitly documented, it is possible to reuse the MediaProjection token returned by the SystemUI. For instance, when a user explicitly terminates an ongoing screen cast session, the app can invoke IMediaProjection.start and IDisplayManager.createVirtualDisplay(mediaProjection, {flag = VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface }, callback) again to initiate screen casting. This action does not trigger a confirmation dialog (since you already have the MediaProjection), but it will display the casting indicator/QuickSetting tiles with the state “on” and the name of the casting application, ensuring that the user is not misled.
In Android 14, a new AIDL call, IMediaProjection.isValid() , is introduced to verify the legitimacy of the MediaProjection provided by the app. The criteria for the check are as follows:
- If the
MediaProjectionwas created within the last 5 minutes and has not invokedIMediaProjection.startor created a privileged VirtualDisplayDevice, it is considered legal. - Otherwise, the judgement is made based on the compat-change settings of the device.
1
<compat-change description="Determines how to respond to an app re-using a consent token; either failing or allowing the user to re-grant consent. <p>Enabled after version 33 (Android T), so applies to target SDK of 34+ (Android U+)." enableSinceTargetSdk="10000" id="266201607" name="MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT" />
Based on my observation, the implementation of this compatibility change is not yet complete (as there is no caller of isValid in beta 4). Once it is fully implemented, it will address the issue of MediaProjection reuse to some extent, but it will not impact the vulnerability discussed in the “Exploit” section.


