There are many ways for code to persist on macOS. One way for applications to persist and thus be automatically launched when the user logs in, is to register as a login item
.
In the context of this blog post, "persistence" is defined as a way for a binary or application to 'register' with the OS in order to be automatically executed when the system is rebooted (and/or when the user logs in).
There are two 'types' of login items:
Traditional login items are the focus of this blog post (specifically how to programmatically detect when new login item is added). These are the items one can see in UI of the System Preferences
application (under Users & Groups
, Login Items
):
On the other hand, 'modern' login items, are designed to work with applications distributed via the Mac App Store. Found within the /Library/loginItems
directory of application bundles, though they may be persistent and thus automatically launched by the OS, a list of such items do not show up in UI.
Luckily KnockKnock will enumerate such login items for you:
Interested in learning more about 'modern' login items?
Read Cory Bohon's excellent write up on the topic.
So why would we want to know when a login item is installed?
First, it's always good to know when software is installing a persistent component. Perhaps you don't want some random application component always starting when you login in, due to performance reasons (as login items may slow down the login / initialization process).
Also, malware has been known to abuse login items in order to persist! For example, OSX.KitM
:
For more info about OSX.KitM, read FSecure's writeup: Mac Spyware Found at Oslo Freedom Forum
One of Objective-See's most popular tools is BlockBlock. BlockBlock provides continual protection by monitoring persistence locations - such as login items. As most Mac malware attempts to persist, BlockBlock provides a high level of generic protection even against 'never been seen before' threats.
However, the BlockBlock plugin that monitored for the new login items recently needed some TCL.
First, the plugin was manually parsing Apple's login item plist (instead of using CoreFoundation
APIs). And second, due to changes in the macOS, the plugin was not able to detect when a new login item was installed on High Sierra.
Note:
While one can programmatically invokeLSSharedFileListCopySnapshot
to return a list of installed login items, this function is user-context sensitive (i.e. will return different values based on the logged in user). Also, if one wants to enumerate installed login items offline (i.e. off the box, or when the system is not running), this API is not applicable. These obstacles are both overcome if instead, one operates instead on the files where the login items are stored.
In this blog we'll detail both improvements, illustrating how to efficiently and comprehensively detect whenever a ('traditional') login item is installed on all recent versions of macOS.
On older versions of macOS, login items are stored in the ~/Library/Preferences/com.apple.loginitems.plist
. Thus when user manually adds a login item, or one is installed programmatically this file is updated. As such, BlockBlock monitors this files for modifications to detect the addition of new login items.
Login items are stored in this file, in a binary plist (bplist00
):
$ file com.apple.loginitems.plist
com.apple.loginitems.plist: Apple binary property list
$ hexdump -C com.apple.loginitems.plist
00000000 62 70 6c 69 73 74 30 30 d1 01 02 5c 53 65 73 73 |bplist00........|
Using macOS's builtin plutil (/usr/bin/plutil
) we can convert this binary plist to the xml-based one:
$ plutil -convert xml1 com.apple.loginitems.plist
SessionItemsControllerCustomListItemsCustomListItemsAliasAAAAAADYAAMAAQAA1Y90iAAASCsAAAAAAAEJOwABCT4AANU3Q24AAAAACSD//gAAAAAAAAAA/////wABABAAAQk7AAEJJQABCSQAAABHAA4AIgAQAGkAVAB1AG4AZQBzAEgAZQBsAHAAZQByAC4AYQBwAHAADwAaAAwATQBhAGMAaQBuAHQAbwBzAGgAIABIAEQAEgA3QXBwbGljYXRpb25zL2lUdW5lcy5hcHAvQ29udGVudHMvTWFjT1MvaVR1bmVzSGVscGVyLmFwcAAAEwABLwD//wAACustomItemPropertiescom.apple.LSSharedFileList.BindingZG5pYgAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAZmlsZTovLy9BcHBsaWNhdGlvbnMvaVR1bmVzLmFwcC9Db250ZW50cy9NYWNPUy9pVHVuZXNIZWxwZXIuYXBwLxYAAAAAAAAAY29tLmFwcGxlLmlUdW5lc0hlbHBlcgIA4AAAMAAAjgAQAAIAAABuysEecom.apple.LSSharedFileList.ItemIsHiddenFlags1NameiTunesHelper
Each login item is stored in the the CustomListItems
array within the SessionItems
dictionary. For BlockBlock, we are interested in the path of any new login item. This information is stored with the Alias
key:
AliasAAAAAADYAAMAAQAA1Y90iAAASCsAAAAAAAEJOwABCT4AANU3Q24AAAAACSD.....
The name is also of importance, but can be easily extracted from the
Name
key:
<key>Name</key>
<string>iTunesHelper</string>
The format of this 'Alias' data, as its name implies, is an Alias Record
. Though the format of these alias records are proprietary and have not documented by Apple, they have been reverse engineered. Luckily, we don't have to resort to manually parsing these records, as Apple provides various CoreFoundation
APIs to help with this!
To parse the login item plist in order to extract paths of new login items, first we create a bookmark
from this alias record via the CFURLCreateBookmarkDataFromAliasRecord
API:
/* Returns bookmark data derived from an alias record */CF_EXPORTCFDataRef ;//bookmarkCFDataRef bookmark = NULL;//extract aliasalias = loginItem[@"Alias"];//create bookmarkbookmark = ;
Once we have a bookmark, it can be 'resolved' into a URL, via the CFURLCreateByResolvingBookmarkData
API:
/* Return a URL that refers to a location specified by resolving bookmark data. If this function returns NULL, the optional error is populated. */CF_EXPORTCFURLRef ;//bookmark urlCFURLRef url = NULL;//resolve bookmark data into URLurl = ;
Armed with a url, we can now extract the full path to the login item's location on disk:
//pathNSString* path = nil;//extract pathpath = ;
Note:
In production code, check forNULL
and make sure to release the appropriatecf*
objects!
Now we (and BlockBlock) can programmatically parse the com.apple.loginitems.plist
via Apple's CoreFoundation
APIs, extracting the paths of any login items:
$ ./parseLoginItems
found login item:
name: iTunesHelper
path: /Applications/iTunes.app/Contents/MacOS/iTunesHelper.app
...
All is well and good...unless you're running High Sierra!
On macOS 10.13, Apple decided to change both where and how login items were stored. Instead of the com.apple.loginitems.plist
file, login items are now stored in the backgrounditems.btm
file, found within ~/Library/Application Support/com.apple.backgroundtaskmanagementagent/
To confirm this, manually add a login item via the System Preferences app, while monitoring file i/o:
# fs_usage -w -f filesystem
lstat64 /Users/patrick/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm backgroundtaskma.418404
chmod /Users/patrick/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm backgroundtaskma.418404
It's trivial for update BlockBlock to monitor for modifications to the backgrounditems.btm
file in order to detect the addition of a new login item.
However, while the login items are still stored in a binary plist:
$ file ~/Library/Application\ Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm
~/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm: Apple binary property list
...the format of this plist (backgrounditems.btm
) has changed:
$archiver.....$classCF$UID9NS.uuidbytesahDw+gpfTRutfTxjDoYZuA==Ym9va/QCAAAAAAQQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAIAAAwAAAABAQAAQXBwbGljYXRpb25zDwAAAAEBAAAxUGFzc3dvcmQgNi5hcHAACAAAAAEGAAAEAAAAGAAAAAgAAAAEAwAANOEMAAEAAAAIAAAABAMAAHTcswABAAAACAAAAAEGAABAAAAAUAAAAAgAAAAABAAAQcBXiwCAAAAYAAAAAQIAAAIAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAgAAAABCQAAZmlsZTovLy8MAAAAAQEAAE1hY2ludG9zaCBIRAgAAAAEAwAAAOAB4+gAAAAIAAAAAAQAAEG/GfuIAAAAJAAAAAEBAAA3QUNBNzU5MS02MDY2LTMzQjctOEZFNi03NUJDNEUzRjAzRDQYAAAAAQIAAIEAAAABAAAA7xMAAAEAAAAAAAAAAAAAAAEAAAABAQAALwAAAAAAAAABBQAACwAAAAEBAAAxUGFzc3dvcmQgNgCoAAAAAQIAAGVmYzZhMzM4MTgzZTY0MTQ2NDQxNTQxYjdmNTg0ZWJmOWY1M2VmMTg7MDAwMDAwMDA7MDAwMDAwMDA7MDAwMDAwMDAwMDAwMDAyMDtjb20uYXBwbGUuYXBwLXNhbmRib3gucmVhZC13cml0ZTswMTswMTAwMDAwNDswMDAwMDAwMTAwYjNkYzc0Oy9hcHBsaWNhdGlvbnMvMXBhc3N3b3JkIDYuYXBwALQAAAD+////AQAAAAAAAAAOAAAABBAAADAAAAAAAAAABRAAAGAAAAAAAAAAEBAAAIAAAAAAAAAAQBAAAHAAAAAAAAAAAiAAADABAAAAAAAABSAAAKAAAAAAAAAAECAAALAAAAAAAAAAESAAAOQAAAAAAAAAEiAAAMQAAAAAAAAAEyAAANQAAAAAAAAAICAAABABAAAAAAAAMCAAADwBAAAAAAAAF/AAAEQBAAAAAAAAgPAAAFgBAAAAAAAA$classesBookmarkNSObject$classnameBookmark$classesBackgroundLoginItemBackgroundItemNSObject$classnameBackgroundLoginItem
From the $archiver
key, we can infer that it a serialized object. Other keys such as Bookmark
indicate it's likely "bookmark" data.
The path of the login item is likely in the data blobs of the serialized object (or in the NS.data
key/value pair). But how to extract and parse this encoded/serialized data?
Lucky for us, the talented MikeyMikey, posted a detailed exposé on Apple's "Bookmark" data: "Apple's Bookmark Data - exposed!".
Though not specifically discussing login items (i.e. the backgrounditems.btm
file) in this writeup, he discusses various APIs to deal with this "bookmark" data. Specifically he notes some Apple documentation which states:
"you can use the [
NSURL
's]resourceValuesForKeys:fromBookmarkData:
method to obtain information about the bookmark, such as the last known path (NSURLPathKey
)."
Sounds exactly what we want - and turns out, it is :) In short, to extract the paths of all login items (or new ones that are added), simply iterate over the data blobs in the backgrounditems.btm
file and for each, invoke the NSURL
's resourceValuesForKeys:fromBookmarkData:
method. If this method succeeds, it will returns a dictionary of properties of the bookmark, including an embedded dictionary (key: NSURLBookmarkAllPropertiesKey
) that contains the path of the login item (key: _NSURLPathKey
):
//extract all login items pathsfor(id object in data[@"$objects"])
In this post, we discussed how to extract the paths of installed (or newly persisted) login items.
On older versions of macOS, one can use Apple's CoreFoundation
API to parse the com.apple.loginitems.plist.
With the advent of High Sierra, Apple has changed both the location of the file, and it's format. Thus new APIs, specifically, NSURL
's resourceValuesForKeys:fromBookmarkData:
should be used to parse the backgrounditems.btm
file.
With these updates, BlockBlock is now able to detect login item persistent even on the latest version of Apple's OS: