Beyond the good ol' LaunchAgents - 33 - Widgets

This is part 33 in the series of “Beyond the good ol’ LaunchAgents”, where I try to collect various persistence techniques for macOS. For more background check the introduction.

Widgets are application extensions you can place on your desktop to display some key information from your main app. Although Apple says developers shouldn’t implement any functionality there and just use them as a display, they are still apps that run on their own and we can run code inside them. We will explore how we can create them, use them as persistence and also investigate what widgets will automatically run on our system.

Creating Widgets Link to heading

I’m not a developer myself and also relied on external source to create one. I think others do a much better job explaining it, and walking through, so I will just refer here to the ones I found useful.

Getting started with WidgetKit

How to Build a Widget in Swift with WidgetKit

The one I created based on the video above is uploaded to my theevilbit/macos Github repository. This is just a sample widget based on that tutorial, nothing extra.

When the application with a widget is compiled, essentially they will become an appex bundle within our app.

Although widgets are “application extensions” and one would expect they are loaded into a dedicated process, they are in fact run on their own. We can find this if we list processes with “Widget” in their name.

csaby@max ~ % pgrep -lf Widget
743 /System/Applications/Weather.app/Contents/PlugIns/WeatherWidget.appex/Contents/MacOS/WeatherWidget
757 /System/Applications/Calendar.app/Contents/PlugIns/CalendarWidgetExtension.appex/Contents/MacOS/CalendarWidgetExtension
759 /System/Applications/Clock.app/Contents/PlugIns/WorldClockWidget.appex/Contents/MacOS/WorldClockWidget

Loading widgets Link to heading

To run or load a widget, a user would normally open widgets, select “Edit Widgets”, which brings up the window below. Editing Widgets

In this window we can drag & drop widgets, select the size we want, etc… but it’s very GUI interaction heavy and from an attacker point of view this is very inconvenient, essentially impossible to persist our widget this way. We need to find a way to load a widget programmatically.

Eventually I found that widgets are started by NotificationCenter and their active configuration is stored in the ~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist configuration file. Because this plist is located inside a container, it enjoys TCC’s App Data protection, thus normally we can’t touch it unless we bypass TCC or have Full Disk Access rights granted. This makes this persistence slightly inconvenient.

Let’s explore how this PLIST is structured, how we can analyze it and persist via editing it.

com.apple.notificationcenterui.plist Link to heading

This property list turns out to be very complex as we will see shortly. Let’s take a look at it.

csaby@max widgets % plutil -p ~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist | head -n 20
{
  "fontStyle" => 0
  "last-analytics-stamp" => 739606677.208112
  "widgets" => {
    "DesktopWidgetPlacementStorage" => {length = 95, bytes = 0x62706c69 73743030 d2010203 045f1014 ... 00000000 0000003a }
    "instances" => [
      0 => {length = 2772, bytes = 0x62706c69 73743030 d2010203 04567769 ... 00000000 00000aaa }
      1 => {length = 3015, bytes = 0x62706c69 73743030 d2010203 04567769 ... 00000000 00000b9d }
      2 => {length = 4532, bytes = 0x62706c69 73743030 d2010203 04567769 ... 00000000 0000118a }
    ]
    "vers" => 1
    "widgets" => [
      0 => {length = 3405, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000d1b }
      1 => {length = 1800, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 000006d6 }
      2 => {length = 1523, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 000005c1 }
      3 => {length = 1426, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000560 }
      4 => {length = 3536, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000d9e }
      5 => {length = 3300, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000cb2 }
      6 => {length = 1664, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 0000064e }
      7 => {length = 1943, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000765 }
...

We find that there is a shorter list of base64 encoded items in widgets.instances and much more in widgets.widgets. instances will store information about the widgets that have been configured to run, while widgets will contain a list of all available widgets (including those from your iOS device). DesktopWidgetPlacementStorage stores information about how the widgets are placed, but only if the widget is placed directly on the Desktop, and not as an item in the notification bar.

I wrote a short shell script which takes a plist as an input and will decode each embedded base64 strings, which is normally inside the data tags. Running this script, what we find is that each widgets.instances decodes as follows.

<dict>
	<key>uuid</key>
	<string>403B6E51-3F19-4268-981E-10BF4F6F76DE</string>
	<key>widget</key>
	<data>
	YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMS
	AAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGsCwwZISIjKisxNTk9VSRudWxs
	1g0ODxAREhMUFRYXGF8QEWV4dGVuc2lvbklkZW50aXR5V2ludGVudDJUa2luZF8QEmFj
	dGl2aXR5SWRlbnRpZmllclYkY2xhc3NWZmFtaWx5gAKAB4AGgACACxAB1BobHBEWHh8g
	XxAQZGV2aWNlSWRlbnRpZmllcl8QGWNvbnRhaW5lckJ1bmRsZUlkZW50aWZpZXJfEBll
	eHRlbnNpb25CdW5kbGVJZGVudGlmaWVygACABIADgAVfEBhjb20uYXBwbGUud2VhdGhl
...

Damn. This is a plist inside a plist, with more base64 data. Not fun. Let’s decode this as well.

<?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>$archiver</key>
	<string>NSKeyedArchiver</string>
	<key>$objects</key>
	<array>
		<string>$null</string>

		...

		<string>com.apple.weather.widget</string>
		<string>com.apple.weather</string>
		<dict>
			<key>$classes</key>
			<array>
				<string>CHSExtensionIdentity</string>
				<string>NSObject</string>
			</array>
			<key>$classname</key>
			<string>CHSExtensionIdentity</string>
		</dict>
		<string>com.apple.weather</string>

		...

			<key>$class</key>
			<dict>
				<key>CF$UID</key>
				<integer>9</integer>
			</dict>
			<key>NS.data</key>
			<data>
			5E1faW5kZXhpbmdIYXNoM9K5lBGWVOURVXBhcmFtZXRlckNvbWJp
			...
			</data>
		</dict>
		<dict>
			<key>$classes</key>
			<array>
				<string>NSMutableData</string>
				<string>NSData</string>
				<string>NSObject</string>
			</array>
			<key>$classname</key>
			<string>NSMutableData</string>
		</dict>
		<dict>
			<key>$classes</key>
			<array>
				<string>CHSIntentReference</string>
				<string>NSObject</string>
			</array>
			<key>$classname</key>
			<string>CHSIntentReference</string>
		</dict>
		<dict>
			<key>$classes</key>
			<array>
				<string>CHSWidget</string>
				<string>NSObject</string>
			</array>
			<key>$classname</key>
			<string>CHSWidget</string>
		</dict>
	</array>
	<key>$top</key>
	<dict>
		<key>root</key>
		<dict>
			<key>CF$UID</key>
			<integer>1</integer>
		</dict>
	</dict>
	<key>$version</key>
	<integer>100000</integer>
</dict>
</plist>

We find that this is an NSKeyedArchiver data. It stores an actual object with all of its properties. We also find that there is more base64 encoded stuff within the NS.Data section. The main object which is stored here is CHSWidget and the data inside NS.Data is an INIntent object. The former is defined in ChronoServices.framework and the later is in Intents.framework. We can load these frameworks and their classes dynamically, and we can then use NSLog to display the class contents.

I again wrote a small tool, this time in Objective-C to parse all the widget.instances.

csaby@max widgets % ./parseinstances ~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist 
2024-06-12 15:58:24.643 parseinstances[94282:5374885] Successfully loaded required frameworks.
2024-06-12 15:58:24.677 parseinstances[94282:5374885] Successfully loaded plist file.
2024-06-12 15:58:24.677 parseinstances[94282:5374885] Successfully retrieved widget instances.
2024-06-12 15:58:24.677 parseinstances[94282:5374885] Processing UUID: 403B6E51-3F19-4268-981E-10BF4F6F76DE
2024-06-12 15:58:24.678 parseinstances[94282:5374885] Decoded widget: <CHSWidget: 0x60000001d260; extensionIdentity: com.apple.weather::com.apple.weather.widget; kind: com.apple.weather; family: systemSmall; hasIntent?: YES; activityIdentifier: 0x0>
2024-06-12 15:58:24.683 parseinstances[94282:5374885] Intent: <INIntent: 0x600003c74090> {
    location = <INCustomObject: 0x600002d640a0> {
        hash = <null>;
        pronunciationHint = <null>;
        subtitleString = <null>;
        superclass = <null>;
        debugDescription = <null>;
        identifier = localCity;
        description = <null>;
        alternativeSpeakableMatches = <null>;
        displayString = My Location;
    };
}
2024-06-12 15:58:24.683 parseinstances[94282:5374885] Processing UUID: 0338F223-5CB8-46E1-9F54-E0871CB8B807
2024-06-12 15:58:24.684 parseinstances[94282:5374885] Decoded widget: <CHSWidget: 0x60000001a490; extensionIdentity: com.apple.iCal::com.apple.iCal.CalendarWidgetExtension; kind: com.apple.CalendarWidget.CalendarUpNextWidget; family: systemSmall; hasIntent?: YES; activityIdentifier: 0x0>
2024-06-12 15:58:24.684 parseinstances[94282:5374885] Intent: <INIntent: 0x600003c702d0> {
    mirrorCalendarApp = 1;
    hideAllDayEvents = <null>;
    calendars = <null>;
}
2024-06-12 15:58:24.684 parseinstances[94282:5374885] Processing UUID: 225C4FF3-C7C4-4DCE-901F-833D06954835
2024-06-12 15:58:24.684 parseinstances[94282:5374885] Decoded widget: <CHSWidget: 0x60000001b960; extensionIdentity: com.apple.clock::com.apple.clock.WorldClockWidget; kind: com.apple.mobiletimer.WorldClock; family: systemMedium; hasIntent?: YES; activityIdentifier: 0x0>
2024-06-12 15:58:24.685 parseinstances[94282:5374885] Intent: <INIntent: 0x600003c70360> {
    cities = (
        <INCustomObject: 0x600002d64b40> {
            hash = <null>;
            pronunciationHint = <null>;
            subtitleString = <null>;
            superclass = <null>;
            debugDescription = <null>;
            identifier = 75;
            description = <null>;
            alternativeSpeakableMatches = <null>;
            displayString = Cupertino, U.S.A.;
        },

These are all the active widgets that are actively loaded by the system. I think this is very handy for a forensics investigation.

I extended the above tool to also parse all the widgets.widgets instances, which contain CHSWidgetDescriptor data. Again, this is a list of all widgets available on the system, including those available on your iPhone!

So far so good, we can examine the available widgets, but we still can’t persist easily.

Persisting Link to heading

Ultimately what we should do here is create the classes, and store them using NSKeyedArchiver. For this we would need to define all these private classes, which contain reference to other private classes, etc… this is painful enough for me to take a shortcut. If we have a widget binary, which we deploy on our own system, we can simply take the encoded base64 string, and copy it to another system.

So I wrote another shell script, which will take a copy of this plist before adding a widget, then after adding a widget, diff it, and create a shell script which we can take to a machine and run it to persist our widget, which will look like the following.

plutil -insert widgets.instances -data <base64data> -append com.apple.notificationcenterui.plist

Once we added our entry we need to restart NotificationCenter so our widget is loaded, and it will be also loaded after restart.

The widget will be displayed in the notification bar in this case, so it’s not visible at first place. Also there is no alert from backgrundtaskmanagement that a new item was added.

All the tools I created are available on my Gist, here: Tools for working with widgets