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 invoke LSSharedFileListCopySnapshot 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.

Parsing Login Items

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

<?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>SessionItems</key>
        <dict>
                <key>Controller</key>
                <string>CustomListItems</string>
                <key>CustomListItems</key>
                <array>
                        <dict>
                                <key>Alias</key>
                                <data>
                                AAAAAADYAAMAAQAA1Y90iAAASCsAAAAAAAEJOwABCT4A
                                ANU3Q24AAAAACSD//gAAAAAAAAAA/////wABABAAAQk7
                                AAEJJQABCSQAAABHAA4AIgAQAGkAVAB1AG4AZQBzAEgA
                                ZQBsAHAAZQByAC4AYQBwAHAADwAaAAwATQBhAGMAaQBu
                                AHQAbwBzAGgAIABIAEQAEgA3QXBwbGljYXRpb25zL2lU
                                dW5lcy5hcHAvQ29udGVudHMvTWFjT1MvaVR1bmVzSGVs
                                cGVyLmFwcAAAEwABLwD//wAA
                                </data>
                                <key>CustomItemProperties</key>
                                <dict>
                                        <key>com.apple.LSSharedFileList.Binding</key>
                                        <data>
                                        ZG5pYgAAAAACAAAAAAAAAAAAAAAAAAAAAAAA
                                        AEAAAAAAAAAAZmlsZTovLy9BcHBsaWNhdGlv
                                        bnMvaVR1bmVzLmFwcC9Db250ZW50cy9NYWNP
                                        Uy9pVHVuZXNIZWxwZXIuYXBwLxYAAAAAAAAA
                                        Y29tLmFwcGxlLmlUdW5lc0hlbHBlcgIA4AAA
                                        MAAAjgAQAAIAAABuysEe
                                        </data>
                                        <key>com.apple.LSSharedFileList.ItemIsHidden</key>
                                        <true/>
                                </dict>
                                <key>Flags</key>
                                <integer>1</integer>
                                <key>Name</key>
                                <string>iTunesHelper</string>
                        </dict>
                </array>
        </dict>
</dict>
</plist>

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:

<key>Alias</key>
<data>
AAAAAADYAAMAAQAA1Y90iAAASCsAAAAAAAEJOwABCT4A  
ANU3Q24AAAAACSD.....

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_EXPORT
CFDataRef CFURLCreateBookmarkDataFromAliasRecord ( CFAllocatorRef allocatorRef, CFDataRef aliasRecordDataRef );
 
 
//bookmark
CFDataRef bookmark = NULL;
 
//extract alias
alias = loginItem[@"Alias"];
 
//create bookmark
bookmark = CFURLCreateBookmarkDataFromAliasRecord(kCFAllocatorDefault,(\__bridge CFDataRef)(alias));

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_EXPORT
CFURLRef CFURLCreateByResolvingBookmarkData ( CFAllocatorRef allocator, CFDataRef bookmark, CFURLBookmarkResolutionOptions options, CFURLRef relativeToURL, CFArrayRef resourcePropertiesToInclude, Boolean* isStale, CFErrorRef* error ) API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
 
//bookmark url
CFURLRef url = NULL;
 
//resolve bookmark data into URL
url = CFURLCreateByResolvingBookmarkData(kCFAllocatorDefault, bookmark, kCFBookmarkResolutionWithoutUIMask, nil, nil, nil, nil);

Armed with a url, we can now extract the full path to the login item's location on disk:

//path
NSString* path = nil;
 
//extract path
path = CFBridgingRelease(CFURLCopyPath(url));

Note:
In production code, check for NULL and make sure to release the appropriate cf* 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:

<?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>
    .....
    <dict>
            <key>$class</key>
            <dict>
                <key>CF$UID</key>
                <integer>9</integer>
            </dict>
            <key>NS.uuidbytes</key>
            <data>
            ahDw+gpfTRutfTxjDoYZuA==
            </data>
        </dict>
        <data>
        Ym9va/QCAAAAAAQQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        AAAACAIAAAwAAAABAQAAQXBwbGljYXRpb25zDwAAAAEBAAAxUGFzc3dvcmQg
        Ni5hcHAACAAAAAEGAAAEAAAAGAAAAAgAAAAEAwAANOEMAAEAAAAIAAAABAMA
        AHTcswABAAAACAAAAAEGAABAAAAAUAAAAAgAAAAABAAAQcBXiwCAAAAYAAAA
        AQIAAAIAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAgAAAABCQAAZmlsZTovLy8M
        AAAAAQEAAE1hY2ludG9zaCBIRAgAAAAEAwAAAOAB4+gAAAAIAAAAAAQAAEG/
        GfuIAAAAJAAAAAEBAAA3QUNBNzU5MS02MDY2LTMzQjctOEZFNi03NUJDNEUz
        RjAzRDQYAAAAAQIAAIEAAAABAAAA7xMAAAEAAAAAAAAAAAAAAAEAAAABAQAA
        LwAAAAAAAAABBQAACwAAAAEBAAAxUGFzc3dvcmQgNgCoAAAAAQIAAGVmYzZh
        MzM4MTgzZTY0MTQ2NDQxNTQxYjdmNTg0ZWJmOWY1M2VmMTg7MDAwMDAwMDA7
        MDAwMDAwMDA7MDAwMDAwMDAwMDAwMDAyMDtjb20uYXBwbGUuYXBwLXNhbmRi
        b3gucmVhZC13cml0ZTswMTswMTAwMDAwNDswMDAwMDAwMTAwYjNkYzc0Oy9h
        cHBsaWNhdGlvbnMvMXBhc3N3b3JkIDYuYXBwALQAAAD+////AQAAAAAAAAAO
        AAAABBAAADAAAAAAAAAABRAAAGAAAAAAAAAAEBAAAIAAAAAAAAAAQBAAAHAA
        AAAAAAAAAiAAADABAAAAAAAABSAAAKAAAAAAAAAAECAAALAAAAAAAAAAESAA
        AOQAAAAAAAAAEiAAAMQAAAAAAAAAEyAAANQAAAAAAAAAICAAABABAAAAAAAA
        MCAAADwBAAAAAAAAF/AAAEQBAAAAAAAAgPAAAFgBAAAAAAAA
        </data>
        <dict>
            <key>$classes</key>
            <array>
                <string>Bookmark</string>
                <string>NSObject</string>
            </array>
            <key>$classname</key>
            <string>Bookmark</string>
        </dict>
        <dict>
            <key>$classes</key>
            <array>
                <string>BackgroundLoginItem</string>
                <string>BackgroundItem</string>
                <string>NSObject</string>
            </array>
            <key>$classname</key>
            <string>BackgroundLoginItem</string>
        </dict>

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 paths
    for(id object in data[@"$objects"])
    {
        //straight data?
        if(YES == [object isKindOfClass:[NSData class]])
            bookmark = object;
 
        //dictionary w/ data?
        else if(YES == [object isKindOfClass:[NSDictionary class]])
          bookmark = [object objectForKey:@"NS.data"];
 
        //extract properties
        // "resourceValuesForKeys" returns a dictionary,
        // want the "NSURLBookmarkAllPropertiesKey" dictionary inside that
        properties = [NSURL resourceValuesForKeys:@[@"NSURLBookmarkAllPropertiesKey"] fromBookmarkData:bookmark][@"NSURLBookmarkAllPropertiesKey"];
 
        //extract path
        path = properties[@"_NSURLPathKey"];
 
        ... 
Conclusion

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: