-
-
Notifications
You must be signed in to change notification settings - Fork 68
/
STPrivilegedTask.m
executable file
·314 lines (257 loc) · 11.3 KB
/
STPrivilegedTask.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/*
STPrivilegedTask - NSTask-like wrapper around AuthorizationExecuteWithPrivileges
Copyright (C) 2008-2023 Sveinbjorn Thordarson <sveinbjorn@sveinbjorn.org>
BSD License
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor that of any other
contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "STPrivilegedTask.h"
#import <Security/Authorization.h>
#import <Security/AuthorizationTags.h>
#import <stdio.h>
#import <unistd.h>
#import <dlfcn.h>
NSString * const STPrivilegedTaskDidTerminateNotification = @"STPrivilegedTaskDidTerminateNotification";
// New error code denoting that AuthorizationExecuteWithPrivileges no longer exists
OSStatus const errAuthorizationFnNoLongerExists = -70001;
// Create fn pointer to AuthorizationExecuteWithPrivileges
// in case it doesn't exist in this version of macOS
static OSStatus (*_AuthExecuteWithPrivsFn)(AuthorizationRef authorization,
const char *pathToTool,
AuthorizationFlags options,
char * const *arguments,
FILE **communicationsPipe) = NULL;
@implementation STPrivilegedTask
{
NSTimer *_checkStatusTimer;
}
+ (void)initialize {
// On 10.7, AuthorizationExecuteWithPrivileges is deprecated. We want
// to still use it since there's no good alternative (without requiring
// code signing). We'll look up the function through dyld and fail if
// it is no longer accessible. If Apple removes the function entirely
// this will fail gracefully. If they keep the function and throw some
// sort of exception, this won't fail gracefully, but that's a risk
// we'll have to take for now.
// Pattern by Andy Kim from Potion Factory LLC
#pragma GCC diagnostic ignored "-Wpedantic" // stop the pedantry!
#pragma clang diagnostic push
_AuthExecuteWithPrivsFn = dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges");
#pragma clang diagnostic pop
}
- (instancetype)init {
self = [super init];
if (self) {
_currentDirectoryPath = [[NSFileManager defaultManager] currentDirectoryPath];
}
return self;
}
- (instancetype)initWithLaunchPath:(NSString *)path {
self = [self init];
if (self) {
_launchPath = path;
}
return self;
}
- (instancetype)initWithLaunchPath:(NSString *)path
arguments:(NSArray *)args {
self = [self initWithLaunchPath:path];
if (self) {
_arguments = args;
}
return self;
}
- (instancetype)initWithLaunchPath:(NSString *)path
arguments:(NSArray *)args
currentDirectory:(NSString *)cwd {
self = [self initWithLaunchPath:path arguments:args];
if (self) {
_currentDirectoryPath = cwd;
}
return self;
}
#pragma mark -
+ (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path {
STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path];
[task launch];
[task waitUntilExit];
return task;
}
+ (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path arguments:(NSArray *)args {
STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path arguments:args];
[task launch];
[task waitUntilExit];
return task;
}
+ (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path
arguments:(NSArray *)args
currentDirectory:(NSString *)cwd {
STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path arguments:args currentDirectory:cwd];
[task launch];
[task waitUntilExit];
return task;
}
+ (STPrivilegedTask *)launchedPrivilegedTaskWithLaunchPath:(NSString *)path
arguments:(NSArray *)args
currentDirectory:(NSString *)cwd
authorization:(AuthorizationRef)authorization {
STPrivilegedTask *task = [[STPrivilegedTask alloc] initWithLaunchPath:path arguments:args currentDirectory:cwd];
[task launchWithAuthorization:authorization];
[task waitUntilExit];
return task;
}
# pragma mark -
// return 0 for success
- (OSStatus)launch {
if (_isRunning) {
NSLog(@"Task already running: %@", [self description]);
return 0;
}
if ([STPrivilegedTask authorizationFunctionAvailable] == NO) {
NSLog(@"AuthorizationExecuteWithPrivileges() function not available on this system");
return errAuthorizationFnNoLongerExists;
}
OSStatus err = noErr;
const char *toolPath = [self.launchPath fileSystemRepresentation];
AuthorizationRef authorizationRef;
AuthorizationItem myItems = { kAuthorizationRightExecute, strlen(toolPath), &toolPath, 0 };
AuthorizationRights myRights = { 1, &myItems };
AuthorizationFlags flags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights;
// Use Apple's Authentication Manager API to create an Authorization Reference
err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authorizationRef);
if (err != errAuthorizationSuccess) {
return err;
}
// Pre-authorize the privileged operation
err = AuthorizationCopyRights(authorizationRef, &myRights, kAuthorizationEmptyEnvironment, flags, NULL);
if (err != errAuthorizationSuccess) {
return err;
}
// OK, at this point we have received authorization for the task so we launch it.
err = [self launchWithAuthorization:authorizationRef];
// Free the auth ref
AuthorizationFree(authorizationRef, kAuthorizationFlagDefaults);
return err;
}
- (OSStatus)launchWithAuthorization:(AuthorizationRef)authorization {
if (_isRunning) {
NSLog(@"Task already running: %@", [self description]);
return 0;
}
if ([STPrivilegedTask authorizationFunctionAvailable] == NO) {
NSLog(@"AuthorizationExecuteWithPrivileges() function not available on this system");
return errAuthorizationFnNoLongerExists;
}
// Assuming the authorization is valid for the task.
// Let's prepare to launch it.
NSArray *arguments = self.arguments;
NSUInteger numArgs = [arguments count];
char *args[numArgs + 1];
FILE *outputFile;
const char *toolPath = [self.launchPath fileSystemRepresentation];
// First, construct an array of C strings w. all the arguments from NSArray
// This is the format required by AuthorizationExecuteWithPrivileges function
for (int i = 0; i < numArgs; i++) {
NSString *argString = arguments[i];
const char *fsrep = [argString fileSystemRepresentation];
NSUInteger stringLength = strlen(fsrep);
args[i] = calloc((stringLength + 1), sizeof(char));
snprintf(args[i], stringLength + 1, "%s", fsrep);
}
args[numArgs] = NULL;
// Change to the specified current working directory
// NB: This is process-wide and could interfere with the behaviour of concurrent tasks
char *prevCwd = (char *)getcwd(nil, 0);
chdir([self.currentDirectoryPath fileSystemRepresentation]);
// Use Authorization Reference to execute script with privileges.
// This is where the magic happens.
OSStatus err = _AuthExecuteWithPrivsFn(authorization, toolPath, kAuthorizationFlagDefaults, args, &outputFile);
// OK, now we're done executing, let's change back to old dir
chdir(prevCwd);
// Free the alloc'd argument strings
for (int i = 0; i < numArgs; i++) {
free(args[i]);
}
// We return err if execution failed
if (err != errAuthorizationSuccess) {
return err;
} else {
_isRunning = YES;
}
// Get file handle for the command output
_outputFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileno(outputFile) closeOnDealloc:YES];
_processIdentifier = fcntl(fileno(outputFile), F_GETOWN, 0);
// Start monitoring task
_checkStatusTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(checkTaskStatus) userInfo:nil repeats:YES];
return err;
}
- (void)terminate {
// This doesn't work without a PID, and we can't get one. Stupid Security API.
// int ret = kill(pid, SIGKILL);
//
// if (ret != 0) {
// NSLog(@"Error %d", errno);
// }
}
// Hang until task is done
- (void)waitUntilExit {
if (!_isRunning) {
NSLog(@"Task %@ is not running", [super description]);
return;
}
[_checkStatusTimer invalidate];
int status;
pid_t pid = 0;
while ((pid = waitpid(_processIdentifier, &status, WNOHANG)) == 0) {
// Do nothing
}
_isRunning = NO;
_terminationStatus = WEXITSTATUS(status);
}
// Check if task has terminated
- (void)checkTaskStatus {
int status;
pid_t pid = waitpid(_processIdentifier, &status, WNOHANG);
if (pid != 0) {
_isRunning = NO;
_terminationStatus = WEXITSTATUS(status);
[_checkStatusTimer invalidate];
[[NSNotificationCenter defaultCenter] postNotificationName:STPrivilegedTaskDidTerminateNotification object:self];
if (_terminationHandler) {
_terminationHandler(self);
}
}
}
#pragma mark -
+ (BOOL)authorizationFunctionAvailable {
return _AuthExecuteWithPrivsFn ? YES : NO;
}
#pragma mark -
// Nice description for debugging purposes
- (NSString *)description {
NSString *commandDescription = [NSString stringWithString:self.launchPath];
for (NSString *arg in self.arguments) {
commandDescription = [commandDescription stringByAppendingFormat:@" '%@'", arg];
}
[commandDescription stringByAppendingFormat:@" (CWD:%@)", self.currentDirectoryPath];
return [[super description] stringByAppendingFormat:@" %@", commandDescription];
}
@end