Can we cover the orange dot with out disabling SIP? — Alin Panaitiu
A little bit of background.
When macOS Monterey was introduced, Apple added an orange dot indicator that seems on prime of the whole lot each time the microphone is in use.
This has made lots of people very indignant and been broadly considered a nasty transfer.
Kidding, it was fairly a pleasant privateness addition truly. We might lastly see in realtime when an app used the microphone, and what app that’s.
However this wasn’t one thing that everybody wished.
For instance, when projecting live visuals on a big display screen, you may need to make the display screen background black to present the impression of floating results. That vibrant coloured dot within the nook breaks that phantasm.
It may also be a distraction for a particular group of individuals combating consideration deficit.
Oh, and it additionally seems in display screen recordings, which annoys me as effectively each time I need to demo one thing.
# First resolution
After information acquired out, Sydney San Martin posted a command-line implementation in a HackerNews thread known as undot which might seize the dot and transfer it off display screen, because it was only a regular window.
And I made a decision to make a straightforward to make use of app for it known as YellowDot (in my eyes, it seems yellow ¯_(ツ)_/¯)
# macOS 12.2
Apple took word of this, noticed it as a vulnerability, and promptly up to date their code to disable this technique of hiding the dot.
Certainly, it’s a vulnerability. Any app with Accessibility and Microphone Permissions might file audio with out your data.
The issue was that there was no means for the person to choose out of this.
A Privateness Indicator toggle that asks for password or biometric authentication might resolve this drawback for those that need a Mac to be a device that ought to work for them as they need.
# Second resolution
Tyshawn Cormier, one other developer aggravated by this, got here up with a distinct resolution a couple of days after macOS 12.2 was launched: RecordingIndicatorUtility.
It wanted disabling System Integrity Safety (SIP) as a result of it injected code contained in the WindowServer and ControlCenter processes. There, it had entry to strategies that dealt with the drawing of this privateness indicator, and it might simply set its opacity to 0 to cover it.
Fairly a intelligent resolution, however a bit too convoluted and apparently dangerous for most individuals.
Anyway, with no higher concept in hand, I up to date the YellowDot web page to level folks to Tyshawn’s app and proceeded to overlook about it.
# Nonetheless no actual resolution…
After some time I observed one thing odd in my Believable Analytics dashboard:
Even when the app was not working, and I wasn’t making any noise about it, it was nonetheless the second most accessed web page on The low-tech guys.
And most visitors was coming from Google. Individuals had been always in search of a technique to cover this dot.
I could not cease considering that perhaps there’s a means to do that with SIP enabled.
# What the dot?
So if it was not a easy window, how was this dot drawn on the display screen?
Was it drawn on the GPU utilizing Metallic? Was it pixels pushed instantly into the framebuffer?
No, it was nonetheless a rattling window.
// WindowInfo.swift
import Basis
import Cocoa
let information = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID)
print(information as! [[String: AnyObject]])

