Blog
Sep 23, 2022
In the Hunt for the macOS AutoLogin Setup Process
OffSec’s Csaba Fitzl shares how he reverse-engineered the macOS auto-login process, including the walls he hit, and the times he resorted to trial-and-error approaches.
14 min read
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:
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:
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.
- We opened an XPC connection to
logind
, and get an interface tologinwindow
- When we called the password set function in
loginwindow
, it called the same function inlogind
- 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.
Accounts.prefPane
will load its XPC service
com.apple.preferences.users.remoteservice
- The loaded XPC service will load the
SystemAdministration
framework and configure the loginwindow global preferences - Then the XPC service will open an XPC connection to
logind
, and get an interface tologinwindow
. - Then it will call the password set function in
loginwindow
loginwindow
will open an XPC connection tologind
- Finally
loginwindow
will call the password set function oflogind
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.
Cybersecurity leader resources
Sign up for the Secure Leader and get the latest info on industry trends, resources and best practices for security leaders every other week
Latest from OffSec
Enterprise Security
What is Threat Intelligence?
This article explores threat intelligence, its purpose, types, and how organizations can leverage it to enhance cybersecurity.
Sep 27, 2024
9 min read
Insights
Mental Toughness in Cybersecurity: Preparing Teams for High-Pressure Situations
Mental toughness helps cybersecurity teams improve decision-making, collaboration, and resilience, enabling them to perform under constant pressure.
Sep 20, 2024
7 min read
Enterprise Security
The Role of Leadership in Cultivating a Resilient Cybersecurity Team
Learn about the role that leadership plays in cultivating a resilient cybersecurity team.
Sep 13, 2024
5 min read