Skip to content

Instantly share code, notes, and snippets.

@EricRabil
Last active August 7, 2024 04:43
Show Gist options
  • Select an option

  • Save EricRabil/01fe68d61137de41cbdefa3738a77979 to your computer and use it in GitHub Desktop.

Select an option

Save EricRabil/01fe68d61137de41cbdefa3738a77979 to your computer and use it in GitHub Desktop.
mach ipc via exception ports
//
// 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
}
}
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