❯ swift WindowInfo.swift
[
"kCGWindowName": StatusIndicator,
"kCGWindowOwnerName": Window Server
"kCGWindowBounds": {
Height = 16; Width = 8;
X = 1953; Y = 16;
},
"kCGWindowOwnerPID": 403,
"kCGWindowNumber": 713,
"kCGWindowMemoryUsage": 2288,
"kCGWindowStoreType": 1,
"kCGWindowAlpha": 1,
"kCGWindowSharingState": 1,
"kCGWindowLayer": 0,
"kCGWindowIsOnscreen": 1,
]
However this time, it was a particular form of window. Accessibility APIs might not discover it, and thus there was no technique to transfer the window round.
On the lookout for the "StatusIndicator"
string inside SkyLight.framework factors us to:
int system_status_indicator_get_window(PKGSystemStatusIndicator_t*) {
// ...
_WSWindowSetProperty(r20, @"kCGSWindowTitle", @"StatusIndicator");
// ...
}
Seems just like the window is created with an inner technique known as WSWindowCreate
. This creates the window however doesn’t insert it within the CGSLocalWindows
hashmap.
This prevents person dealing with APIs from seeing the window, as they do a CGSWindowGetMapped
to fetch the window object, which seems inside that hashmap.
I used to be questioning if we might name these strategies instantly. They’re not exported so we are able to’t hyperlink them in Swift, however we are able to name them instantly by handle.
system_status_indicator_set_shape
seems attention-grabbing, it expects a CGRect, a CGPoint and a double. These are almost definitely the draw area, the origin and the measurement of the dot.
Let’s see if we are able to transfer the dot offscreen by altering the x
coordinate. I’m intercepting the tactic name utilizing Frida and setting the d0
register to 9999999
.
As a result of constructions like
CGRect
are handed by worth,d0
corresponds to the primarydouble
subject of the primary non-pointer argument: that’s thex
coordinate of the drawing area
yellowdot.js
const symbols = Module.enumerateSymbols('SkyLight')
operate addr(symname) {
return ptr(symbols.discover((sym) => sym.identify == symname).handle)
}
const system_status_indicator_set_shape = new NativeFunction(
addr('_ZL33system_status_indicator_set_shapeP26PKGSystemStatusIndicator_t6CGRect7CGPointd'),
'int', ['pointer', ['double', 'double', 'double', 'double'], ['double', 'double'], 'double']
)
Interceptor.connect(system_status_indicator_set_shape, {
onEnter: operate (args) {
console.log(
`system_status_indicator_set_shape(${args[0]}, rx: ${this.context.d0}, ry: ${this.context.d1}, rw: ${this.context.d2}, rh: ${this.context.d3}, x: ${this.context.d4}, y: ${this.context.d5}, measurement: ${this.context.d6})`
)
// Transfer the dot offscreen
this.context.d0 = 9999999
},
})
Load the script, connect to WindowServer and begin an audio recording:
> sudo frida --load yellowdot.js WindowServer
// Dot in ControlCenter icon
system_status_indicator_set_shape(0x600002f28540, rx: 1953, ry: 16, rw: 8, rh: 16, x: 1957, y: 24, measurement: 3)
// Dot in Full Display
system_status_indicator_set_shape(0x600002f28540, rx: 0, ry: 0, rw: 0, rh: 0, x: 2010, y: 16, measurement: 4)
Okay seems promising.
To name the operate we would want a pointer to a PKGSystemStatusIndicator
struct which we don’t know the format of. I requested ChatGPT about it and got here up with a adequate reply:
Now, how would one name a operate by handle? Isn’t Address space layout randomization an issue right here?
The dyld cache (the place SkyLight resides) will get loaded at a random handle at startup, however fortunately that stays the identical till restart.
We are able to use _dyld_get_image_vmaddr_slide
to get the bottom reminiscence handle of a particular module, then add to that the handle of the image that we acquired from disassembling SkyLight.
Then we forged that handle to a operate utilizing unsafeBitCast
and simply name it with the correct parameters.
yellowdot.swift
import Cocoa
import Basis
import MachO
func getDotWindowID() -> CGWindowID? {
let information = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID)
guard let home windows = information as? [[String: AnyObject]],
let window = home windows.first(the place: { ($0["kCGWindowName"] as? String) == "StatusIndicator" })
else {
return nil
}
return window["kCGWindowNumber"] as? CGWindowID
}
struct PKGSystemStatusIndicator {
var windowID: CGWindowID
var field_78: UInt32
var field_unk1: UnsafeMutablePointer<UInt64>?
var field_unk2: UnsafeMutablePointer<Any>?
}
guard let id = getDotWindowID() else {
exit(1)
}
var pkg = PKGSystemStatusIndicator(windowID: id, field_78: 0, field_unk1: nil, field_unk2: nil)
let libraryName = "/System/Library/PrivateFrameworks/SkyLight.framework/Variations/A/SkyLight"
var slide: Int = 0
let imageCount = _dyld_image_count()
let skyLightIndex = (0 ..< imageCount).first { index in
_dyld_get_image_name(index) == libraryName
}
guard let skyLightIndex, _dyld_get_image_vmaddr_slide(skyLightIndex) != 0 else {
print("The (libraryName) library isn't loaded within the dyld cache")
exit(1)
}
let slide = _dyld_get_image_vmaddr_slide(skyLightIndex)
print("The slide of the (libraryName) library is 0x(String(slide, radix: 16))")
typealias SystemStatusIndicatorSetShapeFunction = @conference(c) (UnsafeMutableRawPointer?, CGRect, CGPoint, Double) -> Void
let systemStatusIndicatorSetShape = unsafeBitCast(0x18514d130 + slide, to: SystemStatusIndicatorSetShapeFunction.self)
systemStatusIndicatorSetShape(&pkg, .zero, CGPoint(x: 999999, y: 16), 4)
> swift yellowdot.swift
The slide of the /System/Library/PrivateFrameworks/SkyLight.framework/Variations/A/SkyLight library is 0x1c98c000
fish: Job 1, 'sudo ./yellowdot' terminated by sign SIGSEGV (Deal with boundary error)
Hmm okay, it’s not really easy.
Debugging this with lldb drops us into WSWindowCreate
the place we are attempting to load one thing from the 0x0
handle. We attain that handle as a result of the inner SkyLight code expects to discover a knowledge construction stuffed with home windows at a particular handle in reminiscence.
Clearly, we don’t have such a factor, that knowledge solely exists contained in the WindowServer course of and we’ve no entry to it.
# systemstatusd
The “name by handle” concept doesn’t work. However one thing else jumped out to me once I was looking for a rogue LaunchDaemon.
/System/Library/LaunchDaemons/com.apple.systemstatusd.plist
This appears to run some form of server for dealing with these indicators. What occurs if we simply disable it?
❯ sudo launchctl bootout system/com.apple.systemstatusd
That labored! Beginning an audio recording not reveals the dot!
It even stops exhibiting the newly added blue location dot once I open the Climate app. Not precisely what I wished.
However does it work with SIP enabled?
❯ sudo launchctl bootout system/com.apple.systemstatusd
Boot-out failed: 150: Operation not permitted whereas System Integrity Safety is engaged
❯ sudo killall systemstatusd
killall: warning: kill -term 448: Operation not permitted
❯ sudo rm /System/Library/LaunchDaemons/com.apple.systemstatusd.plist
override rw-r--r-- root/wheel restricted,compressed for /System/Library/LaunchDaemons/com.apple.systemstatusd.plist? y
rm: /System/Library/LaunchDaemons/com.apple.systemstatusd.plist: Operation not permitted
Nope, they considered the whole lot.
Seems like with SIP enabled, these software program rendered coloured dots are simply as efficient because the inexperienced hardware-wired webcam LED.
With out a 0-day exploit, an attacker has no means of recording audio or location with out you noticing it. Nevertheless it additionally means you might be caught with this dot in your display screen even whenever you absolutely belief the software program.
The one resolution continues to be disabling SIP, and utilizing RecordingIndicatorUtility. Seems like Tyshawn simply changed the code injection technique with toggling this systemstatusd
server as an alternative, so it’s extra more likely to survive macOS updates.