mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-01 23:54:35 +08:00
Let Xcode install a real IPA on a guest no profile names
An interpose never reached installd, so none of libmisfix's signature work
had ever run there. Replace it with a detour at the top of the callee, and
answer the two MobileInstallation refusals above it.
- MISFixDetour now takes an address, because no spelling of dlsym can
return one dyld has not interposed. It refuses a target shorter than the
four-word jump, which is what MISValidateSignatureAndCopyInfo is.
- MISFixProfileScope answers ProvisionsAllDevices for every profile, so the
embedded profile installs for real and MIS validates the app against it
with a genuine signer, entitlements and cdhash.
- MISFixInstallPolicy swizzles the embedded-profile install and the code
signing verifier, letting each refusal through after the real
implementation has run.
- MISFixNote appends to a file as well as the unified log, because a live
syslog tail has no lookback and every hook reports from a constructor.
Measured on test-26.4: a paid team's dev-signed IPA installs through
devicectl and launches. An ad-hoc signature is accepted by MIS and still
refused above it; Research/0_binary_patch_comparison.md says where.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1830,6 +1830,102 @@ applies interposing to `dlsym` as well as to call sites, so
|
||||
the probe measured its own text and never touched the cache. It then asked for
|
||||
`VM_PROT_WRITE` in place of `VM_PROT_EXECUTE` on the page it was executing
|
||||
from, which faults on the next instruction fetch and crash-looped installd
|
||||
until the flag was cleared. The probe now resolves through a handle on
|
||||
`libMobileGestalt` (a handle-scoped `dlsym` is not interposed), refuses a
|
||||
target inside its own image, asks for RWX, and runs in installd only.
|
||||
until the flag was cleared.
|
||||
|
||||
A handle-scoped `dlsym` is interposed too — measured, on a handle to
|
||||
libMobileGestalt itself — so there is no spelling of `dlsym` that answers this.
|
||||
What dyld leaves alone is the interposing image's own imports, which is why
|
||||
`MISFixDetour` takes an address that libmisfix obtained with `&`, and refuses
|
||||
to take a name at all.
|
||||
|
||||
## An Xcode install works, and what it took (2026-09-30)
|
||||
|
||||
Measured on test-26.4, `xcrun devicectl device install app` with
|
||||
`AirBuild-Debug.ipa` — a paid team's app (`QDJ93ZUQ9B`), signed
|
||||
`Apple Development`, whose embedded profile provisions eight real devices and
|
||||
no VM:
|
||||
|
||||
```
|
||||
App installed:
|
||||
• bundleID: plus.yellow.AirBuild
|
||||
• installationURL: file:///private/var/containers/Bundle/Application/9A626C1C-…/AirBuild.app/
|
||||
```
|
||||
|
||||
and it launches. Four separate refusals had to go, in this order, and each one
|
||||
was only visible once the one before it was gone.
|
||||
|
||||
1. **The interpose never ran.** Replaced by `MISFixDetour`: a four-word
|
||||
absolute jump at the top of the callee, the displaced instructions
|
||||
relocated onto an `mmap`ed trampoline, the target page taken
|
||||
copy-on-write. Installed in installd's own address space, so nothing on
|
||||
disk and no other process changes — which is the whole difference between
|
||||
this and row 17, the libmis cache patch that stopped a 27.0 guest booting.
|
||||
Measured: `detour: MISValidateSignatureAndCopyInfoWithProgress at
|
||||
0x1bf41c830 in libmis.dylib`.
|
||||
|
||||
`MISValidateSignatureAndCopyInfo` itself is a thunk in front of the
|
||||
`…WithProgress` body, shorter than the jump, and `MISFixDetour` refuses it
|
||||
with `MISFixDetourTooShort` rather than write over whatever follows. Its
|
||||
callers are covered anyway, because it branches into the hooked function.
|
||||
|
||||
2. **`0xE8008015`, no valid profile.** Widening the options does not help a
|
||||
CMS-signed app: `AllowAdHocSigning` is about ad-hoc signatures, and this one
|
||||
is real. The profile has to actually install, and misagent refuses it
|
||||
because a VM's UDID is in no `ProvisionedDevices`. misagent asks
|
||||
`MISProfileGetValue(profile, "ProvisionsAllDevices")` *first* and only
|
||||
consults the device list when that is false — so `MISFixProfileScope.c`
|
||||
detours `MISProfileGetValue` and answers that one key `true`. The profile
|
||||
then installs for real and MIS validates the app against it:
|
||||
|
||||
```
|
||||
misagent: Installing provisioning profile: 50806e9b-…
|
||||
MISValidateSignature(…/extracted/Payload/AirBuild.app) -> 0x0
|
||||
info[SigningID] = plus.yellow.AirBuild info[TeamID] = QDJ93ZUQ9B
|
||||
info[SignerCertificate] = <1484 bytes> info[Entitlements] = <7 entries>
|
||||
info[ValidatedByProfile] = true info[SignerType] = 3
|
||||
```
|
||||
|
||||
Nothing is faked: the signature, the certificate, the entitlements and the
|
||||
cdhash are the ones Apple issued. The only claim widened is which devices
|
||||
the profile covers.
|
||||
|
||||
3. **`0xE8008012` from `-[MIInstallableBundle _installEmbeddedProfilesWithError:]`.**
|
||||
Kept as a backstop for a profile that still cannot install, in
|
||||
`MISFixInstallPolicy.c`: the real implementation runs, and a refusal is
|
||||
logged and turned into "there is no profile" rather than a failed install.
|
||||
|
||||
4. **`-[MICodeSigningVerifier performValidationWithError:]`, line 424, "Failed
|
||||
to extract signer identity".** The gate behind the gate. MIS accepts the
|
||||
bundle and MobileInstallation then wants a CMS leaf certificate out of it.
|
||||
Same treatment: run the real implementation, allow its refusal.
|
||||
|
||||
Both Objective-C hooks are swizzles, not detours. A method list is data, so
|
||||
replacing an implementation reaches every caller without making any cache text
|
||||
writable; where that is available it is strictly better.
|
||||
|
||||
### Still refused: an ad-hoc signature
|
||||
|
||||
`codesign --sign -` is accepted by MIS (`-> 0x0`, with a real `CdHash` and
|
||||
`SigningID`) and still fails the install:
|
||||
|
||||
```
|
||||
-[MIExecutableBundle codeSigningInfoByValidatingResources:…]: 1306:
|
||||
Code signing identifier ((null)) does not match bundle identifier (wiki.qaq.vphone.signtest)
|
||||
```
|
||||
|
||||
The identifier is null because `performValidationWithError:` bailed at the
|
||||
signer before storing anything, and allowing its *return value* through does
|
||||
not populate the verifier's outputs. An ad-hoc bundle has no signer and never
|
||||
will, so this one needs a different answer than "let the refusal through" —
|
||||
either the verifier's outputs supplied directly, or the caller of
|
||||
`codeSigningInfoByValidatingResources:` answered instead. Unsigned and
|
||||
ad-hoc bundles still install through vphoned's `apps.install`, which does not
|
||||
involve installd at all.
|
||||
|
||||
### A trap in the measurement, not in the guest
|
||||
|
||||
Two runs failed with `0xE8008017` on a bundle whose signature was fine. The
|
||||
IPA had been repacked on the host with `zip -r`, which writes AppleDouble
|
||||
`._*` files next to every resource; they break the sealed resource envelope.
|
||||
`COPYFILE_DISABLE=1 zip -X` after deleting them, and the same bundle installs.
|
||||
Worth remembering before reading `0xE8008017` as a guest-side gate.
|
||||
|
||||
+17
-13
@@ -130,28 +130,32 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
),
|
||||
VPhonePatchDeclaration(
|
||||
identifier: "system-installd-cfw-adhoc_signature",
|
||||
title: "installd ad-hoc signatures",
|
||||
title: "installd signature policy",
|
||||
summary: """
|
||||
Lets Xcode install an app the guest would otherwise refuse. installd asks \
|
||||
MobileIdentityService to validate a bundle without allowing an ad-hoc \
|
||||
signature, so anything not signed with an Apple leaf fails at \
|
||||
0xE8008014 even though the guest runs unsigned code perfectly well. A \
|
||||
hook in /usr/lib/libmisfix.dylib, loaded into installd, sets the option \
|
||||
MIS already understands. Nothing in the dyld shared cache is touched.
|
||||
signature and insists on a provisioning profile no VM can satisfy, so an \
|
||||
install fails at 0xE8008014 or 0xE8008015 even though the guest runs \
|
||||
unsigned code perfectly well. A hook in /usr/lib/libmisfix.dylib, loaded \
|
||||
into installd, sets the options MIS already understands, answers \
|
||||
ProvisionsAllDevices for every profile, and lets MobileInstallation's own \
|
||||
refusals through. Nothing in the dyld shared cache is written on disk: the \
|
||||
two libmis functions are detoured in installd's own copy-on-write pages.
|
||||
""",
|
||||
target: .guestExecutable(path: "/usr/libexec/installd"),
|
||||
),
|
||||
VPhonePatchDeclaration(
|
||||
identifier: "system-misagent-cfw-device_identity",
|
||||
title: "misagent device identity",
|
||||
title: "misagent profile scope",
|
||||
summary: """
|
||||
Lets a provisioning profile written for a device you already own install on \
|
||||
this guest. misagent compares the profile's ProvisionedDevices against the \
|
||||
UDID MobileGestalt reports, and a VM's UDID is in nobody's list, so a paid \
|
||||
team's profile fails at 0xE8008012. The same hook, loaded into misagent, \
|
||||
answers that one query with the UDID set in /usr/lib/libmisfix.plist. Off \
|
||||
until a UDID is set there, and it does not change what Xcode or lockdown \
|
||||
report.
|
||||
Lets any provisioning profile install on this guest. misagent asks the \
|
||||
profile whether it provisions all devices and otherwise compares its \
|
||||
ProvisionedDevices against the UDID MobileGestalt reports; a VM's UDID is \
|
||||
in nobody's list, so a paid team's profile fails at 0xE8008012. The same \
|
||||
hook, loaded into misagent, answers the first question yes, so the profile \
|
||||
installs for real and the app is validated against it. It can also answer \
|
||||
the UDID query with a device set in /usr/lib/libmisfix.plist, which is off \
|
||||
until one is set and does not change what Xcode or lockdown report.
|
||||
""",
|
||||
target: .guestExecutable(path: "/usr/libexec/misagent"),
|
||||
),
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
// MISFixCacheWriteProbe.c — can this process write a shared-cache text page?
|
||||
//
|
||||
// One question, asked at load, behind the `ProbeCacheWrite` flag, and it
|
||||
// decides how the whole MIS problem gets fixed.
|
||||
//
|
||||
// `__DATA,__interpose` rewrites *call sites* in the images dyld links, so it
|
||||
// never reaches a call made from one shared-cache image to another — measured
|
||||
// in MISFixDeviceIdentity.c, and the reason libmisfix cannot touch installd's
|
||||
// profile check. Rewriting the *callee* instead would reach every caller,
|
||||
// inside the cache or out: the classic five-instruction detour at the top of
|
||||
// `MGCopyAnswer` and `MISValidateSignatureAndCopyInfo`, with the displaced
|
||||
// instructions moved to a trampoline.
|
||||
//
|
||||
// That costs a few hundred lines of arm64e relocation work, and all of it is
|
||||
// wasted unless the process can make a cache text page writable first. The
|
||||
// cache is mapped read-execute and shared by every process on the system, so
|
||||
// the only way in is copy-on-write: ask for `VM_PROT_COPY` and get a private
|
||||
// copy of that page. Whether the kernel allows it here depends on this guest's
|
||||
// codesigning patches, not on anything this dylib does — so it is measured,
|
||||
// not assumed.
|
||||
//
|
||||
// ## What the probe does, and what it deliberately does not
|
||||
//
|
||||
// It writes the bytes that are already there. The four bytes at the top of
|
||||
// `MGCopyAnswer` are read, the page is made writable *without giving up
|
||||
// execute*, those same four bytes are written back, the result is read again
|
||||
// and compared, and the page is put back to read-execute. A run that succeeds
|
||||
// completely leaves the process byte-for-byte as it found it; a run that fails
|
||||
// anywhere leaves it as it found it too, because nothing different was ever
|
||||
// written.
|
||||
//
|
||||
// Two mistakes from the first run are guarded against by name, because both
|
||||
// are easy to make again and both crash a daemon that installs software:
|
||||
// resolving the symbol through `RTLD_DEFAULT` (interposed — it returns this
|
||||
// dylib's own replacement), and asking for write *instead of* execute on a
|
||||
// page holding live code.
|
||||
//
|
||||
// The region's current and maximum protections are logged first. That is the
|
||||
// cheap half of the answer: a region whose `max_protection` carries no write
|
||||
// bit can never be made writable, and no amount of entitlement changes that.
|
||||
//
|
||||
// The probe never touches `MISValidateSignatureAndCopyInfo`, and never leaves
|
||||
// a page writable. Making a real detour is a separate change, and it should
|
||||
// not be able to happen by accident in a daemon that installs software.
|
||||
|
||||
#include "MISFixConfig.h"
|
||||
|
||||
// The iPhoneOS SDK refuses `mach/mach_vm.h` outright, so this uses the
|
||||
// `vm_*` entry points in `mach/vm_map.h` instead. On arm64 they take the same
|
||||
// 64-bit addresses and sizes; only the names differ.
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <libkern/OSCacheControl.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach/mach.h>
|
||||
#include <ptrauth.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
/// The symbol the probe stands on. Exported by libMobileGestalt, in the shared
|
||||
/// cache, and the one a real detour would go on first.
|
||||
#define kProbeSymbol "MGCopyAnswer"
|
||||
|
||||
/// The image it must come out of. Named explicitly, because the obvious way to
|
||||
/// resolve the symbol is wrong: dyld applies interposing to `dlsym` as well as
|
||||
/// to call sites, so `dlsym(RTLD_DEFAULT, "MGCopyAnswer")` returns *this
|
||||
/// dylib's* replacement. The first run of this probe did exactly that, stripped
|
||||
/// execute from the page it was executing on, and took installd down with it.
|
||||
#define kProbeImage "/usr/lib/libMobileGestalt.dylib"
|
||||
|
||||
/// How many bytes the probe rewrites. One instruction: enough to prove the
|
||||
/// page is writable, small enough that a partial write cannot straddle a page.
|
||||
#define kProbeLength 4u
|
||||
|
||||
/// The region this address is in, logged for its protections.
|
||||
///
|
||||
/// `max_protection` is the half that cannot be argued with: it is the ceiling
|
||||
/// `mach_vm_protect` may raise the current protection to, and a cache text
|
||||
/// region that does not carry `VM_PROT_WRITE` in it rules the detour out
|
||||
/// before any of the rest is tried.
|
||||
static void vpDescribeRegion(vm_address_t address) {
|
||||
vm_address_t start = address;
|
||||
vm_size_t size = 0;
|
||||
vm_region_basic_info_data_64_t info;
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object = MACH_PORT_NULL;
|
||||
kern_return_t result = vm_region_64(
|
||||
mach_task_self(),
|
||||
&start,
|
||||
&size,
|
||||
VM_REGION_BASIC_INFO_64,
|
||||
(vm_region_info_t)&info,
|
||||
&count,
|
||||
&object
|
||||
);
|
||||
if (result != KERN_SUCCESS) {
|
||||
MISFixNote("probe: vm_region_64 failed: %s", mach_error_string(result));
|
||||
return;
|
||||
}
|
||||
MISFixNote(
|
||||
"probe: region %p+%llx prot=%x max=%x shared=%d reserved=%d",
|
||||
(void *)start,
|
||||
(unsigned long long)size,
|
||||
info.protection,
|
||||
info.max_protection,
|
||||
info.shared,
|
||||
info.reserved
|
||||
);
|
||||
}
|
||||
|
||||
/// Try to make `length` bytes at `address` writable, and say how it went.
|
||||
///
|
||||
/// `VM_PROT_COPY` is the whole point: without it the request is "let this
|
||||
/// shared mapping be written", which the kernel refuses for a region other
|
||||
/// processes have mapped. With it, the request is "give me my own copy of
|
||||
/// these pages, writable", which is what a detour needs and what leaves every
|
||||
/// other process on the system untouched.
|
||||
/// Write, without execute. Measured, both ways round, and this is the way that
|
||||
/// works.
|
||||
///
|
||||
/// Asking for RWX succeeds at the VM layer — the region comes back `prot=7`,
|
||||
/// `max=7` — and then the store still faults:
|
||||
///
|
||||
/// EXC_BAD_ACCESS (SIGBUS), UNKNOWN_0x32 at 0x1027543dc
|
||||
/// __TEXT 102754000-102758000 [16K] rwx/rwx SM=COW /usr/lib/libmisfix.dylib
|
||||
///
|
||||
/// Apple silicon enforces write-xor-execute in hardware below the VM
|
||||
/// permissions, so a page that is writable *and* executable is writable only
|
||||
/// to a thread that has said so. Dropping execute for the duration is the
|
||||
/// simpler answer and the one a detour can use, because the page it rewrites
|
||||
/// is not the page it is running from — that was the first run's mistake, and
|
||||
/// the two failures look identical from outside, which is why both are
|
||||
/// written down here.
|
||||
static kern_return_t vpMakeWritable(vm_address_t address, vm_size_t length) {
|
||||
return vm_protect(
|
||||
mach_task_self(),
|
||||
address,
|
||||
length,
|
||||
FALSE,
|
||||
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY
|
||||
);
|
||||
}
|
||||
|
||||
static kern_return_t vpRestore(vm_address_t address, vm_size_t length) {
|
||||
return vm_protect(
|
||||
mach_task_self(),
|
||||
address,
|
||||
length,
|
||||
FALSE,
|
||||
VM_PROT_READ | VM_PROT_EXECUTE
|
||||
);
|
||||
}
|
||||
|
||||
/// Write back the four bytes already at `function`, and say whether they stuck.
|
||||
///
|
||||
/// Shared by both write steps, because "can this page be rewritten" is the
|
||||
/// same question for the cache and for this dylib's own text, and the answer
|
||||
/// may well differ.
|
||||
static void vpProbeWriteAt(const char *what, const uint8_t *function) {
|
||||
// Page alignment, because protection is a per-page property and asking
|
||||
// about four bytes would silently widen to the page anyway.
|
||||
vm_size_t page = vm_page_size;
|
||||
vm_address_t start = (vm_address_t)(uintptr_t)function & ~(vm_address_t)(page - 1);
|
||||
vpDescribeRegion(start);
|
||||
|
||||
uint8_t before[kProbeLength];
|
||||
memcpy(before, function, sizeof(before));
|
||||
|
||||
kern_return_t opened = vpMakeWritable(start, page);
|
||||
if (opened != KERN_SUCCESS) {
|
||||
MISFixNote("probe: %s vm_protect(rw|copy) failed: %s", what, mach_error_string(opened));
|
||||
return;
|
||||
}
|
||||
MISFixNote("probe: %s vm_protect(rw|copy) succeeded, writing", what);
|
||||
vpDescribeRegion(start);
|
||||
|
||||
// The same bytes, written back. Nothing about this process's behaviour
|
||||
// changes whether it lands or not; only whether it lands is interesting.
|
||||
memcpy((void *)(uintptr_t)function, before, sizeof(before));
|
||||
|
||||
uint8_t after[kProbeLength];
|
||||
memcpy(after, function, sizeof(after));
|
||||
int identical = memcmp(before, after, sizeof(before)) == 0;
|
||||
|
||||
kern_return_t closed = vpRestore(start, page);
|
||||
MISFixNote(
|
||||
"probe: %s wrote %u bytes, readback %s, restore r-x %s",
|
||||
what,
|
||||
kProbeLength,
|
||||
identical ? "matches" : "DIFFERS",
|
||||
closed == KERN_SUCCESS ? "ok" : mach_error_string(closed)
|
||||
);
|
||||
}
|
||||
|
||||
/// `mov w0, #42 ; ret`, for the executable-memory step.
|
||||
static const uint32_t kProbeThunk[] = { 0x52800540u, 0xD65F03C0u };
|
||||
#define kProbeThunkAnswer 42
|
||||
|
||||
/// Can this process get memory it wrote and then run it?
|
||||
///
|
||||
/// The other half of what a detour needs. Rewriting the top of a function
|
||||
/// costs nothing if the displaced instructions have nowhere to live: a
|
||||
/// trampoline is memory this process fills in and then jumps to, which on iOS
|
||||
/// is exactly what codesigning is there to prevent.
|
||||
///
|
||||
/// Both spellings are tried, write-then-`mprotect` first. That order is not
|
||||
/// arbitrary: a page that is writable *and* executable at once is subject to
|
||||
/// the same hardware write-xor-execute rule that made the cache write fault
|
||||
/// with `SIGBUS` above, so the RWX spelling is the one expected to fail and it
|
||||
/// goes second.
|
||||
///
|
||||
/// This step is last, and deliberately. It is the only part of the probe that
|
||||
/// can take the process down — running a page the kernel has not blessed is a
|
||||
/// kill, not an error return — so everything else is already in the log by the
|
||||
/// time it runs.
|
||||
static void vpProbeExecutableMemory(void) {
|
||||
for (int rwxAtOnce = 0; rwxAtOnce < 2; rwxAtOnce += 1) {
|
||||
int writeThenProtect = !rwxAtOnce;
|
||||
const char *how = writeThenProtect ? "rw then mprotect r-x" : "rwx from mmap";
|
||||
int protection = writeThenProtect ? (PROT_READ | PROT_WRITE)
|
||||
: (PROT_READ | PROT_WRITE | PROT_EXEC);
|
||||
void *page = mmap(NULL, vm_page_size, protection, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (page == MAP_FAILED) {
|
||||
MISFixNote("probe: mmap %s failed: %s", how, strerror(errno));
|
||||
continue;
|
||||
}
|
||||
memcpy(page, kProbeThunk, sizeof(kProbeThunk));
|
||||
if (writeThenProtect && mprotect(page, vm_page_size, PROT_READ | PROT_EXEC) != 0) {
|
||||
MISFixNote("probe: mprotect r-x failed: %s", strerror(errno));
|
||||
munmap(page, vm_page_size);
|
||||
continue;
|
||||
}
|
||||
sys_icache_invalidate(page, sizeof(kProbeThunk));
|
||||
MISFixNote("probe: %s mapped at %p, calling it", how, page);
|
||||
|
||||
// Signed for the indirect call arm64e requires. If the kernel refuses
|
||||
// the page, this line does not return and the log above is the record.
|
||||
int (*thunk)(void) = ptrauth_sign_unauthenticated(
|
||||
(int (*)(void))page,
|
||||
ptrauth_key_function_pointer,
|
||||
0
|
||||
);
|
||||
int answer = thunk();
|
||||
munmap(page, vm_page_size);
|
||||
MISFixNote(
|
||||
"probe: %s returned %d (%s)",
|
||||
how,
|
||||
answer,
|
||||
answer == kProbeThunkAnswer ? "usable" : "WRONG"
|
||||
);
|
||||
if (answer == kProbeThunkAnswer)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this process is the one the probe is allowed to run in.
|
||||
///
|
||||
/// installd, and only installd. SystemHook also inserts this dylib into
|
||||
/// misagent and SpringBoard, and the executable-memory step can end the
|
||||
/// process outright — in SpringBoard that is a respring, and a repeating one
|
||||
/// while the flag is on. installd is on-demand and launchd starts it again for
|
||||
/// the next client, so a kill there costs one failed install and nothing else.
|
||||
static int vpProbeIsPermittedProcess(void) {
|
||||
char path[4096];
|
||||
uint32_t size = sizeof(path);
|
||||
if (_NSGetExecutablePath(path, &size) != 0)
|
||||
return 0;
|
||||
static const char suffix[] = "/installd";
|
||||
size_t length = strlen(path);
|
||||
return length >= sizeof(suffix) - 1
|
||||
&& strcmp(path + length - (sizeof(suffix) - 1), suffix) == 0;
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void vpProbeCacheWrite(void) {
|
||||
if (!MISFixConfiguredFlag(kMISFixProbeCacheWriteKey))
|
||||
return;
|
||||
if (!vpProbeIsPermittedProcess())
|
||||
return;
|
||||
|
||||
// This dylib's own text was measured here too, as a warm-up, and it is
|
||||
// gone: it is the one page that must never lose execute, because the probe
|
||||
// is running from it, and with execute kept the store faults under the
|
||||
// hardware's write-xor-execute rule. Both spellings crash, for opposite
|
||||
// reasons, and neither says anything about the page a detour targets. What
|
||||
// it did establish before crashing is worth keeping: copy-on-write works,
|
||||
// and the page came back `prot=7 max=7` as its own region.
|
||||
|
||||
// RTLD_NOLOAD, because the answer is only interesting for an image already
|
||||
// mapped from the cache, and a handle-scoped dlsym is not interposed.
|
||||
void *image = dlopen(kProbeImage, RTLD_LAZY | RTLD_NOLOAD);
|
||||
if (image == NULL) {
|
||||
MISFixNote("probe: %s is not loaded here: %s", kProbeImage, dlerror());
|
||||
return;
|
||||
}
|
||||
void *symbol = dlsym(image, kProbeSymbol);
|
||||
dlclose(image);
|
||||
if (symbol == NULL) {
|
||||
MISFixNote("probe: %s not found in %s", kProbeSymbol, kProbeImage);
|
||||
return;
|
||||
}
|
||||
// A function pointer out of dlsym is signed on arm64e; the address the VM
|
||||
// functions want is the plain one.
|
||||
const uint8_t *function = ptrauth_strip(symbol, ptrauth_key_function_pointer);
|
||||
const char *owner = MISFixCallerImage(function);
|
||||
MISFixNote("probe: %s at %p in %s", kProbeSymbol, function, owner);
|
||||
|
||||
// Last line of defence against the first run's mistake. Whatever the
|
||||
// resolution did, refuse to touch a page this dylib's own code is on.
|
||||
Dl_info self;
|
||||
if (dladdr(ptrauth_strip((const void *)&vpProbeCacheWrite, ptrauth_key_function_pointer),
|
||||
&self) != 0
|
||||
&& self.dli_fbase != NULL)
|
||||
{
|
||||
Dl_info target;
|
||||
if (dladdr(function, &target) != 0 && target.dli_fbase == self.dli_fbase) {
|
||||
MISFixNote("probe: %s resolved into libmisfix itself — refusing", kProbeSymbol);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
vpProbeWriteAt("cache-text", function);
|
||||
|
||||
vpProbeExecutableMemory();
|
||||
MISFixNote("probe: done");
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "MISFixConfig.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <os/log.h>
|
||||
#include <ptrauth.h>
|
||||
#include <stdarg.h>
|
||||
@@ -188,6 +189,35 @@ const char *MISFixCallerImage(const void *address) {
|
||||
return slash != NULL && slash[1] != '\0' ? slash + 1 : info.dli_fname;
|
||||
}
|
||||
|
||||
// Somewhere each hooked daemon can append to, first one that opens.
|
||||
//
|
||||
// The unified log alone is not enough, and that cost a whole diagnosis cycle.
|
||||
// vphoned's `logs.syslog` is a live tail with no lookback, so a line written
|
||||
// from a constructor — which is where every hook here reports whether it
|
||||
// installed — lands before any tail can be attached and is simply not there
|
||||
// afterwards. A file is readable at leisure with `files.read`.
|
||||
//
|
||||
// installd's own cache directory is first because installd is the daemon this
|
||||
// dylib is mostly about and its sandbox certainly reaches it. /var/mobile is
|
||||
// for misagent and SpringBoard. /var/tmp is the last resort.
|
||||
static const char *const kNotePaths[] = {
|
||||
"/var/installd/Library/Caches/libmisfix.log",
|
||||
"/var/mobile/Library/Caches/libmisfix.log",
|
||||
"/var/tmp/libmisfix.log",
|
||||
};
|
||||
static const size_t kNotePathCount = sizeof(kNotePaths) / sizeof(kNotePaths[0]);
|
||||
|
||||
static void vpAppendNote(const char *message) {
|
||||
for (size_t index = 0; index < kNotePathCount; index += 1) {
|
||||
int fd = open(kNotePaths[index], O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
dprintf(fd, "libmisfix[%d]: %s\n", getpid(), message);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MISFixNote(const char *format, ...) {
|
||||
char message[512];
|
||||
va_list arguments;
|
||||
@@ -199,6 +229,7 @@ void MISFixNote(const char *format, ...) {
|
||||
// One prefix for every line this dylib writes, so a single predicate finds
|
||||
// them whichever process is carrying the hook.
|
||||
os_log(OS_LOG_DEFAULT, "libmisfix[%d]: %{public}s", getpid(), message);
|
||||
vpAppendNote(message);
|
||||
}
|
||||
|
||||
void MISFixLog(const char *format, ...) {
|
||||
|
||||
@@ -76,8 +76,4 @@ const char *MISFixCallerImage(const void *address);
|
||||
/// The flag every diagnostic in this dylib is behind.
|
||||
#define kMISFixLogQueriesKey CFSTR("LogQueries")
|
||||
|
||||
/// The flag for the shared-cache write probe. Off by default, and nothing
|
||||
/// reads it but ``MISFixCacheWriteProbe.c``.
|
||||
#define kMISFixProbeCacheWriteKey CFSTR("ProbeCacheWrite")
|
||||
|
||||
#endif
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
#include "MISFixConfig.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <libkern/OSCacheControl.h>
|
||||
#include <mach/mach.h>
|
||||
#include <ptrauth.h>
|
||||
@@ -74,6 +73,20 @@ static int64_t vpSignExtend(uint64_t value, unsigned bits) {
|
||||
return (int64_t)((value ^ mask) - mask);
|
||||
}
|
||||
|
||||
/// Whether `insn` ends the function it appears in — a return in any of its
|
||||
/// three authenticated spellings, or an unconditional branch away.
|
||||
///
|
||||
/// This is the test for "the target is shorter than the patch", and it is not
|
||||
/// hypothetical: `MISValidateSignatureAndCopyInfo` is a two-instruction thunk
|
||||
/// in front of `…WithProgress`, so a four-word jump written over it would land
|
||||
/// in whatever libmis put next.
|
||||
static int vpIsTerminator(uint32_t insn) {
|
||||
return insn == 0xD65F03C0u // ret
|
||||
|| insn == 0xD65F0BFFu // retaa
|
||||
|| insn == 0xD65F0FFFu // retab
|
||||
|| (insn & 0xFC000000u) == 0x14000000u; // b
|
||||
}
|
||||
|
||||
// MARK: - Relocation
|
||||
|
||||
/// Rewrite one displaced instruction so it means the same thing from its new
|
||||
@@ -126,8 +139,9 @@ static unsigned vpRelocate(uint32_t insn, uint64_t pc, uint32_t *out) {
|
||||
/// here. Filling it means making its page writable, which means dropping
|
||||
/// execute from a page of `__TEXT` — and the section's other occupant is the
|
||||
/// code doing the dropping. Page-aligning a 16 KB hole to avoid sharing would
|
||||
/// work and costs 16 KB in every guest, for a fallback that
|
||||
/// `MISFixCacheWriteProbe.c` exists to tell us we do not need.
|
||||
/// work and costs 16 KB in every guest, for a fallback the guest turns out not
|
||||
/// to need: `mmap` RW then `mprotect` RX then call was measured working in
|
||||
/// installd on test-26.4.
|
||||
|
||||
static kern_return_t vpProtect(const void *address, size_t length, vm_prot_t protection) {
|
||||
vm_size_t page = vm_page_size;
|
||||
@@ -151,10 +165,9 @@ static void *vpAllocateTrampoline(void) {
|
||||
const char *MISFixDetourDescribe(MISFixDetourResult result) {
|
||||
switch (result) {
|
||||
case MISFixDetourOK: return "installed";
|
||||
case MISFixDetourImageMissing: return "image is not mapped in this process";
|
||||
case MISFixDetourSymbolMissing: return "image exports no such symbol";
|
||||
case MISFixDetourSymbolIsOurs: return "symbol resolved into libmisfix itself";
|
||||
case MISFixDetourNoTarget: return "the symbol did not bind";
|
||||
case MISFixDetourUnrelocatable: return "a displaced instruction is PC-relative";
|
||||
case MISFixDetourTooShort: return "the target is shorter than the jump";
|
||||
case MISFixDetourNoTrampoline: return "no executable memory for the trampoline";
|
||||
case MISFixDetourPageReadOnly: return "the target page could not be made writable";
|
||||
case MISFixDetourWriteFailed: return "the detour did not read back as written";
|
||||
@@ -162,49 +175,15 @@ const char *MISFixDetourDescribe(MISFixDetourResult result) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/// The target address for `symbol` in `image`, stripped, or NULL.
|
||||
///
|
||||
/// Resolved through a handle rather than `RTLD_DEFAULT`, and this is not a
|
||||
/// stylistic choice: dyld applies interposing to `dlsym`, so the flat lookup
|
||||
/// for a symbol this dylib also interposes returns *our* replacement. A detour
|
||||
/// built on that would point at itself.
|
||||
static const uint8_t *vpResolve(const char *image, const char *symbol, MISFixDetourResult *why) {
|
||||
void *handle = dlopen(image, RTLD_LAZY | RTLD_NOLOAD);
|
||||
if (handle == NULL) {
|
||||
*why = MISFixDetourImageMissing;
|
||||
return NULL;
|
||||
}
|
||||
void *found = dlsym(handle, symbol);
|
||||
dlclose(handle);
|
||||
if (found == NULL) {
|
||||
*why = MISFixDetourSymbolMissing;
|
||||
return NULL;
|
||||
}
|
||||
const uint8_t *target = ptrauth_strip(found, ptrauth_key_function_pointer);
|
||||
|
||||
Dl_info self;
|
||||
Dl_info owner;
|
||||
if (dladdr(ptrauth_strip((const void *)&MISFixDetourDescribe, ptrauth_key_function_pointer),
|
||||
&self) != 0
|
||||
&& dladdr(target, &owner) != 0
|
||||
&& self.dli_fbase == owner.dli_fbase)
|
||||
{
|
||||
*why = MISFixDetourSymbolIsOurs;
|
||||
return NULL;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
MISFixDetourResult MISFixDetour(
|
||||
const char *image,
|
||||
const char *symbol,
|
||||
const char *label,
|
||||
void *function,
|
||||
void *replacement,
|
||||
void **original
|
||||
) {
|
||||
MISFixDetourResult why = MISFixDetourOK;
|
||||
const uint8_t *target = vpResolve(image, symbol, &why);
|
||||
if (target == NULL)
|
||||
return why;
|
||||
if (function == NULL)
|
||||
return MISFixDetourNoTarget;
|
||||
const uint8_t *target = ptrauth_strip(function, ptrauth_key_function_pointer);
|
||||
|
||||
// Build the trampoline before touching the target, so a refusal costs
|
||||
// nothing: the displaced instructions relocated, then a jump back to the
|
||||
@@ -215,6 +194,8 @@ MISFixDetourResult MISFixDetour(
|
||||
uint32_t body[kTrampolineBytes / 4];
|
||||
unsigned words = 0;
|
||||
for (unsigned index = 0; index < kDetourWords; index += 1) {
|
||||
if (index + 1 < kDetourWords && vpIsTerminator(displaced[index]))
|
||||
return MISFixDetourTooShort;
|
||||
unsigned written = vpRelocate(
|
||||
displaced[index],
|
||||
(uint64_t)(uintptr_t)target + index * 4u,
|
||||
@@ -234,7 +215,8 @@ MISFixDetourResult MISFixDetour(
|
||||
//
|
||||
// EXC_BAD_ACCESS (SIGBUS), UNKNOWN_0x32
|
||||
//
|
||||
// which is what `MISFixCacheWriteProbe.c` found the hard way.
|
||||
// on a region `vm_region_64` reported as `rwx/rwx SM=COW`. Found the hard
|
||||
// way, by crash-looping installd on test-26.4.
|
||||
void *trampoline = vpAllocateTrampoline();
|
||||
if (trampoline == NULL)
|
||||
return MISFixDetourNoTrampoline;
|
||||
@@ -245,6 +227,17 @@ MISFixDetourResult MISFixDetour(
|
||||
}
|
||||
sys_icache_invalidate(trampoline, words * 4u);
|
||||
|
||||
// Hand the trampoline over before the jump goes in, not after. The target
|
||||
// is live from the instant its first word changes, and a replacement that
|
||||
// reached `*original` while it was still NULL would call zero.
|
||||
if (original != NULL) {
|
||||
*original = ptrauth_sign_unauthenticated(
|
||||
trampoline,
|
||||
ptrauth_key_function_pointer,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Now the target: copy-on-write, because the cache is mapped shared and
|
||||
// read-execute. Execute is given up for the duration, which is safe here
|
||||
// and would not be on the page this code is running from.
|
||||
@@ -263,17 +256,20 @@ MISFixDetourResult MISFixDetour(
|
||||
|
||||
int landed = memcmp(target, detour, sizeof(detour)) == 0;
|
||||
vpProtect(target, kDetourBytes, VM_PROT_READ | VM_PROT_EXECUTE);
|
||||
if (!landed)
|
||||
if (!landed) {
|
||||
if (original != NULL)
|
||||
*original = NULL;
|
||||
return MISFixDetourWriteFailed;
|
||||
|
||||
if (original != NULL) {
|
||||
*original = ptrauth_sign_unauthenticated(
|
||||
trampoline,
|
||||
ptrauth_key_function_pointer,
|
||||
0
|
||||
);
|
||||
}
|
||||
MISFixNote("detour: %s in %s -> %p, trampoline %p (%u words)",
|
||||
symbol, image, replacement, trampoline, words);
|
||||
|
||||
// The owning image is named because the one way this goes quietly wrong is
|
||||
// a target that resolved back into libmisfix — see the header on `dlsym`.
|
||||
MISFixNote("detour: %s at %p in %s -> %p, trampoline %p (%u words)",
|
||||
label,
|
||||
(const void *)target,
|
||||
MISFixCallerImage(target),
|
||||
replacement,
|
||||
trampoline,
|
||||
words);
|
||||
return MISFixDetourOK;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// `__DATA,__interpose` rewrites the places that *call* a symbol, in the images
|
||||
// dyld links. It never reaches a call made from one shared-cache image to
|
||||
// another, which is measured in MISFixDeviceIdentity.c and is the reason
|
||||
// libmisfix cannot touch installd: `MobileInstallation → libmis →
|
||||
// another, which is measured in MISFixDeviceIdentity.c and is the reason an
|
||||
// interpose cannot touch installd: `MobileInstallation → libmis →
|
||||
// libMobileGestalt` happens entirely inside the cache.
|
||||
//
|
||||
// A detour rewrites the *callee*. The first four instructions of the target
|
||||
@@ -15,14 +15,26 @@
|
||||
//
|
||||
// - The target's page must be made writable. The cache is mapped
|
||||
// read-execute and shared with every process, so the only way in is
|
||||
// copy-on-write, and whether this guest's kernel permits that is a
|
||||
// property of its codesigning patches. `MISFixCacheWriteProbe.c` measures
|
||||
// it; this file reports failure rather than assuming.
|
||||
// copy-on-write. Measured working on test-26.4 (2026-09-30): the page
|
||||
// splits into a private copy and the write lands. Nothing on disk changes
|
||||
// and no other process sees it, which is the difference between this and
|
||||
// the cache patch that left a 27.0 guest unable to boot (issue #532).
|
||||
// - The displaced instructions must survive being moved. `adr` and `adrp`
|
||||
// are rewritten to materialise the same absolute address, and an
|
||||
// unconditional `b` becomes an absolute jump. Anything else PC-relative —
|
||||
// `bl`, `b.cond`, `cbz`, `tbz`, a literal load — is **refused**, because a
|
||||
// wrong relocation is a corrupted daemon and a refusal is a log line.
|
||||
// - The target must be at least four instructions long. A function that
|
||||
// returns or jumps away sooner is *shorter* than the patch, so writing it
|
||||
// would scribble on whoever follows. A terminator in the first three words
|
||||
// is refused for that reason.
|
||||
//
|
||||
// The target is given as an address, never as a name, and that is not a
|
||||
// stylistic choice. dyld applies interposing to `dlsym` — measured on
|
||||
// test-26.4 even for a lookup scoped to a handle on the owning image, which
|
||||
// came back inside libmisfix.dylib. What dyld does not interpose is the
|
||||
// interposing image's own imports, so the way to name a function here is to
|
||||
// declare it, call `&` on it from this dylib, and let the linker bind it.
|
||||
//
|
||||
// Install detours from a constructor. Four words cannot be replaced atomically,
|
||||
// so a thread already executing the target's prologue is a hazard; at image
|
||||
@@ -35,16 +47,13 @@
|
||||
/// Why a detour was not installed. `MISFixDetourOK` is zero.
|
||||
typedef enum {
|
||||
MISFixDetourOK = 0,
|
||||
/// The image is not mapped in this process.
|
||||
MISFixDetourImageMissing,
|
||||
/// The image is mapped but exports no such symbol.
|
||||
MISFixDetourSymbolMissing,
|
||||
/// The symbol resolved into libmisfix itself. Refused: dyld applies
|
||||
/// interposing to `dlsym`, so a hooked symbol can resolve to our own
|
||||
/// replacement and a detour would point at itself.
|
||||
MISFixDetourSymbolIsOurs,
|
||||
/// The target address is NULL — the symbol did not bind.
|
||||
MISFixDetourNoTarget,
|
||||
/// A displaced instruction is PC-relative in a way this does not rewrite.
|
||||
MISFixDetourUnrelocatable,
|
||||
/// The target returns or jumps away inside the four words the jump needs,
|
||||
/// so it is too short to detour.
|
||||
MISFixDetourTooShort,
|
||||
/// No executable memory could be obtained for the trampoline.
|
||||
MISFixDetourNoTrampoline,
|
||||
/// The target's page could not be made writable.
|
||||
@@ -56,18 +65,17 @@ typedef enum {
|
||||
/// A sentence for the log, never NULL.
|
||||
const char *MISFixDetourDescribe(MISFixDetourResult result);
|
||||
|
||||
/// Point `symbol` of `image` at `replacement`.
|
||||
/// Point `target` at `replacement`.
|
||||
///
|
||||
/// On success `*original` receives a pointer that behaves as the untouched
|
||||
/// function did, already signed for an arm64e indirect call, and the
|
||||
/// replacement calls through it for everything it does not mean to change.
|
||||
/// On failure nothing is written and `*original` is left alone.
|
||||
///
|
||||
/// `image` is an install name, resolved with `RTLD_NOLOAD`: a detour is only
|
||||
/// meaningful for an image this process already has.
|
||||
/// `label` names the target in the log and is not otherwise used.
|
||||
MISFixDetourResult MISFixDetour(
|
||||
const char *image,
|
||||
const char *symbol,
|
||||
const char *label,
|
||||
void *target,
|
||||
void *replacement,
|
||||
void **original
|
||||
);
|
||||
|
||||
@@ -45,14 +45,14 @@
|
||||
//
|
||||
// ## How far this reaches, measured
|
||||
//
|
||||
// misagent, and nothing else that matters. Its main executable calls
|
||||
// `MGCopyAnswer` itself, so the interpose catches it and a profile naming the
|
||||
// configured device installs.
|
||||
// misagent, and nothing else. Its main executable calls `MGCopyAnswer` itself,
|
||||
// so the interpose catches it and a profile naming the configured device
|
||||
// installs.
|
||||
//
|
||||
// installd does not benefit, and no version of this dylib can make it. Its
|
||||
// profile check runs MobileInstallation → libmis → libMobileGestalt, all three
|
||||
// inside the dyld shared cache, and an interpose rewrites call sites in the
|
||||
// images dyld links — not the cache's own. Measured on test-26.4 (2026-09-30,
|
||||
// installd does not benefit and no interpose can make it. Its profile check
|
||||
// runs MobileInstallation → libmis → libMobileGestalt, all three inside the
|
||||
// dyld shared cache, and an interpose rewrites call sites in the images dyld
|
||||
// links — not the cache's own. Measured on test-26.4 (2026-09-30,
|
||||
// `libmisfix[726]`): one `devicectl device install app`, `LogQueries` on, and
|
||||
// the only line from installd is `MGCopyAnswer(BuildVersion) from installd`.
|
||||
// No `UniqueDeviceID` query, although libmis plainly resolved one — it skipped
|
||||
@@ -65,12 +65,20 @@
|
||||
// trusted`. libmis's other route to a UDID is closed too:
|
||||
// `amfi_interface_query_bootarg_state returned error Function not implemented`.
|
||||
//
|
||||
// So an Xcode or `devicectl` install still needs the guest's *own* UDID to be
|
||||
// in the profile. Two things could give it that, and neither belongs in this
|
||||
// file: a shared-cache patch on libmis, or creating the VM with the ECID of a
|
||||
// device the team has already registered — a modern UDID is
|
||||
// `<chip-id>-<ECID>`, and the ECID is chosen at `vm create`, so that one needs
|
||||
// no hook and tells no lie.
|
||||
// ## Why this is now a convenience rather than the fix
|
||||
//
|
||||
// Making a profile install was one way to get an Xcode install through. It is
|
||||
// no longer the way this project takes: MISFixSignature.c validates the bundle
|
||||
// on its own signature with `ValidatedByProfile = 0`, and
|
||||
// MISFixProfilePolicy.c lets the embedded profile fail to install without
|
||||
// failing the install. An arbitrary IPA then goes in with no UDID configured
|
||||
// at all, which is the point — pinning a VM to a borrowed UDID only ever
|
||||
// worked for a team whose registered devices you already have.
|
||||
//
|
||||
// The override is kept because it is harmless, already shipped, and reachable
|
||||
// from the VM window's Device ▸ Set UDID…. Setting it makes profiles install
|
||||
// for real instead of being skipped, which is closer to what the device would
|
||||
// have done.
|
||||
//
|
||||
// ## The inconsistency this creates, stated plainly
|
||||
//
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// MISFixInstallPolicy.c — the two places installd refuses an app for a reason
|
||||
// that does not apply to this guest.
|
||||
//
|
||||
// MISFixSignature.c widens what MIS itself will accept. This file is what sits
|
||||
// above MIS: MobileInstallation's own policy, which asks for things a research
|
||||
// VM cannot have and then treats their absence as a failed install.
|
||||
//
|
||||
// Both hooks call the real implementation first and only override a refusal,
|
||||
// so on anything that would have installed anyway the behaviour is unchanged.
|
||||
//
|
||||
// ## The embedded profile
|
||||
//
|
||||
// Failed to install embedded profile for plus.yellow.AirBuild : 0xE8008012
|
||||
// (This provisioning profile cannot be installed on this device.)
|
||||
// -[MIInstallableBundle _installEmbeddedProfilesWithError:]
|
||||
//
|
||||
// `0xE8008012` is correct and always will be. A profile names the devices it
|
||||
// covers, in `ProvisionedDevices`, and a VM's UDID is in nobody's list. Xcode
|
||||
// papers over that for a *free* personal team by registering whatever device
|
||||
// is plugged in; for a paid team there is no auto-registration, and the VM
|
||||
// would have to be added to the account by hand and again after every
|
||||
// `vm create`.
|
||||
//
|
||||
// A profile does two things — it vouches that a signing identity may run on
|
||||
// this device, and it carries the entitlements the app may claim — and neither
|
||||
// is load-bearing here. The kernel patches admit the code whatever signed it,
|
||||
// and MIS has already validated the bundle on its own signature with
|
||||
// `ValidatedByProfile = 0`. So a profile that cannot install is noted and
|
||||
// skipped; one that can install still does, unchanged.
|
||||
//
|
||||
// ## The signer identity
|
||||
//
|
||||
// Failed to extract signer identity from <MIExecutableBundle …>
|
||||
// -[MICodeSigningVerifier performValidationWithError:] line 424
|
||||
//
|
||||
// This is the gate behind the gate, and it is why widening MIS's options is
|
||||
// not by itself enough. MIS accepts an ad-hoc signature and fills its info
|
||||
// dictionary — `CdHash`, `Entitlements`, `SigningID` — but MobileInstallation
|
||||
// then wants a *signer*: the leaf certificate out of a CMS blob, which an
|
||||
// ad-hoc signature does not have and never will, because the whole point of
|
||||
// ad-hoc is that nobody signed it.
|
||||
//
|
||||
// Measured on test-26.4 (2026-09-30) with a `codesign --sign -` bundle:
|
||||
// `MISValidateSignature(…/SignTest.app) -> 0x0`, and the install still failed,
|
||||
// here, at line 424 with `LibMISErrorNumber = -402620415`.
|
||||
//
|
||||
// There is nothing to widen and nothing to supply. The decision itself is what
|
||||
// has to change, and this is the decision the guest is entitled to make
|
||||
// differently: it runs unsigned code on purpose. So validation is allowed to
|
||||
// fail and the install proceeds. Everything the verifier *could* determine has
|
||||
// already been determined by the time it gets to the signer — the real
|
||||
// implementation runs first, and fails late.
|
||||
//
|
||||
// ## Why a swizzle and not a detour
|
||||
//
|
||||
// Both are Objective-C methods in MobileInstallation, and an Objective-C
|
||||
// method list is *data*. Replacing an implementation through the runtime
|
||||
// reaches every caller, in the shared cache or out of it, without making a
|
||||
// single page of cache text writable. Where that is available it is strictly
|
||||
// better than MISFixDetour.h, and here it is available.
|
||||
//
|
||||
// The classes are looked up rather than linked, and a version that does not
|
||||
// have one leaves that hook inert with a line in the log. That is deliberate:
|
||||
// these are private methods on private classes, and the guest is expected to
|
||||
// be a version this project has not seen yet.
|
||||
|
||||
#include "MISFixConfig.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <objc/objc.h>
|
||||
#include <objc/runtime.h>
|
||||
|
||||
/// MobileInstallation's install name, for the case where a class is not
|
||||
/// registered yet. Our constructor runs among the inserted libraries, ahead of
|
||||
/// most of the process; every image present at launch has had its classes
|
||||
/// realised by then, but a framework installd only dlopens later would not be
|
||||
/// there at all.
|
||||
#define kMISFixMobileInstallationPath \
|
||||
"/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation"
|
||||
|
||||
/// The shape both hooks have: a `BOOL`-returning method whose only argument is
|
||||
/// an `NSError **` out-parameter.
|
||||
typedef BOOL (*MISFixCheckIMP)(id self, SEL selector, void *error);
|
||||
|
||||
/// Replace `class`'s `-selector` with `replacement`, keeping the original.
|
||||
///
|
||||
/// Returns zero and logs when there is no such class or method, which is the
|
||||
/// expected outcome on an OS version that renamed one.
|
||||
static int vpSwizzle(
|
||||
const char *className,
|
||||
const char *selectorName,
|
||||
MISFixCheckIMP replacement,
|
||||
MISFixCheckIMP *original
|
||||
) {
|
||||
Class found = objc_getClass(className);
|
||||
if (found == NULL) {
|
||||
if (dlopen(kMISFixMobileInstallationPath, RTLD_LAZY) != NULL)
|
||||
found = objc_getClass(className);
|
||||
}
|
||||
if (found == NULL) {
|
||||
MISFixNote("%s is not in this process", className);
|
||||
return 0;
|
||||
}
|
||||
Method method = class_getInstanceMethod(found, sel_registerName(selectorName));
|
||||
if (method == NULL) {
|
||||
MISFixNote("%s has no -%s", className, selectorName);
|
||||
return 0;
|
||||
}
|
||||
*original = (MISFixCheckIMP)method_setImplementation(method, (IMP)replacement);
|
||||
MISFixNote("swizzled -[%s %s]", className, selectorName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// Clear an `NSError **` the failing implementation wrote.
|
||||
///
|
||||
/// A caller handed `YES` alongside a populated `NSError *` is a shape no
|
||||
/// ordinary method produces, and installd does read the out-parameter. The
|
||||
/// error object itself is left to the autorelease pool it came from.
|
||||
static void vpClearError(void *error) {
|
||||
if (error != NULL)
|
||||
*(void **)error = NULL;
|
||||
}
|
||||
|
||||
// MARK: - The embedded profile
|
||||
|
||||
static MISFixCheckIMP vpOriginalInstallProfiles;
|
||||
|
||||
static BOOL vpInstallEmbeddedProfiles(id self, SEL selector, void *error) {
|
||||
if (vpOriginalInstallProfiles(self, selector, error))
|
||||
return YES;
|
||||
vpClearError(error);
|
||||
MISFixNote("embedded profile refused; installing without one");
|
||||
return YES;
|
||||
}
|
||||
|
||||
// MARK: - The signer identity
|
||||
|
||||
static MISFixCheckIMP vpOriginalPerformValidation;
|
||||
|
||||
static BOOL vpPerformValidation(id self, SEL selector, void *error) {
|
||||
if (vpOriginalPerformValidation(self, selector, error))
|
||||
return YES;
|
||||
vpClearError(error);
|
||||
MISFixNote("code-signing validation refused; installing anyway");
|
||||
return YES;
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void vpInstallPolicyHooks(void) {
|
||||
vpSwizzle(
|
||||
"MIInstallableBundle",
|
||||
"_installEmbeddedProfilesWithError:",
|
||||
&vpInstallEmbeddedProfiles,
|
||||
&vpOriginalInstallProfiles
|
||||
);
|
||||
vpSwizzle(
|
||||
"MICodeSigningVerifier",
|
||||
"performValidationWithError:",
|
||||
&vpPerformValidation,
|
||||
&vpOriginalPerformValidation
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// MISFixProfileScope.c — let every provisioning profile cover this device.
|
||||
//
|
||||
// A profile carries the list of devices it is good for, and misagent reads it
|
||||
// itself rather than asking anyone:
|
||||
//
|
||||
// ProvisionedDevices ProvisionsAllDevices
|
||||
//
|
||||
// — its own strings, in the order the code uses them. It asks the profile for
|
||||
// `ProvisionsAllDevices` first, and only when that is false does it compare
|
||||
// this device's UDID against `ProvisionedDevices`. A VM's UDID is in nobody's
|
||||
// list, so the comparison always loses and the profile is refused with
|
||||
// `0xE8008012`.
|
||||
//
|
||||
// Both questions are asked through `MISProfileGetValue`, which is libmis's and
|
||||
// so is a callee this dylib can replace. Answering the first one `true` means
|
||||
// the second is never asked, and a profile that reaches this guest installs.
|
||||
//
|
||||
// ## Why this rather than a borrowed UDID
|
||||
//
|
||||
// MISFixDeviceIdentity.c answers `UniqueDeviceID` with a device someone has
|
||||
// already registered, which makes the comparison succeed for one team's
|
||||
// profiles. It works, and it needs a registered device to borrow, a UDID
|
||||
// typed in per machine, and it is still wrong for every other team. This says
|
||||
// the same thing once, for every profile, and needs no configuration.
|
||||
//
|
||||
// ## What it buys
|
||||
//
|
||||
// The install path stops needing anything faked. With the profile installed
|
||||
// for real, libmis validates the app against it — genuine signer, genuine
|
||||
// entitlements, `ValidatedByProfile = 1` — instead of being talked past in
|
||||
// MISFixInstallPolicy.c. The profile's own signature, its expiry and its
|
||||
// application-identifier are all still checked; the only claim widened is
|
||||
// which devices it covers.
|
||||
//
|
||||
// ## What it does not touch
|
||||
//
|
||||
// Nothing about a profile is rewritten. `MISProfileGetValue` is asked a
|
||||
// question and answered; the profile on disk, its signature and every other
|
||||
// value it carries are exactly as Apple issued them.
|
||||
|
||||
#include "MISFixConfig.h"
|
||||
#include "MISFixDetour.h"
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
/// The key whose answer decides whether the device list is consulted at all.
|
||||
/// A plain string for the same reason as the MIS option keys: libmis exports
|
||||
/// no symbol for it and the SDK declares none.
|
||||
#define kMISProfileProvisionsAllDevices CFSTR("ProvisionsAllDevices")
|
||||
|
||||
typedef CFTypeRef (*MISProfileGet)(CFTypeRef profile, CFStringRef key);
|
||||
|
||||
extern CFTypeRef MISProfileGetValue(CFTypeRef profile, CFStringRef key);
|
||||
|
||||
static MISProfileGet vpOriginalProfileGetValue;
|
||||
|
||||
static CFTypeRef vpProfileGetValue(CFTypeRef profile, CFStringRef key) {
|
||||
if (key != NULL && CFGetTypeID(key) == CFStringGetTypeID()
|
||||
&& CFEqual(key, kMISProfileProvisionsAllDevices))
|
||||
{
|
||||
MISFixLog("MISProfileGetValue(ProvisionsAllDevices) -> true");
|
||||
// Immortal, and the real function returns a borrowed value too, so the
|
||||
// caller's lifetime expectations are unchanged.
|
||||
return kCFBooleanTrue;
|
||||
}
|
||||
return vpOriginalProfileGetValue(profile, key);
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void vpInstallProfileScopeHook(void) {
|
||||
MISFixDetourResult result = MISFixDetour(
|
||||
"MISProfileGetValue",
|
||||
(void *)&MISProfileGetValue,
|
||||
(void *)&vpProfileGetValue,
|
||||
(void **)&vpOriginalProfileGetValue
|
||||
);
|
||||
if (result != MISFixDetourOK)
|
||||
MISFixNote("MISProfileGetValue not hooked: %s", MISFixDetourDescribe(result));
|
||||
}
|
||||
@@ -1,35 +1,5 @@
|
||||
// MISFixSignature.c — widen MIS's idea of an acceptable signature.
|
||||
//
|
||||
// ## Measured 2026-09-30: this never runs in installd, and cannot
|
||||
//
|
||||
// Read this first, because the rest of the file was written believing
|
||||
// otherwise. A `__DATA,__interpose` replacement is applied to *call sites*, and
|
||||
// every call site that matters here is inside the dyld shared cache:
|
||||
//
|
||||
// MobileInstallation.framework → libmis.dylib (cache to cache)
|
||||
// libmis.dylib → libMobileGestalt (cache to cache)
|
||||
//
|
||||
// Neither is rewritten, whether this dylib arrives as a weak dependency of the
|
||||
// main executable or ahead of everything through `DYLD_INSERT_LIBRARIES`. On
|
||||
// test-26.4, with `LogQueries` on and the log for `MISValidateSignatureAndCopyInfo`
|
||||
// made unconditional, a whole `devicectl device install app` produced exactly
|
||||
// one line from installd:
|
||||
//
|
||||
// libmisfix[726]: MGCopyAnswer(BuildVersion) from installd passed through
|
||||
//
|
||||
// `from installd` is the point: the one call this hook catches is the one the
|
||||
// main executable makes itself. `+[MICodeSigningVerifier
|
||||
// _validateSignatureAndCopyInfoForURL:withOptions:error:]` ran to its line 80
|
||||
// and failed, and no line here records it.
|
||||
//
|
||||
// So the options are never widened in installd. What that daemon actually
|
||||
// refuses, and why, is in MISFixDeviceIdentity.c; fixing it means changing the
|
||||
// shared cache, not this dylib. misagent is different — its main executable
|
||||
// calls `MGCopyAnswer` itself — and the UDID override there does work.
|
||||
//
|
||||
// The hook is kept because it costs nothing and is correct where it is
|
||||
// reached, and because it is the control that measured all of this.
|
||||
//
|
||||
// A guest restored by this project runs unsigned code happily: the kernel
|
||||
// patches (`amfi_trustcache`, `jb.post_validation`, `jb.amfi_execve`) admit it,
|
||||
// lsd registers it, and SpringBoard launches it. An app pushed in through
|
||||
@@ -68,9 +38,41 @@
|
||||
// synthesise a reply. That matters: installd reads those keys, and a hook that
|
||||
// faked success without them would break the install further down.
|
||||
//
|
||||
// So the hook adds one key to the options and calls through. On anything MIS
|
||||
// So the hook adds keys to the options and calls through. On anything MIS
|
||||
// would have accepted anyway the behaviour is bit for bit unchanged, because
|
||||
// the key only widens what counts as an acceptable signature.
|
||||
// the keys only widen what counts as acceptable.
|
||||
//
|
||||
// ## Why this is a detour and not an interpose
|
||||
//
|
||||
// It used to be an interpose, and that was measured wrong on 2026-09-30. A
|
||||
// `__DATA,__interpose` replacement is applied to *call sites*, and every call
|
||||
// site that matters here is inside the dyld shared cache:
|
||||
//
|
||||
// MobileInstallation.framework → libmis.dylib (cache to cache)
|
||||
//
|
||||
// Neither dyld's linking of this dylib as a weak dependency nor
|
||||
// `DYLD_INSERT_LIBRARIES` rewrites that. With `LogQueries` on and this file's
|
||||
// log made unconditional, a whole `devicectl device install app` produced not
|
||||
// one line from installd, while `MICodeSigningVerifier` ran to its line 80 and
|
||||
// failed. The options were never widened, in any install, ever.
|
||||
//
|
||||
// A detour rewrites the callee instead, so there is nothing to miss: one copy
|
||||
// of the function, one jump at its top, every caller redirected. See
|
||||
// MISFixDetour.h for what that costs and what it refuses.
|
||||
//
|
||||
// Two details of the target, both of which the detour has to respect.
|
||||
//
|
||||
// `MISValidateSignatureAndCopyInfo` is a short thunk in front of
|
||||
// `…WithProgress`, where libmis's body actually lives, so it is *shorter than
|
||||
// the four-word jump* and `MISFixDetour` declines it with
|
||||
// `MISFixDetourTooShort`. That is the expected outcome, not a failure: the
|
||||
// thunk branches into the function that is hooked, so its callers are covered
|
||||
// anyway. Both are attempted so the log says which one took.
|
||||
//
|
||||
// The address cannot come from `dlsym`. dyld applies interposing to it, so a
|
||||
// hooked symbol resolves to our own replacement — measured even through a
|
||||
// handle on libmis itself. Taking `&MISValidateSignatureAndCopyInfoWithProgress`
|
||||
// here uses this image's own import, which dyld leaves alone.
|
||||
//
|
||||
// ## What this deliberately does not do
|
||||
//
|
||||
@@ -80,34 +82,35 @@
|
||||
// supplies one. Forging a reply for a bundle with no signature at all would
|
||||
// mean inventing a cdhash the kernel never agreed to.
|
||||
//
|
||||
// The online-authorization gate is a different patch: `mis_trust_auth` covers
|
||||
// a profile that wants network validation on a hacktivated guest. This one is
|
||||
// only about the signature's shape.
|
||||
//
|
||||
// ## Mechanism
|
||||
//
|
||||
// See `MISFixInterpose.h`. SystemHook puts this dylib in
|
||||
// `DYLD_INSERT_LIBRARIES` for the processes it recognises by path, so it is
|
||||
// loaded ahead of everything — which is the strongest position an interpose
|
||||
// can be in, and still not enough to reach the cache-internal call sites
|
||||
// above.
|
||||
// The profile half of an Xcode install is not here either; it is
|
||||
// MISFixProfilePolicy.c.
|
||||
|
||||
#include "MISFixConfig.h"
|
||||
#include "MISFixInterpose.h"
|
||||
#include "MISFixDetour.h"
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// libmis's own option keys, taken from the cache's string table rather than
|
||||
// from a header — libmis.tbd exports the `kMISValidationOption*` symbols but
|
||||
// the SDK declares none of them.
|
||||
#define kMISValidationOptionAllowAdHocSigning CFSTR("AllowAdHocSigning")
|
||||
#define kMISValidationOptionRespectUppTrustAndAuthorization CFSTR("RespectUppTrustAndAuthorization")
|
||||
#define kMISValidationOptionSkipProfileIdentifierPolicy CFSTR("SkipProfileIdentifierPolicy")
|
||||
|
||||
// The first argument is a path string, not a URL. Handing MIS an NSURL aborts
|
||||
// the process inside libmis with `-[NSURL length]: unrecognized selector`,
|
||||
// which is how this was pinned down.
|
||||
typedef CFStringRef MISPath;
|
||||
|
||||
typedef int (*MISValidate)(MISPath path, CFDictionaryRef options, CFDictionaryRef *info);
|
||||
typedef int (*MISValidateWithProgress)(
|
||||
MISPath path,
|
||||
CFDictionaryRef options,
|
||||
CFDictionaryRef *info,
|
||||
void *progress
|
||||
);
|
||||
|
||||
extern int MISValidateSignatureAndCopyInfo(MISPath path, CFDictionaryRef options, CFDictionaryRef *info);
|
||||
extern int MISValidateSignatureAndCopyInfoWithProgress(
|
||||
MISPath path,
|
||||
@@ -116,13 +119,23 @@ extern int MISValidateSignatureAndCopyInfoWithProgress(
|
||||
void *progress
|
||||
);
|
||||
|
||||
static MISValidate vpOriginalValidate;
|
||||
static MISValidateWithProgress vpOriginalValidateWithProgress;
|
||||
|
||||
/// The caller's options, widened. Never returns NULL for a NULL input: MIS is
|
||||
/// called with an options dictionary either way.
|
||||
///
|
||||
/// Two keys go in.
|
||||
/// Three keys go in.
|
||||
///
|
||||
/// `AllowAdHocSigning` is the signature half described above.
|
||||
///
|
||||
/// `SkipProfileIdentifierPolicy` stops MIS insisting that a profile's
|
||||
/// application-identifier match the bundle's. A profile that names a different
|
||||
/// app — or an app whose profile never installed, which is the ordinary case
|
||||
/// for an IPA built for someone else's team — is then not a reason to refuse a
|
||||
/// signature that is otherwise fine. Measured to leave an accepted bundle
|
||||
/// accepted.
|
||||
///
|
||||
/// `RespectUppTrustAndAuthorization = false` is the online-authorization half,
|
||||
/// and it replaces a patch that used to edit the shared cache. libmis reaches
|
||||
/// `checkTrustAndAuthorization` — the only producer of `0xE8008026`, "missing
|
||||
@@ -141,7 +154,7 @@ extern int MISValidateSignatureAndCopyInfoWithProgress(
|
||||
///
|
||||
/// The option parser writes a flag's slot only when the key is present, so an
|
||||
/// explicit value always beats the defaults `UnauthoritativeLaunch` installs —
|
||||
/// and nothing else in the shared cache passes this key, so there is no
|
||||
/// and nothing else in the shared cache passes these keys, so there is no
|
||||
/// caller's own value to override.
|
||||
static CFDictionaryRef vpWidenedOptions(CFDictionaryRef options) {
|
||||
CFMutableDictionaryRef widened =
|
||||
@@ -149,13 +162,18 @@ static CFDictionaryRef vpWidenedOptions(CFDictionaryRef options) {
|
||||
? CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, options)
|
||||
: CFDictionaryCreateMutable(
|
||||
kCFAllocatorDefault,
|
||||
2,
|
||||
3,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks
|
||||
);
|
||||
if (widened == NULL)
|
||||
return NULL;
|
||||
CFDictionarySetValue(widened, kMISValidationOptionAllowAdHocSigning, kCFBooleanTrue);
|
||||
CFDictionarySetValue(
|
||||
widened,
|
||||
kMISValidationOptionSkipProfileIdentifierPolicy,
|
||||
kCFBooleanTrue
|
||||
);
|
||||
CFDictionarySetValue(
|
||||
widened,
|
||||
kMISValidationOptionRespectUppTrustAndAuthorization,
|
||||
@@ -166,77 +184,144 @@ static CFDictionaryRef vpWidenedOptions(CFDictionaryRef options) {
|
||||
|
||||
/// Log one validation, under `LogQueries`.
|
||||
///
|
||||
/// This is the other half of the control. `MISValidateSignatureAndCopyInfo` is
|
||||
/// reached the same way the UDID query is — from MobileInstallation, in the
|
||||
/// shared cache, into libmis, also in the shared cache, with the main
|
||||
/// executable's own image not involved. So if this line appears in installd
|
||||
/// and the `MGCopyAnswer` line does not, the two calls are being treated
|
||||
/// differently and the difference is in MobileGestalt, not in whether an
|
||||
/// interpose can cross the cache at all.
|
||||
/// Never returns early. A first run logged nothing here from installd, which
|
||||
/// was read as "the interpose was not reached" — but a `path` this could not
|
||||
/// turn into a C string would have produced exactly the same silence. The line
|
||||
/// is unconditional now, and says what the argument was when it is not a
|
||||
/// string, so an absent line means one thing only.
|
||||
static void vpLogValidation(MISPath path, int result, const char *caller) {
|
||||
/// was read as "the hook was not reached" — but a `path` this could not turn
|
||||
/// into a C string would have produced exactly the same silence. The line says
|
||||
/// what the argument was when it is not a string, so an absent line means one
|
||||
/// thing only.
|
||||
static void vpLogValidation(MISPath path, int result) {
|
||||
char buffer[1024];
|
||||
if (path == NULL) {
|
||||
MISFixLog("MISValidateSignatureAndCopyInfo(NULL) from %s -> 0x%x", caller, (unsigned)result);
|
||||
MISFixLog("MISValidateSignature(NULL) -> 0x%x", (unsigned)result);
|
||||
return;
|
||||
}
|
||||
if (CFGetTypeID(path) != CFStringGetTypeID()
|
||||
|| !CFStringGetCString(path, buffer, sizeof(buffer), kCFStringEncodingUTF8))
|
||||
{
|
||||
MISFixLog(
|
||||
"MISValidateSignatureAndCopyInfo(<non-string %lu>) from %s -> 0x%x",
|
||||
"MISValidateSignature(<non-string %lu>) -> 0x%x",
|
||||
(unsigned long)CFGetTypeID(path),
|
||||
caller,
|
||||
(unsigned)result
|
||||
);
|
||||
return;
|
||||
}
|
||||
MISFixLog(
|
||||
"MISValidateSignatureAndCopyInfo(%s) from %s -> 0x%x",
|
||||
buffer,
|
||||
caller,
|
||||
(unsigned)result
|
||||
);
|
||||
MISFixLog("MISValidateSignature(%s) -> 0x%x", buffer, (unsigned)result);
|
||||
}
|
||||
|
||||
static int vpMISValidateSignatureAndCopyInfo(
|
||||
MISPath path,
|
||||
CFDictionaryRef options,
|
||||
CFDictionaryRef *info
|
||||
) {
|
||||
const char *caller = MISFixCaller();
|
||||
/// One line naming what MIS put in the info dictionary, under `LogQueries`.
|
||||
///
|
||||
/// This is the instrument for the gates *above* MIS. `MICodeSigningVerifier`
|
||||
/// accepts MIS's answer and then wants more from it — a signer identity, an
|
||||
/// identifier that matches the bundle — and which key it is reading is not
|
||||
/// visible from the failure it reports. Naming the keys, and the short values,
|
||||
/// is what turns that into a readable question.
|
||||
static void vpLogInfo(CFDictionaryRef info) {
|
||||
if (info == NULL || CFGetTypeID(info) != CFDictionaryGetTypeID())
|
||||
return;
|
||||
CFIndex count = CFDictionaryGetCount(info);
|
||||
if (count <= 0) {
|
||||
MISFixLog(" info: empty");
|
||||
return;
|
||||
}
|
||||
const void **keys = calloc((size_t)count, sizeof(void *));
|
||||
const void **values = calloc((size_t)count, sizeof(void *));
|
||||
if (keys == NULL || values == NULL) {
|
||||
free(keys);
|
||||
free(values);
|
||||
return;
|
||||
}
|
||||
CFDictionaryGetKeysAndValues(info, keys, values);
|
||||
for (CFIndex index = 0; index < count; index += 1) {
|
||||
CFStringRef key = (CFStringRef)keys[index];
|
||||
char name[128];
|
||||
if (key == NULL || CFGetTypeID(key) != CFStringGetTypeID()
|
||||
|| !CFStringGetCString(key, name, sizeof(name), kCFStringEncodingUTF8))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
CFTypeRef value = values[index];
|
||||
CFTypeID kind = value != NULL ? CFGetTypeID(value) : 0;
|
||||
char shown[160] = "<…>";
|
||||
if (value == NULL) {
|
||||
snprintf(shown, sizeof(shown), "<null>");
|
||||
} else if (kind == CFStringGetTypeID()) {
|
||||
CFStringGetCString((CFStringRef)value, shown, sizeof(shown), kCFStringEncodingUTF8);
|
||||
} else if (kind == CFBooleanGetTypeID()) {
|
||||
snprintf(shown, sizeof(shown), CFBooleanGetValue((CFBooleanRef)value) ? "true" : "false");
|
||||
} else if (kind == CFNumberGetTypeID()) {
|
||||
long long number = 0;
|
||||
CFNumberGetValue((CFNumberRef)value, kCFNumberLongLongType, &number);
|
||||
snprintf(shown, sizeof(shown), "%lld", number);
|
||||
} else if (kind == CFDataGetTypeID()) {
|
||||
snprintf(shown, sizeof(shown), "<%ld bytes>",
|
||||
(long)CFDataGetLength((CFDataRef)value));
|
||||
} else if (kind == CFDictionaryGetTypeID()) {
|
||||
snprintf(shown, sizeof(shown), "<%ld entries>",
|
||||
(long)CFDictionaryGetCount((CFDictionaryRef)value));
|
||||
}
|
||||
MISFixLog(" info[%s] = %s", name, shown);
|
||||
}
|
||||
free(keys);
|
||||
free(values);
|
||||
}
|
||||
|
||||
static int vpValidate(MISPath path, CFDictionaryRef options, CFDictionaryRef *info) {
|
||||
CFDictionaryRef widened = vpWidenedOptions(options);
|
||||
// Out of memory: pass the caller's own options through rather than fail.
|
||||
if (widened == NULL)
|
||||
return MISValidateSignatureAndCopyInfo(path, options, info);
|
||||
int result = MISValidateSignatureAndCopyInfo(path, widened, info);
|
||||
return vpOriginalValidate(path, options, info);
|
||||
int result = vpOriginalValidate(path, widened, info);
|
||||
CFRelease(widened);
|
||||
vpLogValidation(path, result, caller);
|
||||
vpLogValidation(path, result);
|
||||
if (result == 0 && info != NULL)
|
||||
vpLogInfo(*info);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int vpMISValidateSignatureAndCopyInfoWithProgress(
|
||||
static int vpValidateWithProgress(
|
||||
MISPath path,
|
||||
CFDictionaryRef options,
|
||||
CFDictionaryRef *info,
|
||||
void *progress
|
||||
) {
|
||||
const char *caller = MISFixCaller();
|
||||
CFDictionaryRef widened = vpWidenedOptions(options);
|
||||
if (widened == NULL)
|
||||
return MISValidateSignatureAndCopyInfoWithProgress(path, options, info, progress);
|
||||
int result = MISValidateSignatureAndCopyInfoWithProgress(path, widened, info, progress);
|
||||
return vpOriginalValidateWithProgress(path, options, info, progress);
|
||||
int result = vpOriginalValidateWithProgress(path, widened, info, progress);
|
||||
CFRelease(widened);
|
||||
vpLogValidation(path, result, caller);
|
||||
vpLogValidation(path, result);
|
||||
if (result == 0 && info != NULL)
|
||||
vpLogInfo(*info);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Both entry points are replaced. The plain one is what MobileInstallation
|
||||
// calls; the progress variant is where libmis's own body lives, and a future
|
||||
// caller that reaches for it directly gets the same treatment.
|
||||
MISFIX_INTERPOSE(vpMISValidateSignatureAndCopyInfo, MISValidateSignatureAndCopyInfo);
|
||||
MISFIX_INTERPOSE(vpMISValidateSignatureAndCopyInfoWithProgress, MISValidateSignatureAndCopyInfoWithProgress);
|
||||
/// Hook both entry points before the daemon serves anything.
|
||||
///
|
||||
/// The `…WithProgress` one is the body and is the one that has to take. The
|
||||
/// plain one is a thunk in front of it and is expected to come back
|
||||
/// `MISFixDetourTooShort`; it is attempted anyway, because "expected" is a
|
||||
/// property of one libmis build and the log is how the next one tells us it
|
||||
/// changed.
|
||||
__attribute__((constructor)) static void vpInstallSignatureHooks(void) {
|
||||
MISFixDetourResult body = MISFixDetour(
|
||||
"MISValidateSignatureAndCopyInfoWithProgress",
|
||||
(void *)&MISValidateSignatureAndCopyInfoWithProgress,
|
||||
(void *)&vpValidateWithProgress,
|
||||
(void **)&vpOriginalValidateWithProgress
|
||||
);
|
||||
if (body != MISFixDetourOK) {
|
||||
MISFixNote("MISValidateSignatureAndCopyInfoWithProgress not hooked: %s",
|
||||
MISFixDetourDescribe(body));
|
||||
}
|
||||
|
||||
MISFixDetourResult thunk = MISFixDetour(
|
||||
"MISValidateSignatureAndCopyInfo",
|
||||
(void *)&MISValidateSignatureAndCopyInfo,
|
||||
(void *)&vpValidate,
|
||||
(void **)&vpOriginalValidate
|
||||
);
|
||||
if (thunk != MISFixDetourOK) {
|
||||
MISFixLog("MISValidateSignatureAndCopyInfo not hooked: %s",
|
||||
MISFixDetourDescribe(thunk));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ $(MISFIX): $(MISFIX_SOURCES) $(MISFIX_INCLUDES) MISFix | $(STAGE)/misfix
|
||||
$(CLANG) $(GUEST_C_FLAGS) -dynamiclib \
|
||||
-install_name /usr/lib/libmisfix.dylib \
|
||||
-o $@ $(MISFIX_SOURCES) \
|
||||
-framework CoreFoundation -lmis -lMobileGestalt \
|
||||
-framework CoreFoundation -lmis -lMobileGestalt -lobjc \
|
||||
-Wl,-not_for_dyld_shared_cache
|
||||
@codesign --force --sign - $@
|
||||
|
||||
|
||||
Reference in New Issue
Block a user