mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-01 23:54:35 +08:00
Tell the host the configured UDID, not only the profile check
The UDID override used to reach misagent and installd alone; Xcode, lockdown and usbmuxd kept seeing the guest's own, so a paid team's profile could not name the VM. The host reads the UDID in three places, and each is reachable from userspace: - lockdownd and remoted join vpIsMISFixTarget, so the spawn hooks insert libmisfix into them. Only the MobileGestalt interpose acts there (MISFixProcessOnlyNeedsIdentity keeps the MIS detours out). The hook now matches the obfuscated key remoted asks with, re6Zb+zwFKJNlkQTUeT+/w. - MGCopyAnswerWithError takes three arguments; the hook declared two and crashed remoted, the first hooked caller of that spelling. - vphoned sets the USB serial string, which is what usbmuxd names a device by (vphoned_usb.m, com.apple.private.usbdevice.setdescription, with AllowMultipleCreates), goes off the bus and back, and reapplies it at boot once the USB device exists. - udid.set/clear SIGKILL the hooked daemons (remoted ignores SIGTERM) and always re-enumerate, which is also what relaunches remoted. Measured on test-27.0: idevice_id, lockdown and the RSD handshake over both transports report the override after udid.set and after a reboot, and the guest's own after udid.clear. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -271,12 +271,42 @@ re-running the installer never puts an empty file over a UDID someone set.
|
||||
|
||||
The VM window sets it from Device › Set UDID… and Reset UDID, through
|
||||
vphoned's `udid.set` and `udid.clear` (`Research/vphoned_http_api.md`). vphoned
|
||||
writes the data-volume file, reads it back and restarts misagent.
|
||||
writes the data-volume file, reads it back and restarts the hooked daemons.
|
||||
|
||||
### The inconsistency this creates
|
||||
### The host sees it too (2026-09-30)
|
||||
|
||||
The guest now answers two ways about which device it is. Xcode, `devicectl`
|
||||
and lockdown still see its own UDID; only the processes carrying the hook see
|
||||
the configured one. That is deliberate and was accepted explicitly — making
|
||||
the two agree would mean a re-restore for a UDID that still could not match a
|
||||
real device's.
|
||||
This section used to say the host keeps seeing the guest's own UDID, and that
|
||||
making the two agree needed a re-restore. It needs neither. The host learns
|
||||
the UDID in three places, all of them in userspace or reachable from it:
|
||||
|
||||
| Where | Who answers | How the override gets there |
|
||||
| --- | --- | --- |
|
||||
| usbmuxd, `idevice_id` | the USB serial string | IOUSBDeviceFamily takes a new description from a process holding `com.apple.private.usbdevice.setdescription`; vphoned sets the serial (with `AllowMultipleCreates`, or the controller refuses with `0xe00002e2` once the device exists) and goes off the bus and back |
|
||||
| lockdown `GetValue UniqueDeviceID` | lockdownd | libmisfix is inserted into it; it calls `MGCopyAnswer(UniqueDeviceID)` from its main executable |
|
||||
| RSD handshake (CoreDevice, Xcode) | remoted | libmisfix is inserted into it; it calls `MGCopyAnswerWithError` with the obfuscated key `re6Zb+zwFKJNlkQTUeT+/w` |
|
||||
|
||||
The kernel builds the USB serial itself from `/chosen` `chip-id` and
|
||||
`unique-chip-id` (`%08X%016llX`); `IOPlatformSerialNumber` is `vphone-1337`
|
||||
and plays no part. vphoned starts before the USB device exists, when the
|
||||
controller has no description to change, so it retries in the background
|
||||
until it can.
|
||||
|
||||
Measured on test-27.0 with `00008150-00112233445566AA`: `idevice_id`,
|
||||
`ideviceinfo -k UniqueDeviceID` and `remotectl show` (both the
|
||||
`virtualmachine` and the `ncm` transports) all report the override, after
|
||||
`udid.set` and after a reboot; `udid.clear` puts all three back.
|
||||
`ideviceinstaller list` works under the new identity.
|
||||
|
||||
Two things turned up on the way. `MGCopyAnswerWithError` takes three
|
||||
arguments (question, options, error), and the hook had declared two; misagent
|
||||
never calls that spelling, remoted does, and it crashed at `0xe` until the
|
||||
prototype was fixed. And remoted runs with `EnableTransactions`, so SIGTERM
|
||||
does not stop it, and it is launched by the NCM link coming up, not on
|
||||
demand; vphoned sends SIGKILL and always re-enumerates.
|
||||
|
||||
What stays the guest's own is TXM's and the kernel's view, and CoreDevice's
|
||||
record of a pairing made under the old UDID. A new UDID is a new device to
|
||||
the host, so the guest asks to trust the computer again; it keeps one
|
||||
lockdown pair record per host, so switching back asks again too. Pairing
|
||||
with `devicectl` itself is not covered here: it times out against vphone
|
||||
guests for reasons unrelated to the UDID.
|
||||
|
||||
@@ -286,7 +286,7 @@ request carries `"force": true`.
|
||||
| Packages (read-only) | `packages.list`, `status`, `info {path}`, `compare`, `tweaks`, `repos` |
|
||||
| Bootstrap | `bootstrap.install {layout}`, `bootstrap.status`, `bootstrap.inspect`, `bootstrap.uninstall {jbroot, force}`, `bootstrap.firmware` (see above) |
|
||||
| Environment | `environment.status` (SHA-256 of each vphone library in `/usr/lib`, or null when absent, plus the staging directory), `environment.install {libraries: [{name, sha256}]}` (see below) |
|
||||
| Profile UDID | `udid.get`, `udid.set {udid}`, `udid.clear` — each returns `{udid, path}`, the UDID libmisfix gives misagent's profile check (null: the guest's own) and the settings file it came from; `set` and `clear` also return `restarted_pids` (see below) |
|
||||
| Profile UDID | `udid.get`, `udid.set {udid}`, `udid.clear` — each returns `{udid, path}`, the UDID the guest gives its profile checks and the host (null: the guest's own) and the settings file it came from; `set` and `clear` also return `restarted_pids`, `usb_serial` and `usb_reenumerated` (see below) |
|
||||
| Setup Assistant | `setup.status` (`{pending, running, pid, setup_done, setup_version, current_version}`), `setup.skip` **force** (sets `SetupDone`, `SetupFinishedAllSteps` and `SetupVersion` in `com.apple.purplebuddy`, restarts SpringBoard, returns the status plus `respring`); `/v1/health` carries `setup_pending` — see `Research/Guest/setup_assistant_skip.md` |
|
||||
|
||||
`processes.list` joins icli's kernel process list with `proc_pid_rusage`
|
||||
@@ -319,17 +319,24 @@ camera client loads the new hook. The result lists `installed`,
|
||||
`restarted_pids` and `reboot_required`, which is true when the launchd hook
|
||||
changed: launchd keeps the copy it mapped at boot.
|
||||
|
||||
The profile UDID methods drive `libmisfix.dylib`'s `MGCopyAnswer` interpose
|
||||
in misagent (`Research/Guest/xcode_install_signature_gate.md`). The hook reads
|
||||
The profile UDID methods drive `libmisfix.dylib`'s MobileGestalt interpose in
|
||||
misagent, installd, lockdownd and remoted, and the USB serial string
|
||||
(`Research/Guest/xcode_install_signature_gate.md`). The hook reads
|
||||
`UniqueDeviceID` from the first of `/var/db/vphone/misfix.plist` and
|
||||
`/usr/lib/libmisfix.plist` that exists. vphoned owns the first: `udid.set`
|
||||
stores the string exactly as sent, with no format check, so the API and
|
||||
`vphone.sock`'s `rpc` verb can try unusual values; `udid.clear` removes the key
|
||||
but keeps the file, so a value in the `/usr/lib` copy cannot take over. The file
|
||||
is written as a binary plist in one rename and read back, then vphoned sends
|
||||
SIGTERM to misagent; launchd starts it again for the next profile check. The
|
||||
guest does not restart, and installd is left running so an install in progress
|
||||
is not aborted. The VM window's Device › Set UDID… is stricter than the API: it
|
||||
SIGKILL to misagent, installd, lockdownd and remoted (remoted outlives
|
||||
SIGTERM), sets the USB serial to the UDID without its hyphen (the guest's own
|
||||
for `clear`) and takes the USB device off the bus and back. That relaunches
|
||||
remoted and makes usbmuxd, lockdown clients and CoreDevice read the identity
|
||||
again; `idevice_id` lists the new UDID within seconds. The guest does not
|
||||
restart. vphoned reapplies a configured UDID to the USB serial at every boot,
|
||||
once the USB device exists. A new UDID is a new device to the host: the guest
|
||||
asks to trust the computer again, and keeps one pair record per host, so
|
||||
switching back needs trusting again too. The VM window's Device › Set UDID… is stricter than the API: it
|
||||
accepts only 8 and 16 hex digits joined by a hyphen (sent upper-case) or 40 hex
|
||||
digits (sent lower-case).
|
||||
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
<true/>
|
||||
<key>com.apple.private.system-keychain</key>
|
||||
<true/>
|
||||
<key>com.apple.private.usbdevice.setdescription</key>
|
||||
<true/>
|
||||
<key>com.apple.private.usernotifications.bundle-identifiers</key>
|
||||
<true/>
|
||||
<key>com.apple.private.usernotifications.settings</key>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import VphonedNative
|
||||
|
||||
// MARK: - Profile UDID
|
||||
|
||||
/// The UDID libmisfix gives the hooked daemons when they check a provisioning
|
||||
/// profile's `ProvisionedDevices` (`VPhoneGuestComponents/MISFix`). Lockdown,
|
||||
/// Xcode and devicectl keep seeing the guest's own UDID; only the hooked
|
||||
/// daemons see this one.
|
||||
/// The UDID the guest gives: to the hooked daemons checking a provisioning
|
||||
/// profile's `ProvisionedDevices`, and to the host through lockdownd, remoted
|
||||
/// and the USB serial string (`VPhoneGuestComponents/MISFix/MISFixDeviceIdentity.c`
|
||||
/// has the three routes). TXM and the kernel keep the guest's own.
|
||||
///
|
||||
/// The hook reads the first of two files that exists and re-reads it when its
|
||||
/// modification time or size changes. vphoned owns the data-volume file.
|
||||
@@ -15,8 +16,9 @@ import Foundation
|
||||
/// cannot take over again.
|
||||
///
|
||||
/// A change also stops every daemon that carries the hook, so the next check
|
||||
/// runs in a fresh process that has cached nothing. launchd starts them on
|
||||
/// demand.
|
||||
/// runs in a fresh process that has cached nothing, and takes the USB device
|
||||
/// off the bus and back, which relaunches remoted and makes the host read the
|
||||
/// identity again. launchd starts the rest on demand.
|
||||
///
|
||||
/// That means installd as well as misagent, and it used to mean only misagent.
|
||||
/// The reason given was that installd "asks misagent", and it does not: a
|
||||
@@ -48,7 +50,7 @@ extension GuestAPI {
|
||||
/// `vpIsMISFixTarget` in `VPhoneGuestComponents/Shared/InjectionEnvironment.h`,
|
||||
/// bar SpringBoard: it carries the hook for the launch check alone, never
|
||||
/// asks for the UDID, and stopping it would take the home screen down with it.
|
||||
static let udidHookedDaemons = ["misagent", "installd"]
|
||||
static let udidHookedDaemons = ["misagent", "installd", "lockdownd", "remoted"]
|
||||
|
||||
static func executeDeviceIdentity(_ method: String, _ params: [String: Any]) throws -> [String: Any]? {
|
||||
switch method {
|
||||
@@ -74,10 +76,66 @@ extension GuestAPI {
|
||||
guard state["path"] as? String == udidConfigPaths[0], state["udid"] as? String == udid else {
|
||||
throw GuestAPIError.operationFailed("\(udidConfigPaths[0]) did not read back as written")
|
||||
}
|
||||
state["restarted_pids"] = udidHookedDaemons.flatMap { stopProcesses(named: $0) }
|
||||
// SIGKILL: remoted runs with EnableTransactions and outlives a SIGTERM.
|
||||
state["restarted_pids"] = udidHookedDaemons.flatMap { stopProcesses(named: $0, signal: SIGKILL) }
|
||||
// Always off the bus and back, even for an unchanged serial: that is
|
||||
// what launches remoted again (its launch event is the NCM link coming
|
||||
// up), and what makes the host ask lockdown who this is once more.
|
||||
let usb = try applyUSBSerial(udid, reenumerate: true)
|
||||
state["usb_serial"] = usb.serial
|
||||
state["usb_reenumerated"] = usb.changed
|
||||
return state
|
||||
}
|
||||
|
||||
/// Shows the host `udid` — or the guest's own UDID when nil — as the USB
|
||||
/// serial string, which is where usbmuxd, and so `idevice_id`, gets it.
|
||||
/// The dashes go, as on a real device: usbmuxd puts one back after the
|
||||
/// eighth character of a 24-character serial. Returns the serial in effect and
|
||||
/// whether the device went off the bus to show it.
|
||||
@discardableResult
|
||||
static func applyUSBSerial(_ udid: String?, reenumerate: Bool = false) throws -> (serial: String, changed: Bool) {
|
||||
let serial: String
|
||||
if let udid {
|
||||
serial = udid.replacingOccurrences(of: "-", with: "")
|
||||
} else {
|
||||
guard let own = vp_usb_own_serial() else {
|
||||
throw GuestAPIError.operationFailed("/chosen has no chip-id or unique-chip-id")
|
||||
}
|
||||
serial = String(cString: own)
|
||||
free(own)
|
||||
}
|
||||
var changed = false
|
||||
if let error = vp_usb_set_serial(serial, reenumerate, &changed) {
|
||||
defer { free(error) }
|
||||
throw GuestAPIError.operationFailed(String(cString: error))
|
||||
}
|
||||
return (serial, changed)
|
||||
}
|
||||
|
||||
/// Puts a configured UDID back on the USB serial after a boot or a vphoned
|
||||
/// restart; the kernel starts from the guest's own every time. A guest with
|
||||
/// no override is left untouched.
|
||||
///
|
||||
/// vphoned starts before the USB device exists, and until it does the
|
||||
/// controller has no description to change (measured on test-27.0). So this
|
||||
/// retries in the background, every two seconds for five minutes, until the
|
||||
/// serial is set.
|
||||
static func restoreUSBSerialOnStartup(attempt: Int = 0) {
|
||||
guard let udid = udidState()["udid"] as? String else { return }
|
||||
do {
|
||||
let usb = try applyUSBSerial(udid)
|
||||
NSLog("vphoned: USB serial %@ after %d retries", usb.serial, attempt)
|
||||
} catch {
|
||||
guard attempt < 150 else {
|
||||
NSLog("vphoned: USB serial not set: %@", String(describing: error))
|
||||
return
|
||||
}
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2) {
|
||||
restoreUSBSerialOnStartup(attempt: attempt + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The UDID the hook answers with and the file it comes from, resolved the
|
||||
/// way `MISFixCopyConfiguredDeviceIdentifier` resolves it. `udid` is null
|
||||
/// when the guest answers with its own.
|
||||
|
||||
@@ -169,12 +169,12 @@ extension GuestAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends SIGTERM to every process with this name; launchd starts an
|
||||
/// on-demand daemon again for its next client.
|
||||
static func stopProcesses(named name: String) -> [Int] {
|
||||
/// Sends `signal` (SIGTERM unless told otherwise) to every process with this
|
||||
/// name; launchd starts an on-demand daemon again for its next client.
|
||||
static func stopProcesses(named name: String, signal: Int32 = SIGTERM) -> [Int] {
|
||||
let rows = (try? listProcesses(filter: name))?["processes"] as? [[String: Any]] ?? []
|
||||
return rows.compactMap { row in
|
||||
guard row["name"] as? String == name, let pid = row["pid"] as? Int, kill(pid_t(pid), SIGTERM) == 0
|
||||
guard row["name"] as? String == name, let pid = row["pid"] as? Int, kill(pid_t(pid), signal) == 0
|
||||
else { return nil }
|
||||
return pid
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ vp_vcam_start()
|
||||
// that pays for `dlopen`; a base without the symbols logs and stays a no-op.
|
||||
_ = vp_hid_load()
|
||||
GuestIrisinInstaller.refreshBootstrapOnStartup()
|
||||
GuestAPI.restoreUSBSerialOnStartup()
|
||||
|
||||
let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
||||
let filePool = NIOThreadPool(numberOfThreads: 2)
|
||||
|
||||
@@ -39,3 +39,11 @@ typedef struct {
|
||||
|
||||
/// Identity and resource usage for one process. Returns false when the process is gone.
|
||||
bool vp_process_usage(int pid, VPProcessUsage *usage);
|
||||
|
||||
/// Make `serial` the USB serial string the host sees, re-enumerating only when
|
||||
/// it changes or `force` is set (`*changed`). Returns a malloc-owned error or NULL.
|
||||
char *vp_usb_set_serial(const char *serial, bool force, bool *changed);
|
||||
|
||||
/// The guest's own USB serial, built from `/chosen` `chip-id` and
|
||||
/// `unique-chip-id`. malloc-owned, or NULL when the device tree lacks either.
|
||||
char *vp_usb_own_serial(void);
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// vphoned_usb.m — the serial number the guest shows the host over USB.
|
||||
//
|
||||
// The host's usbmuxd names a device after its USB serial string, and that is
|
||||
// what `idevice_id` lists and what a usbmuxd client connects by. The guest
|
||||
// kernel fills it with the UDID it builds from `chip-id` and `unique-chip-id`;
|
||||
// IOUSBDeviceFamily takes a replacement from any process holding
|
||||
// `com.apple.private.usbdevice.setdescription`, which vphoned does. Taking the
|
||||
// device off the bus and back makes the host enumerate it again and read the
|
||||
// new string.
|
||||
//
|
||||
// lockdownd and remoted answer with the override through libmisfix; this is
|
||||
// the third place the host learns the UDID, and the only one in the kernel.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "VphonedNative.h"
|
||||
|
||||
typedef struct __IOUSBDeviceController *VPUSBControllerRef;
|
||||
typedef struct __IOUSBDeviceDescription *VPUSBDescriptionRef;
|
||||
|
||||
static IOReturn (*pControllerCreate)(CFAllocatorRef, VPUSBControllerRef *);
|
||||
static VPUSBDescriptionRef (*pDescriptionFromController)(CFAllocatorRef, VPUSBControllerRef);
|
||||
static CFStringRef (*pGetSerial)(VPUSBDescriptionRef);
|
||||
static void (*pSetSerial)(VPUSBDescriptionRef, CFStringRef);
|
||||
static CFMutableDictionaryRef (*pGetInfo)(VPUSBDescriptionRef); // exported with a leading underscore
|
||||
static IOReturn (*pSetDescription)(VPUSBControllerRef, VPUSBDescriptionRef);
|
||||
static IOReturn (*pGoOffAndOnBus)(VPUSBControllerRef, uint32_t);
|
||||
|
||||
static char *failure(NSString *message) {
|
||||
return strdup(message.UTF8String);
|
||||
}
|
||||
|
||||
static bool load_symbols(void) {
|
||||
static bool loaded;
|
||||
if (loaded) return true;
|
||||
void *ioKit = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
|
||||
if (!ioKit) return false;
|
||||
pControllerCreate = dlsym(ioKit, "IOUSBDeviceControllerCreate");
|
||||
pDescriptionFromController = dlsym(ioKit, "IOUSBDeviceDescriptionCreateFromController");
|
||||
pGetSerial = dlsym(ioKit, "IOUSBDeviceDescriptionGetSerialString");
|
||||
pSetSerial = dlsym(ioKit, "IOUSBDeviceDescriptionSetSerialString");
|
||||
pGetInfo = dlsym(ioKit, "_IOUSBDeviceDescriptionGetInfo");
|
||||
pSetDescription = dlsym(ioKit, "IOUSBDeviceControllerSetDescription");
|
||||
pGoOffAndOnBus = dlsym(ioKit, "IOUSBDeviceControllerGoOffAndOnBus");
|
||||
loaded = pControllerCreate && pDescriptionFromController && pGetSerial && pSetSerial && pGetInfo
|
||||
&& pSetDescription && pGoOffAndOnBus;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
char *vp_usb_set_serial(const char *serial, bool force, bool *changed) {
|
||||
*changed = false;
|
||||
if (!load_symbols()) return failure(@"IOUSBDeviceController symbols are missing");
|
||||
|
||||
VPUSBControllerRef controller = NULL;
|
||||
IOReturn status = pControllerCreate(kCFAllocatorDefault, &controller);
|
||||
if (status != kIOReturnSuccess || !controller)
|
||||
return failure([NSString stringWithFormat:@"IOUSBDeviceControllerCreate: 0x%08x", status]);
|
||||
|
||||
char *error = NULL;
|
||||
VPUSBDescriptionRef description = pDescriptionFromController(kCFAllocatorDefault, controller);
|
||||
if (!description) {
|
||||
error = failure(@"The USB controller has no description to change");
|
||||
} else {
|
||||
NSString *wanted = @(serial);
|
||||
NSString *current = (__bridge NSString *)pGetSerial(description);
|
||||
if (force || ![current isEqualToString:wanted]) {
|
||||
pSetSerial(description, (__bridge CFStringRef)wanted);
|
||||
// The device already exists; the controller refuses a second
|
||||
// description that does not say it may replace the first.
|
||||
CFMutableDictionaryRef info = pGetInfo(description);
|
||||
if (info) CFDictionarySetValue(info, CFSTR("AllowMultipleCreates"), kCFBooleanTrue);
|
||||
status = pSetDescription(controller, description);
|
||||
if (status != kIOReturnSuccess) {
|
||||
error = failure([NSString stringWithFormat:@"IOUSBDeviceControllerSetDescription: 0x%08x", status]);
|
||||
} else {
|
||||
// Long enough for the host to see a disconnect rather than a glitch.
|
||||
status = pGoOffAndOnBus(controller, 1000);
|
||||
if (status != kIOReturnSuccess)
|
||||
error = failure([NSString stringWithFormat:@"IOUSBDeviceControllerGoOffAndOnBus: 0x%08x", status]);
|
||||
else
|
||||
*changed = true;
|
||||
}
|
||||
}
|
||||
CFRelease(description);
|
||||
}
|
||||
CFRelease(controller);
|
||||
return error;
|
||||
}
|
||||
|
||||
static bool read_chosen(io_registry_entry_t chosen, CFStringRef key, void *value, size_t size) {
|
||||
CFTypeRef data = IORegistryEntryCreateCFProperty(chosen, key, kCFAllocatorDefault, 0);
|
||||
bool ok = data && CFGetTypeID(data) == CFDataGetTypeID() && (size_t)CFDataGetLength(data) >= size;
|
||||
if (ok) CFDataGetBytes(data, CFRangeMake(0, (CFIndex)size), value);
|
||||
if (data) CFRelease(data);
|
||||
return ok;
|
||||
}
|
||||
|
||||
char *vp_usb_own_serial(void) {
|
||||
// The same inputs, in the same format, as the kernel's own serial and the
|
||||
// host's `udid-prediction.txt`.
|
||||
io_registry_entry_t chosen = IORegistryEntryFromPath(kIOMainPortDefault, "IODeviceTree:/chosen");
|
||||
if (chosen == MACH_PORT_NULL) return NULL;
|
||||
uint32_t chipID = 0;
|
||||
uint64_t ecid = 0;
|
||||
bool ok = read_chosen(chosen, CFSTR("chip-id"), &chipID, sizeof(chipID))
|
||||
&& read_chosen(chosen, CFSTR("unique-chip-id"), &ecid, sizeof(ecid));
|
||||
IOObjectRelease(chosen);
|
||||
if (!ok) return NULL;
|
||||
char *serial = NULL;
|
||||
asprintf(&serial, "%08X%016llX", chipID, ecid);
|
||||
return serial;
|
||||
}
|
||||
+2
-1
@@ -169,7 +169,8 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
Installs the launchd environment and plists the guest tools read, and the hooks \
|
||||
launchd and SystemHook insert at spawn. Among them is libmisfix.dylib, inserted \
|
||||
into installd, misagent and SpringBoard, which lets Xcode install and launch an \
|
||||
app signed for someone else's team, or ad hoc, without writing the shared cache.
|
||||
app signed for someone else's team, or ad hoc, without writing the shared cache, \
|
||||
and into lockdownd and remoted, which tell the host a configured UDID.
|
||||
""",
|
||||
target: .guestFile(path: "/Library/LaunchDaemons"),
|
||||
bootEssential: true,
|
||||
|
||||
@@ -254,3 +254,7 @@ void MISFixLog(const char *format, ...) {
|
||||
return;
|
||||
MISFixNote("%s", message);
|
||||
}
|
||||
|
||||
int MISFixProcessOnlyNeedsIdentity(void) {
|
||||
return MISFixProcessIs("lockdownd") || MISFixProcessIs("remoted");
|
||||
}
|
||||
|
||||
@@ -79,6 +79,11 @@ const char *MISFixCallerImage(const void *address);
|
||||
/// change a process the hook has no business in.
|
||||
int MISFixProcessIs(const char *name);
|
||||
|
||||
/// Whether this is lockdownd or remoted, which carry the dylib only so the host
|
||||
/// is told the configured UDID. They never evaluate a signature, so the MIS
|
||||
/// detours leave their copy of libmis alone.
|
||||
int MISFixProcessOnlyNeedsIdentity(void);
|
||||
|
||||
/// The image of whoever called the function this appears in.
|
||||
#define MISFixCaller() MISFixCallerImage(__builtin_return_address(0))
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
//
|
||||
// ## How far this reaches, measured
|
||||
//
|
||||
// misagent, and nothing else. Its main executable calls `MGCopyAnswer` itself,
|
||||
// so the interpose catches it and a profile naming the configured device
|
||||
// installs.
|
||||
// misagent, lockdownd and remoted: their main executables call MobileGestalt
|
||||
// themselves, so the interpose catches them. In misagent a profile naming the
|
||||
// configured device installs.
|
||||
//
|
||||
// installd does not benefit and no interpose can make it. Its profile check
|
||||
// runs MobileInstallation → libmis → libMobileGestalt, all three inside the
|
||||
@@ -80,19 +80,30 @@
|
||||
// for real instead of being skipped, which is closer to what the device would
|
||||
// have done.
|
||||
//
|
||||
// ## The inconsistency this creates, stated plainly
|
||||
// ## What the host sees (2026-09-30)
|
||||
//
|
||||
// The guest gives two different answers about which device it is. What Xcode,
|
||||
// `devicectl` and lockdown report is unchanged — that UDID is built by TXM
|
||||
// before the kernel runs, out of the device tree's `chip-id` and
|
||||
// `unique-chip-id`, and nothing in userspace can alter it. Only the processes
|
||||
// carrying this hook see the configured value.
|
||||
// The host learns the UDID three ways, and all three now give the configured
|
||||
// one, so Xcode signs for a device the team has registered:
|
||||
//
|
||||
// That is deliberate. Making the two agree would mean rewriting
|
||||
// `unique-chip-id`, which is the ECID the guest's SHSH blob is issued against,
|
||||
// so the VM would have to be restored again — and `chip-id` is fixed at
|
||||
// 0x0000FE01 by the virtual SoC, so a real iPhone's UDID could not be
|
||||
// reproduced even then.
|
||||
// usbmuxd (`idevice_id`) the USB serial string. The kernel builds it from
|
||||
// `chip-id` and `unique-chip-id`; vphoned replaces it
|
||||
// (`vphoned_usb.m`) and takes the device off the bus
|
||||
// and back so the host reads it again.
|
||||
// lockdown `GetValue` lockdownd carries this hook and asks
|
||||
// `MGCopyAnswer(UniqueDeviceID)` itself.
|
||||
// RSD handshake remoted carries this hook and asks through
|
||||
// `MGCopyAnswerWithError` with the obfuscated key
|
||||
// (`kMISFixUniqueDeviceIDObfuscatedProperty`).
|
||||
//
|
||||
// What stays the guest's own: TXM's and the kernel's view (AMFI, codesigning),
|
||||
// which is built before any of this runs, and CoreDevice's record of a pairing
|
||||
// made under the old UDID, which it keeps until that pairing is removed. The
|
||||
// guest keeps one lockdown pair record per host, so switching the UDID asks
|
||||
// the host to be trusted again.
|
||||
//
|
||||
// Matching a real iPhone's UDID in the kernel as well would mean rewriting
|
||||
// `unique-chip-id`, the ECID the SHSH blob is issued against, and `chip-id` is
|
||||
// fixed at 0x0000FE01 by the virtual SoC. Userspace is where this stops.
|
||||
|
||||
#include "MISFixConfig.h"
|
||||
#include "MISFixInterpose.h"
|
||||
@@ -100,7 +111,10 @@
|
||||
#include <mach-o/dyld.h>
|
||||
|
||||
extern CFTypeRef MGCopyAnswer(CFStringRef property);
|
||||
extern CFTypeRef MGCopyAnswerWithError(CFStringRef property, uint32_t *error);
|
||||
// Three arguments: the middle one is an options dictionary. Declaring two
|
||||
// passed the caller's options through as the error pointer, and remoted — the
|
||||
// first hooked process to call this spelling — faulted writing to it.
|
||||
extern CFTypeRef MGCopyAnswerWithError(CFStringRef property, CFDictionaryRef options, uint32_t *error);
|
||||
|
||||
/// Log every MobileGestalt query this hook sees, and whether it answered.
|
||||
///
|
||||
@@ -149,13 +163,21 @@ __attribute__((constructor)) static void vpAnnounce(void) {
|
||||
/// there is no public header, and this is the literal misagent carries.
|
||||
#define kMISFixUniqueDeviceIDProperty CFSTR("UniqueDeviceID")
|
||||
|
||||
/// The same key as MobileGestalt also accepts it: base64 of
|
||||
/// MD5("MGCopyAnswer" + key), unpadded. remoted asks this way when it builds the
|
||||
/// RSD handshake that CoreDevice and Xcode read the UDID from.
|
||||
#define kMISFixUniqueDeviceIDObfuscatedProperty CFSTR("re6Zb+zwFKJNlkQTUeT+/w")
|
||||
|
||||
/// The configured answer for `property`, already retained for the caller, or
|
||||
/// NULL to let MobileGestalt answer.
|
||||
static CFTypeRef vpOverrideFor(CFStringRef property) {
|
||||
if (property == NULL || CFGetTypeID(property) != CFStringGetTypeID())
|
||||
return NULL;
|
||||
if (!CFEqual(property, kMISFixUniqueDeviceIDProperty))
|
||||
if (!CFEqual(property, kMISFixUniqueDeviceIDProperty)
|
||||
&& !CFEqual(property, kMISFixUniqueDeviceIDObfuscatedProperty))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CFStringRef configured = MISFixCopyConfiguredDeviceIdentifier();
|
||||
if (configured == NULL)
|
||||
@@ -172,19 +194,18 @@ static CFTypeRef vpMGCopyAnswer(CFStringRef property) {
|
||||
return override != NULL ? override : MGCopyAnswer(property);
|
||||
}
|
||||
|
||||
static CFTypeRef vpMGCopyAnswerWithError(CFStringRef property, uint32_t *error) {
|
||||
static CFTypeRef vpMGCopyAnswerWithError(CFStringRef property, CFDictionaryRef options, uint32_t *error) {
|
||||
const char *caller = MISFixCaller();
|
||||
CFTypeRef override = vpOverrideFor(property);
|
||||
vpLogQuery(property, override != NULL, caller);
|
||||
if (override == NULL)
|
||||
return MGCopyAnswerWithError(property, error);
|
||||
return MGCopyAnswerWithError(property, options, error);
|
||||
if (error != NULL)
|
||||
*error = 0;
|
||||
return override;
|
||||
}
|
||||
|
||||
// Both spellings are replaced. misagent imports only the plain one; the
|
||||
// variant is covered so a different consumer of this hook cannot read around
|
||||
// it by accident.
|
||||
// Both spellings are replaced. misagent imports only the plain one;
|
||||
// remoted asks for `UniqueDeviceID` through the variant.
|
||||
MISFIX_INTERPOSE(vpMGCopyAnswer, MGCopyAnswer);
|
||||
MISFIX_INTERPOSE(vpMGCopyAnswerWithError, MGCopyAnswerWithError);
|
||||
|
||||
@@ -67,6 +67,8 @@ static CFTypeRef vpProfileGetValue(CFTypeRef profile, CFStringRef key) {
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void vpInstallProfileScopeHook(void) {
|
||||
if (MISFixProcessOnlyNeedsIdentity())
|
||||
return;
|
||||
MISFixDetourResult result = MISFixDetour(
|
||||
"MISProfileGetValue",
|
||||
(void *)&MISProfileGetValue,
|
||||
|
||||
@@ -303,6 +303,8 @@ static int vpValidateWithProgress(
|
||||
/// property of one libmis build and the log is how the next one tells us it
|
||||
/// changed.
|
||||
__attribute__((constructor)) static void vpInstallSignatureHooks(void) {
|
||||
if (MISFixProcessOnlyNeedsIdentity())
|
||||
return;
|
||||
MISFixDetourResult body = MISFixDetour(
|
||||
"MISValidateSignatureAndCopyInfoWithProgress",
|
||||
(void *)&MISValidateSignatureAndCopyInfoWithProgress,
|
||||
|
||||
@@ -22,7 +22,8 @@ static int vpPathHasSuffix(const char *path, const char *suffix) {
|
||||
|
||||
// The processes that evaluate a code signature or a provisioning profile, and
|
||||
// so the ones that have to agree about what device this is and what signatures
|
||||
// are acceptable. Everything else spawns without libmisfix.
|
||||
// are acceptable, plus the two that tell the host which device this is.
|
||||
// Everything else spawns without libmisfix.
|
||||
//
|
||||
// installd runs `+[MICodeSigningVerifier
|
||||
// _validateSignatureAndCopyInfoForURL:withOptions:error:]`, which
|
||||
@@ -30,6 +31,10 @@ static int vpPathHasSuffix(const char *path, const char *suffix) {
|
||||
// misagent installs the embedded profile and checks ProvisionedDevices.
|
||||
// SpringBoard asks MIS again at launch; without the hook an installed app is
|
||||
// refused there with 0xE8008026.
|
||||
// lockdownd answers lockdown `GetValue UniqueDeviceID` (usbmuxd clients).
|
||||
// remoted puts `UniqueDeviceID` in the RSD handshake (CoreDevice, Xcode).
|
||||
// These two get the MobileGestalt override only; the MIS detours
|
||||
// stand down in them (`MISFixProcessOnlyNeedsIdentity`).
|
||||
//
|
||||
// Both spawn hooks ask this, because the targets do not share a parent:
|
||||
// installd and misagent are started through xpcproxy, which carries
|
||||
@@ -44,6 +49,8 @@ static int vpIsMISFixTarget(const char *path) {
|
||||
return 0;
|
||||
return vpPathHasSuffix(path, "/usr/libexec/installd") ||
|
||||
vpPathHasSuffix(path, "/usr/libexec/misagent") ||
|
||||
vpPathHasSuffix(path, "/usr/libexec/lockdownd") ||
|
||||
vpPathHasSuffix(path, "/usr/libexec/remoted") ||
|
||||
vpPathHasSuffix(path, "/SpringBoard.app/SpringBoard");
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ static void leavesACompleteEnvironmentAlone(void) {
|
||||
static void namesTheMISFixTargets(void) {
|
||||
assert(vpIsMISFixTarget("/usr/libexec/installd"));
|
||||
assert(vpIsMISFixTarget("/usr/libexec/misagent"));
|
||||
assert(vpIsMISFixTarget("/usr/libexec/lockdownd"));
|
||||
assert(vpIsMISFixTarget("/usr/libexec/remoted"));
|
||||
assert(vpIsMISFixTarget("/System/Library/CoreServices/SpringBoard.app/SpringBoard"));
|
||||
assert(!vpIsMISFixTarget("/usr/libexec/xpcproxy"));
|
||||
assert(!vpIsMISFixTarget("/usr/libexec/installdx"));
|
||||
|
||||
Reference in New Issue
Block a user