Last active
August 7, 2024 04:43
-
-
Save EricRabil/01fe68d61137de41cbdefa3738a77979 to your computer and use it in GitHub Desktop.
mach ipc via exception ports
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // MachClient.swift | |
| // | |
| // Created by Eric Rabil on 7/7/21. | |
| // | |
| import Foundation | |
| public let kMachIPCGreet: UInt32 = 4 | |
| public struct MachIPCGreet: Codable { | |
| public let uid: uid_t | |
| public let pid: pid_t | |
| } | |
| @objc | |
| public protocol MachClientDelegate { | |
| @objc optional func machClient(_ client: MachClient, didGreetWithUID uid: uid_t, pid: pid_t) -> Void | |
| } | |
| public extension mach_port_t { | |
| static var exceptionPort: mach_port_t? { | |
| ResolveIPCPort() | |
| } | |
| } | |
| public let IPC_EXCEPTION_MASK = EXC_MASK_RPC_ALERT | |
| /// Resolves the mach IPC port that was passed to us on the IPC_EXCEPTION_MASK exception port | |
| func ResolveIPCPort() -> mach_port_t? { | |
| var masksCnt: mach_msg_type_number_t = 0 | |
| let ports = exception_handler_array_t.allocate(capacity: Int(EXC_TYPES_COUNT)) | |
| let exception_mask_array: exception_mask_array_t = .allocate(capacity: Int(EXC_TYPES_COUNT)) | |
| let behaviors = exception_behavior_array_t.allocate(capacity: Int(EXC_TYPES_COUNT)) | |
| let flavors = exception_flavor_array_t.allocate(capacity: Int(EXC_TYPES_COUNT)) | |
| task_get_exception_ports(mach_task_self_, exception_mask_t(IPC_EXCEPTION_MASK), exception_mask_array, &masksCnt, ports, behaviors, flavors) | |
| guard masksCnt == 1 else { | |
| // if theres more than one port, we can't know which port is the one we want | |
| log.fault("multiple ports on mask, cannot safely continue.") | |
| return nil | |
| } | |
| return ports[0] | |
| } | |
| extension kern_return_t: Error {} | |
| /// Allocate a mach_port that other processes will send to | |
| public func MachPortCreate() -> Result<mach_port_t, kern_return_t> { | |
| var port: mach_port_t = 0 | |
| var err: kern_return_t = 0 | |
| // add receive rights to the process | |
| err = mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_RECEIVE, &port) | |
| guard err == KERN_SUCCESS else { | |
| log("cant allocate mach port: %d", err) | |
| return .failure(err) | |
| } | |
| // allow other processes to send us messages | |
| err = mach_port_insert_right(mach_task_self_, port, port, UInt32(MACH_MSG_TYPE_MAKE_SEND)) | |
| guard err == KERN_SUCCESS else { | |
| log("cant insert send right: %d", err) | |
| mach_port_destroy(mach_task_self_, port) | |
| return .failure(err) | |
| } | |
| return .success(port) | |
| } | |
| /// Wrapper for mach IPC, facilitating communication between idshelper and loginmanagerd | |
| public class MachClient: NSObject, PortDelegate { | |
| public private(set) var send_mach_port: mach_port_t = 0 | |
| public private(set) var rcv_mach_port: mach_port_t = 0 | |
| public let sendPort: Port | |
| public let receivePort: Port | |
| private let encoder: PropertyListEncoder = { | |
| let encoder = PropertyListEncoder() | |
| encoder.outputFormat = .binary | |
| return encoder | |
| }() | |
| public var delegate: MachClientDelegate? = nil | |
| /// Create an IPC client that communicates over the exception port | |
| public static func exceptionPortClient() -> MachClient { | |
| defer { | |
| print("exception port client initialized") | |
| } | |
| return MachClient(scheduler: .current, rcv_mach_port: try! MachPortCreate().get(), send_mach_port: .exceptionPort!) | |
| } | |
| /// Create an IPC client whose port will be passed to other processes | |
| public convenience init(scheduler: RunLoop = .current) throws { | |
| let port = try MachPortCreate().get() | |
| self.init(scheduler: scheduler, rcv_mach_port: port, send_mach_port: port) | |
| } | |
| /// Create an IPC client from arbitrary receive and send ports | |
| public init(scheduler: RunLoop = .current, rcv_mach_port: mach_port_t, send_mach_port: mach_port_t) { | |
| self.send_mach_port = send_mach_port | |
| self.rcv_mach_port = rcv_mach_port | |
| self.sendPort = NSMachPort(machPort: self.send_mach_port) | |
| self.receivePort = { | |
| let nsPort = NSMachPort(machPort: rcv_mach_port) | |
| nsPort.schedule(in: scheduler, forMode: .common) | |
| return nsPort | |
| }() | |
| super.init() | |
| self.receivePort.setDelegate(self) | |
| register(msgid: kMachIPCGreet, type: MachIPCGreet.self) { message, client, greeting in | |
| self.delegate?.machClient?(client, didGreetWithUID: greeting.uid, pid: greeting.pid) | |
| } | |
| } | |
| /// Create a pseudo-client that is passed to handlers so they can reply | |
| private init(sendPort: Port, receivePort: Port) { | |
| self.sendPort = sendPort | |
| self.receivePort = receivePort | |
| } | |
| /// Sends a message over the stored sendPort and receivePort, scheduled to go within 0.1 seconds | |
| private func sendMessage(withID msgid: UInt32, components: [Any]? = nil) { | |
| let message = PortMessage(send: sendPort, receive: receivePort, components: components) | |
| message.msgid = msgid | |
| message.send(before: .init(timeIntervalSinceNow: 0.1)) | |
| } | |
| /// Sends a codable value, encoded to a binary plist | |
| private func sendMessage<P: Encodable>(withID msgid: UInt32, data: P) { | |
| sendMessage(withID: msgid, components: [ | |
| try! encoder.encode(data) | |
| ]) | |
| } | |
| public func greet() { | |
| sendMessage(withID: kMachIPCGreet, data: MachIPCGreet(uid: getuid(), pid: getpid())) | |
| } | |
| private typealias PortMessageHandler = (PortMessage) -> () | |
| private var handlers = [UInt32: PortMessageHandler]() | |
| /// Registers an IPC handler for the given message ID | |
| private func register<P: Decodable>(msgid: UInt32, type: P.Type, _ callback: @escaping (PortMessage, MachClient, P) -> ()) { | |
| handlers[msgid] = { message in | |
| guard let parsed = message.decode(to: P.self), let sendPort = message.sendPort, let receivePort = message.receivePort else { | |
| log.fault("failed to decode for msgid %d", msgid) | |
| return | |
| } | |
| callback(message, MachClient(sendPort: sendPort, receivePort: receivePort), parsed) | |
| } | |
| } | |
| public func handle(_ message: PortMessage) { | |
| guard let handler = handlers[message.msgid] else { | |
| return | |
| } | |
| handler(message) | |
| } | |
| } | |
| extension PortMessage { | |
| /// Decodes the first element of the message components to a type | |
| func decode<P: Decodable>(to: P.Type) -> P? { | |
| guard let data = components?.first as? Data else { | |
| return nil | |
| } | |
| guard let parsed = try? PropertyListDecoder().decode(P.self, from: data) else { | |
| return nil | |
| } | |
| return parsed | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| let ShouldPerformBootstrapImpersonation = true | |
| /** | |
| Spawn a child process using posix_spawn, passing along a mach port and impersonating a user if desired. | |
| - Parameter executable: the absolute path to the executable to launch | |
| - Parameter args: array of string arguments to pass to the executable | |
| - Parameter uid: the uid of the user to impersonate, otherwise current user | |
| - Parameter exceptionPort: the port to pass to the process at IPC_EXCEPTION_MASK | |
| - Parameter env: the additional environment variables to expose to the executable | |
| */ | |
| public func PosixSpawn(executable: String, args: [String], uid: uid_t = getuid(), exceptionPort: mach_port_t = mach_port_t(MACH_PORT_NULL), env: [String: String] = [:]) -> Result<(pid_t, mach_port_t?), Error> { | |
| var pid: pid_t = -1 | |
| var err: Int32 = 0 | |
| var attr: posix_spawnattr_t? | |
| let old_euid = geteuid() | |
| var oldEnv = [String: UnsafeMutablePointer<CChar>]() | |
| defer { | |
| // Restore the old euid | |
| if old_euid != getuid() { | |
| seteuid(old_euid) | |
| } | |
| // Deallocate spawnattr | |
| posix_spawnattr_destroy(&attr) | |
| // Restore old environment variables | |
| oldEnv.forEach { key, value in | |
| setenv(key, value, 1) | |
| } | |
| } | |
| posix_spawnattr_init(&attr) | |
| // Have process spawn in an independent session (instead of a child) | |
| posix_spawnattr_setflags(&attr, Int16(POSIX_SPAWN_SETSID)) | |
| if exceptionPort != mach_port_t(MACH_PORT_NULL) { | |
| // pass the ipc port to the pre-defined exception port slot | |
| err = posix_spawnattr_setexceptionports_np(&attr, exception_mask_t(IPC_EXCEPTION_MASK), exceptionPort, EXCEPTION_STATE_IDENTITY, MACHINE_THREAD_STATE) | |
| guard err == 0 else { | |
| log.error("setexceptionports fail: %d", err) | |
| return .failure(NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)) | |
| } | |
| } | |
| var bootstrap_port_: mach_port_t? = nil | |
| // Impersonate target user | |
| if uid != geteuid() { | |
| if ShouldPerformBootstrapImpersonation { | |
| let processes = GetBSDProcessList().filter { | |
| $0.ownerUID == uid | |
| }.reduce(into: [String: pid_t]()) { dict, proc in | |
| dict[proc.processName] = proc.kp_proc.p_pid | |
| } | |
| let acceptableProccesses = ["akd", "accountsd", "imagent", "identityservicesd", "Finder", "Dock"] | |
| var bootstrapPort_: mach_port_t = 0 | |
| for processName in acceptableProccesses { | |
| guard let pid = processes[processName] else { | |
| continue | |
| } | |
| var task: mach_port_t = 0 | |
| var ret = task_for_pid(mach_task_self_, pid, &task) | |
| if ret != KERN_SUCCESS { | |
| continue | |
| } | |
| print("\(pid)==\(task)") | |
| ret = task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrapPort_) | |
| mach_port_deallocate(mach_task_self_, task) | |
| if ret == KERN_SUCCESS, bootstrapPort_ > 0 { | |
| print("captured bootstrap port from \(processName)=\(bootstrapPort_)") | |
| break | |
| } else { | |
| bootstrapPort_ = 0 | |
| } | |
| } | |
| guard bootstrapPort_ > 0 else { | |
| print("fuck") | |
| return .failure(ManagedError("Failed to get bootstrap port")) | |
| } | |
| posix_spawnattr_setspecialport_np(&attr, bootstrapPort_, TASK_BOOTSTRAP_PORT) | |
| print("assigned bootstrap port successfully") | |
| bootstrap_port_ = bootstrapPort_ | |
| } | |
| seteuid(uid) | |
| } | |
| for (key, value) in env { | |
| // backup old env variables if present | |
| if let oldValue = getenv(key) { | |
| oldEnv[key] = strdup(oldValue) | |
| } | |
| // store env variable so child inherits it | |
| err = setenv(key, value, 1) | |
| guard err == 0 else { | |
| log.error("setenv fail: %d", err) | |
| return .failure(NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)) | |
| } | |
| } | |
| err = args.withCStrings { args in | |
| // spawn process | |
| posix_spawn(&pid, executable, nil, &attr, args, environ) | |
| } | |
| guard err == 0 else { | |
| log.error("posix_spawn fail: %d", err) | |
| return .failure(NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)) | |
| } | |
| return .success((pid, bootstrap_port_)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment