mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
Let SpringBoard carry libmisfix so a signed app launches on 27.0
A paid team's IPA installed on test-27.0 and was refused at launch with
0xE8008026: SpringBoard asks MIS itself, and it never carried the hook. The
spawn route SystemHook-vphone.c listed it under was never reached.
- system-springboard-cfw-launch_authorization links libmisfix into
SpringBoard with a weak load command, as installd and misagent are.
- SpringBoard's header has 16 spare bytes, so the command names a /mf
root alias and inject-dylib --reclaim-source-version drops
LC_SOURCE_VERSION to leave the re-signer room for its signature.
- MISFixInstallPolicy now runs in installd only.
Measured on test-27.0 after a cold boot: SpringBoard loads libmisfix, MIS
returns 0x0 for AirBuild, and it launches.
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
@@ -46,7 +46,9 @@ extension GuestAPI {
|
||||
/// The daemons `cfw install` injects libmisfix into, and so the ones holding
|
||||
/// a UDID answer that a change has to invalidate. Keep in step with the
|
||||
/// `injectedDylibPath: "/usr/lib/libmisfix.dylib"` call sites in
|
||||
/// `VPhoneCustomFirmwareInstaller`.
|
||||
/// `VPhoneCustomFirmwareInstaller`, 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 func executeDeviceIdentity(_ method: String, _ params: [String: Any]) throws -> [String: Any]? {
|
||||
|
||||
+46
@@ -22,6 +22,13 @@
|
||||
// `ncmds` / `sizeofcmds` in the header change. What *does* move is the code
|
||||
// signature — `.strip` removes it and truncates the slice, which is why the
|
||||
// policy is part of this API rather than a separate pass.
|
||||
//
|
||||
// Some binaries leave almost no padding. SpringBoard on 24A435 has 16 bytes:
|
||||
// even after `.strip` frees its 16-byte LC_CODE_SIGNATURE, a 32-byte command
|
||||
// fits only by leaving the re-signer no room to put the signature back. For
|
||||
// those, `reclaimsSourceVersion` drops LC_SOURCE_VERSION — a version stamp
|
||||
// nothing reads at load time — and moves the commands after it up. Opt-in,
|
||||
// because it is the one case where an existing command is removed.
|
||||
|
||||
import Foundation
|
||||
import VPhonePatchKit
|
||||
@@ -62,6 +69,10 @@ private extension Data {
|
||||
replaceSubrange(offset ..< offset + count, with: Data(repeating: 0, count: count))
|
||||
}
|
||||
|
||||
func isZero(_ range: Range<Int>) -> Bool {
|
||||
!self[range].contains(where: { $0 != 0 })
|
||||
}
|
||||
|
||||
func holds(_ offset: Int, _ length: Int) -> Bool {
|
||||
offset >= 0 && length >= 0 && offset + length <= count
|
||||
}
|
||||
@@ -83,6 +94,8 @@ public struct CustomFirmwareDylibInjection: Sendable {
|
||||
public let isWeak: Bool
|
||||
/// True when LC_CODE_SIGNATURE and its blob were removed.
|
||||
public let removedCodeSignature: Bool
|
||||
/// True when LC_SOURCE_VERSION was dropped to make room.
|
||||
public let removedSourceVersion: Bool
|
||||
/// Slots re-hashed under `.keepAndReattest`. Empty under `.strip`.
|
||||
public let rehashedSlots: [CustomFirmwareSlotRehash]
|
||||
}
|
||||
@@ -114,6 +127,9 @@ public enum CustomFirmwareInjectDylib {
|
||||
static let lcSegment64: UInt32 = 0x19
|
||||
static let lcSymtab: UInt32 = 0x02
|
||||
static let lcCodeSignature: UInt32 = 0x1D
|
||||
static let lcSourceVersion: UInt32 = 0x2A
|
||||
/// sizeof(struct linkedit_data_command): what a re-signer adds back after `.strip`.
|
||||
static let codeSignatureCommandSize = 16
|
||||
static let lcLoadDylib: UInt32 = 0x0C
|
||||
static let lcLoadWeakDylib: UInt32 = 0x8000_0018
|
||||
|
||||
@@ -134,6 +150,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: Bool = true,
|
||||
policy: CodeSignaturePolicy = .strip,
|
||||
allowNonEmptyPadding: Bool = false,
|
||||
reclaimsSourceVersion: Bool = false,
|
||||
) throws -> [CustomFirmwareDylibInjection] {
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
throw PatcherError.fileNotFound(url.path)
|
||||
@@ -145,6 +162,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: weak,
|
||||
policy: policy,
|
||||
allowNonEmptyPadding: allowNonEmptyPadding,
|
||||
reclaimsSourceVersion: reclaimsSourceVersion,
|
||||
)
|
||||
try data.write(to: url)
|
||||
return injections
|
||||
@@ -158,6 +176,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: Bool = true,
|
||||
policy: CodeSignaturePolicy = .strip,
|
||||
allowNonEmptyPadding: Bool = false,
|
||||
reclaimsSourceVersion: Bool = false,
|
||||
) throws -> [CustomFirmwareDylibInjection] {
|
||||
if data.startIndex != 0 {
|
||||
data = Data(data)
|
||||
@@ -169,6 +188,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: weak,
|
||||
policy: policy,
|
||||
allowNonEmptyPadding: allowNonEmptyPadding,
|
||||
reclaimsSourceVersion: reclaimsSourceVersion,
|
||||
)
|
||||
switch data.loadBEValue(UInt32.self, at: 0) {
|
||||
case fatMagic:
|
||||
@@ -196,6 +216,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
let weak: Bool
|
||||
let policy: CodeSignaturePolicy
|
||||
let allowNonEmptyPadding: Bool
|
||||
let reclaimsSourceVersion: Bool
|
||||
}
|
||||
|
||||
// MARK: Universal Binaries
|
||||
@@ -311,6 +332,27 @@ public enum CustomFirmwareInjectDylib {
|
||||
let pathBytes = Array(options.dylibPath.utf8)
|
||||
let paddedPathSize = (pathBytes.count & ~(pathPadding - 1)) + pathPadding
|
||||
let commandSize = dylibCommandSize + paddedPathSize
|
||||
|
||||
// A stripped signature comes back when the binary is re-signed, and
|
||||
// its command needs 16 bytes after ours.
|
||||
var removedSourceVersion = false
|
||||
let needed = commandSize + (removedCodeSignature ? codeSignatureCommandSize : 0)
|
||||
let freeEnd = commandsOffset + sizeofcmds
|
||||
if options.reclaimsSourceVersion,
|
||||
let sourceVersion = layout.sourceVersion,
|
||||
!data.isZero(freeEnd ..< Swift.min(freeEnd + needed, data.count))
|
||||
{
|
||||
guard case .strip = options.policy else {
|
||||
throw PatcherError.invalidFormat("reclaiming LC_SOURCE_VERSION needs the .strip signature policy")
|
||||
}
|
||||
let tail = sourceVersion.commandOffset + sourceVersion.commandSize
|
||||
let moved = Data(data[tail ..< freeEnd])
|
||||
data.replaceSubrange(sourceVersion.commandOffset ..< sourceVersion.commandOffset + moved.count, with: moved)
|
||||
data.zeroBytes(at: freeEnd - sourceVersion.commandSize, count: sourceVersion.commandSize)
|
||||
ncmds -= 1
|
||||
sizeofcmds -= sourceVersion.commandSize
|
||||
removedSourceVersion = true
|
||||
}
|
||||
let commandOffset = commandsOffset + sizeofcmds
|
||||
|
||||
guard data.holds(commandOffset, commandSize), commandOffset + commandSize <= headerOffset + sliceSize else {
|
||||
@@ -370,6 +412,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
loadCommandSize: commandSize,
|
||||
isWeak: options.weak,
|
||||
removedCodeSignature: removedCodeSignature,
|
||||
removedSourceVersion: removedSourceVersion,
|
||||
rehashedSlots: rehashed,
|
||||
)
|
||||
}
|
||||
@@ -387,6 +430,7 @@ public enum CustomFirmwareInjectDylib {
|
||||
struct SliceLayout {
|
||||
var codeSignature: CodeSignatureCommand?
|
||||
var codeSignatureIsLast = false
|
||||
var sourceVersion: (commandOffset: Int, commandSize: Int)?
|
||||
/// Offset of the __LINKEDIT LC_SEGMENT_64 command, and its slice-relative extent.
|
||||
var linkEditCommandOffset: Int?
|
||||
var linkEditFileOffset = 0
|
||||
@@ -424,6 +468,8 @@ public enum CustomFirmwareInjectDylib {
|
||||
dataSize: Int(data.loadLEValue(UInt32.self, at: offset + 12)),
|
||||
)
|
||||
layout.codeSignatureIsLast = index == ncmds - 1
|
||||
case lcSourceVersion:
|
||||
layout.sourceVersion = (offset, cmdsize)
|
||||
case lcSegment64:
|
||||
guard data.holds(offset, 72) else {
|
||||
throw PatcherError.invalidFormat("LC_SEGMENT_64 is truncated")
|
||||
|
||||
+17
-5
@@ -81,11 +81,10 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
summary: """
|
||||
Accepts a provisioning profile that wants online authorization, by short-circuiting \
|
||||
the check in the shared cache. Off by default: libmisfix.dylib already declines the \
|
||||
same check from userspace in installd and misagent, and editing the cache for it \
|
||||
stops an iOS 27 guest booting. Turn it on only on a 26.x base, and only to launch \
|
||||
an app signed with a free personal-team certificate — that launch goes through \
|
||||
SpringBoard, which the hook does not cover. Not offered on iOS 27, where it stops \
|
||||
the guest booting.
|
||||
same check from userspace in installd, misagent and SpringBoard, and editing the \
|
||||
cache for it stops an iOS 27 guest booting. Kept for a 26.x guest whose SpringBoard \
|
||||
was installed without system-springboard-cfw-launch_authorization. Not offered on \
|
||||
iOS 27, where it stops the guest booting.
|
||||
""",
|
||||
target: .dyldSharedCache,
|
||||
applicability: misTrustAuthBases,
|
||||
@@ -160,6 +159,19 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
""",
|
||||
target: .guestExecutable(path: "/usr/libexec/misagent"),
|
||||
),
|
||||
VPhonePatchDeclaration(
|
||||
identifier: "system-springboard-cfw-launch_authorization",
|
||||
title: "SpringBoard launch authorization",
|
||||
summary: """
|
||||
Lets an installed developer-signed app launch. SpringBoard validates the app \
|
||||
with MIS again before launching it, and MIS asks for online authorization a \
|
||||
guest without an activation record can never get, so the launch is refused \
|
||||
with 0xE8008026 and "Unable to Verify App". The same hook, loaded into \
|
||||
SpringBoard, declines that check the way it does in installd. This replaces \
|
||||
dyld-exp-mis_trust_auth without writing the shared cache.
|
||||
""",
|
||||
target: .guestExecutable(path: "/System/Library/CoreServices/SpringBoard.app/SpringBoard"),
|
||||
),
|
||||
VPhonePatchDeclaration(
|
||||
identifier: "system-debugserver-cfw-install",
|
||||
title: "debugserver",
|
||||
|
||||
+8
-6
@@ -68,7 +68,8 @@ public enum FirmwarePatchSetCatalog {
|
||||
/// libmis only calls `checkTrustAndAuthorization` when that flag is set, so
|
||||
/// `0xE8008026` — "missing trust and/or authorization", which a hacktivated
|
||||
/// guest with no activation record can never satisfy — is never produced.
|
||||
/// `cfw install` injects that hook into `installd` and `misagent`.
|
||||
/// `cfw install` injects that hook into `installd`, `misagent` and
|
||||
/// `SpringBoard`.
|
||||
///
|
||||
/// Leaving the patch on costs more than it buys. On iOS 27 it stops the guest
|
||||
/// booting: TXM rejects the re-attested page, dyld cannot map
|
||||
@@ -77,11 +78,12 @@ public enum FirmwarePatchSetCatalog {
|
||||
/// neither the seeding prologue the patcher matches nor the patcher's own
|
||||
/// output, so `cfw install` fails outright before it writes anything.
|
||||
///
|
||||
/// What it still buys, and why it stays declared rather than being deleted:
|
||||
/// the hook only covers the processes it is injected into, so an app signed
|
||||
/// with a *free personal-team* certificate can be installed but is still
|
||||
/// refused at launch, where SpringBoard asks MIS itself. A VM that wants that
|
||||
/// on a 26.x base can check this box; on 27 it should not.
|
||||
/// Why it stays declared rather than being deleted: the hook only covers the
|
||||
/// processes it is injected into, and until
|
||||
/// `system-springboard-cfw-launch_authorization` SpringBoard was not one of
|
||||
/// them, so an installed app was refused at launch. A 26.x guest whose
|
||||
/// SpringBoard predates that patch can check this box instead of running
|
||||
/// `cfw install` again; on 27 it should not.
|
||||
public static let misTrustAuthPatch = "dyld-exp-mis_trust_auth"
|
||||
|
||||
/// The former EXP patches that make the guest claim to be an iPhone17,3.
|
||||
|
||||
+73
@@ -614,4 +614,77 @@ struct CustomFirmwareInjectDylibTests {
|
||||
try #require(after.status == 0, "injected hello failed: \(after.output)")
|
||||
#expect(after.output == "world hello\n")
|
||||
}
|
||||
|
||||
/// SpringBoard's shape: room for the command, but not for the signature a
|
||||
/// re-signer puts back after it. Dropping LC_SOURCE_VERSION has to make that
|
||||
/// room, leave every byte past it alone, and leave a binary dyld still runs.
|
||||
@Test func `reclaiming LC_SOURCE_VERSION makes room for the command and the signature`() throws {
|
||||
let fixtures = MachOFixture.repositoryRoot
|
||||
.appending(path: "VPhoneExecutable/VPhoneCommand/FirmwarePatcherTestFixtures/DylibInjection")
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appending(path: "DylibInjection-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let executable = directory.appending(path: "hello")
|
||||
let dylib = directory.appending(path: "swizzle.dylib")
|
||||
let clang = URL(filePath: "/usr/bin/clang")
|
||||
let buildExecutable = try MachOFixture.run(
|
||||
clang,
|
||||
[
|
||||
"-fobjc-arc", "-framework", "Foundation", "-Wl,-headerpad,0x4000",
|
||||
fixtures.appending(path: "hello.m").path, "-o", executable.path,
|
||||
],
|
||||
)
|
||||
try #require(buildExecutable.status == 0, "hello fixture: \(buildExecutable.output)")
|
||||
let buildDylib = try MachOFixture.run(
|
||||
clang,
|
||||
[
|
||||
"-dynamiclib", "-fobjc-arc", "-framework", "Foundation",
|
||||
fixtures.appending(path: "swizzle.m").path, "-o", dylib.path,
|
||||
],
|
||||
)
|
||||
try #require(buildDylib.status == 0, "swizzle fixture: \(buildDylib.output)")
|
||||
|
||||
// Leave exactly enough zero padding for the command once the 16-byte
|
||||
// LC_CODE_SIGNATURE is stripped, and mark the first byte past it.
|
||||
var data = try Data(contentsOf: executable)
|
||||
let ncmds = data.loadLE(UInt32.self, at: 16)
|
||||
let commandsEnd = 32 + Int(data.loadLE(UInt32.self, at: 20))
|
||||
let pathBytes = dylib.path.utf8.count
|
||||
let commandSize = 24 + (pathBytes & ~7) + 8
|
||||
let marker = commandsEnd + commandSize - 16
|
||||
data[marker] = 0xFF
|
||||
try data.write(to: executable)
|
||||
|
||||
let injection = try #require(
|
||||
try CustomFirmwareInjectDylib.inject(
|
||||
dylibPath: dylib.path,
|
||||
into: executable,
|
||||
reclaimsSourceVersion: true,
|
||||
).first,
|
||||
)
|
||||
#expect(injection.removedCodeSignature)
|
||||
#expect(injection.removedSourceVersion)
|
||||
|
||||
var after = try Data(contentsOf: executable)
|
||||
#expect(after[marker] == 0xFF, "the byte past the reclaimed room must not be written")
|
||||
// Out: LC_CODE_SIGNATURE and LC_SOURCE_VERSION. In: the dylib.
|
||||
#expect(after.loadLE(UInt32.self, at: 16) == ncmds - 1)
|
||||
#expect(Int(after.loadLE(UInt32.self, at: 20)) == commandsEnd - 32 - 16 - 16 + commandSize)
|
||||
#expect(MachOFixture.dylibLoadCommands(in: after).last?.path == dylib.path)
|
||||
#expect(MachOFixture.dylibLoadCommands(in: after).count == MachOFixture.dylibLoadCommands(in: data).count + 1)
|
||||
|
||||
// codesign needs the reserved 16 bytes, and zero ones.
|
||||
after[marker] = 0
|
||||
try after.write(to: executable)
|
||||
let sign = try MachOFixture.run(
|
||||
URL(filePath: "/usr/bin/codesign"),
|
||||
["--force", "--sign", "-", "--timestamp=none", executable.path],
|
||||
)
|
||||
try #require(sign.status == 0, "signing reclaimed hello: \(sign.output)")
|
||||
let run = try MachOFixture.run(executable, [])
|
||||
try #require(run.status == 0, "reclaimed hello failed: \(run.output)")
|
||||
#expect(run.output == "world hello\n")
|
||||
}
|
||||
}
|
||||
|
||||
+31
-5
@@ -677,6 +677,24 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
injectedDylibPath: "/usr/lib/libmisfix.dylib",
|
||||
)
|
||||
}
|
||||
if on("system-springboard-cfw-launch_authorization") {
|
||||
// SpringBoard asks MIS again before it launches an app, and no
|
||||
// spawn hook reaches it: launchd starts it directly, in its
|
||||
// conclave, and it never carries SystemHook. A load command is the
|
||||
// one route that always holds.
|
||||
// Its header has 16 spare bytes, so the command names the short
|
||||
// alias and LC_SOURCE_VERSION makes room for the new signature.
|
||||
try installLibraryAlias(system: system, alias: "mf", target: "/usr/lib/libmisfix.dylib")
|
||||
try patchMachO(
|
||||
system: system,
|
||||
work: work,
|
||||
path: "System/Library/CoreServices/SpringBoard.app/SpringBoard",
|
||||
identifier: "com.apple.springboard",
|
||||
preserveEntitlements: true,
|
||||
injectedDylibPath: "/mf",
|
||||
reclaimsSourceVersion: true,
|
||||
)
|
||||
}
|
||||
if on("system-debugserver-cfw-install") {
|
||||
try patchDebugserver(system: system, work: work)
|
||||
}
|
||||
@@ -921,16 +939,20 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
}
|
||||
// launchd has little free header space for another load command.
|
||||
// /vh fits the same 32-byte command as the old /b without reusing it.
|
||||
let alias = "vh"
|
||||
let target = "/usr/lib/launchdhook-vphone.dylib"
|
||||
try installLibraryAlias(system: system, alias: "vh", target: "/usr/lib/launchdhook-vphone.dylib")
|
||||
try installMISFixDefaults(system: system)
|
||||
}
|
||||
|
||||
/// A symlink at the volume root, so a load command in a binary with little
|
||||
/// header space can name a library in seven bytes or fewer.
|
||||
private func installLibraryAlias(system: VPhoneConfinedDirectory, alias: String, target: String) throws {
|
||||
if try system.exists(alias) {
|
||||
guard try system.readLink(alias) == target else {
|
||||
throw ValidationError("Another file already uses /vh on the VM system volume. Remove it, then install CFW again.")
|
||||
throw ValidationError("Another file already uses /\(alias) on the VM system volume. Remove it, then install CFW again.")
|
||||
}
|
||||
} else {
|
||||
try system.createSymlink(target: target, at: alias)
|
||||
}
|
||||
try installMISFixDefaults(system: system)
|
||||
}
|
||||
|
||||
/// libmisfix's settings file, and only when the guest has none.
|
||||
@@ -1059,6 +1081,7 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
identifier: String? = nil,
|
||||
preserveEntitlements: Bool = false,
|
||||
injectedDylibPath: String? = nil,
|
||||
reclaimsSourceVersion: Bool = false,
|
||||
) throws {
|
||||
let backup = "\(path).bak"
|
||||
if try !system.exists(backup) {
|
||||
@@ -1076,7 +1099,10 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
try patch(verb, [staged.path])
|
||||
}
|
||||
if let injectedDylibPath {
|
||||
try patch("inject-dylib", [staged.path, injectedDylibPath])
|
||||
try patch(
|
||||
"inject-dylib",
|
||||
[staged.path, injectedDylibPath] + (reclaimsSourceVersion ? ["--reclaim-source-version"] : []),
|
||||
)
|
||||
}
|
||||
try VPhoneSigner.sign(
|
||||
fileAt: staged,
|
||||
|
||||
+6
-1
@@ -164,17 +164,22 @@ struct VPhoneCustomFirmwareInjectDylibCommand: ParsableCommand {
|
||||
@Argument(help: "Path the guest will load the dylib from (e.g. /b)")
|
||||
var dylibPath: String
|
||||
|
||||
@Flag(help: "Drop LC_SOURCE_VERSION when the header has no room for the command and the re-signed signature")
|
||||
var reclaimSourceVersion = false
|
||||
|
||||
func run() throws {
|
||||
let injections = try CustomFirmwareInjectDylib.inject(
|
||||
dylibPath: dylibPath,
|
||||
into: binary,
|
||||
weak: true,
|
||||
policy: .strip,
|
||||
reclaimsSourceVersion: reclaimSourceVersion,
|
||||
)
|
||||
for injection in injections {
|
||||
let stripped = injection.removedCodeSignature ? ", signature stripped" : ""
|
||||
let reclaimed = injection.removedSourceVersion ? ", LC_SOURCE_VERSION dropped" : ""
|
||||
print(" [+] LC_LOAD_WEAK_DYLIB \(dylibPath) -> \(binary.lastPathComponent) "
|
||||
+ "(slice +0x\(String(injection.sliceOffset, radix: 16))\(stripped))")
|
||||
+ "(slice +0x\(String(injection.sliceOffset, radix: 16))\(stripped)\(reclaimed))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
EXP variant; the camera does not need them, and with the concealment they left
|
||||
guests without location (issue #438). The fourth is the MIS
|
||||
online-authorization short-circuit: libmisfix.dylib declines the same check from
|
||||
userspace in installd and misagent, so editing the shared cache for it buys
|
||||
nothing there, costs an iOS 27 guest its boot (issue #532), and on 24A435 cannot
|
||||
userspace in installd, misagent and SpringBoard, so editing the shared cache for
|
||||
it buys nothing there, costs an iOS 27 guest its boot (issue #532), and on 24A435 cannot
|
||||
even be applied. See FirmwarePatchSetCatalog.misTrustAuthPatch.
|
||||
|
||||
Each patch's own version gate still decides whether it lands on this pairing of
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <os/log.h>
|
||||
#include <ptrauth.h>
|
||||
#include <stdarg.h>
|
||||
@@ -173,6 +174,15 @@ int MISFixConfiguredFlag(CFStringRef key) {
|
||||
return CFBooleanGetValue((CFBooleanRef)value) ? 1 : 0;
|
||||
}
|
||||
|
||||
int MISFixProcessIs(const char *name) {
|
||||
char path[4096];
|
||||
uint32_t size = sizeof(path);
|
||||
if (name == NULL || _NSGetExecutablePath(path, &size) != 0)
|
||||
return 0;
|
||||
const char *slash = strrchr(path, '/');
|
||||
return strcmp(slash != NULL ? slash + 1 : path, name) == 0;
|
||||
}
|
||||
|
||||
const char *MISFixCallerImage(const void *address) {
|
||||
if (address == NULL)
|
||||
return "?";
|
||||
|
||||
@@ -70,6 +70,15 @@ void MISFixNote(const char *format, ...) __attribute__((format(printf, 1, 2)));
|
||||
/// calls too.
|
||||
const char *MISFixCallerImage(const void *address);
|
||||
|
||||
/// Whether this process's executable is named `name`, compared on the last
|
||||
/// path component.
|
||||
///
|
||||
/// The same dylib is linked into installd, misagent and SpringBoard, and not
|
||||
/// every hook belongs in all three: MobileInstallation's policy is installd's
|
||||
/// alone, and loading that framework into SpringBoard to swizzle it would
|
||||
/// change a process the hook has no business in.
|
||||
int MISFixProcessIs(const char *name);
|
||||
|
||||
/// The image of whoever called the function this appears in.
|
||||
#define MISFixCaller() MISFixCallerImage(__builtin_return_address(0))
|
||||
|
||||
|
||||
@@ -188,7 +188,12 @@ static BOOL vpAllowAdhocSigning(id self, SEL selector) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
/// installd only. The same dylib is linked into misagent and SpringBoard, and
|
||||
/// neither runs an install; `vpSwizzle` would dlopen MobileInstallation into
|
||||
/// them just to find a class they never use.
|
||||
__attribute__((constructor)) static void vpInstallPolicyHooks(void) {
|
||||
if (!MISFixProcessIs("installd"))
|
||||
return;
|
||||
vpOriginalInstallProfiles = (MISFixCheckIMP)vpSwizzle(
|
||||
"MIInstallableBundle",
|
||||
"_installEmbeddedProfilesWithError:",
|
||||
|
||||
@@ -60,11 +60,20 @@ static int vpPathHasSuffix(const char *path, const char *suffix) {
|
||||
// is in MobileInstallation and calls libmis. This is the install.
|
||||
// misagent installs the embedded profile and checks ProvisionedDevices.
|
||||
// SpringBoard asks MIS again at launch, which is the half neither daemon
|
||||
// covers: an app signed with a free personal-team certificate
|
||||
// could be installed and then refused at launch with 0xE8008026.
|
||||
// covers: an installed app is refused at launch with 0xE8008026.
|
||||
//
|
||||
// Matched on the end of the path so a bootstrap or cryptex copy of the same
|
||||
// binary is caught too.
|
||||
//
|
||||
// This list is a second route, not the one the hook depends on. `cfw install`
|
||||
// links libmisfix into all three with a load command, and for SpringBoard
|
||||
// that is the only route that works: SpringBoard never carries this dylib.
|
||||
// Measured on test-27.0 (2026-09-30): launchd starts it without an xpcproxy
|
||||
// (the launchd hook's spawn log has every neighbouring child pid but not
|
||||
// SpringBoard's), and SpringBoard's own constructor line, which any `.app/`
|
||||
// path would write to vphone-systemhook.log, never appears. Its job runs in a
|
||||
// conclave (`_Conclave` in com.apple.SpringBoard.plist); whether the insert
|
||||
// is dropped there or never made is not settled.
|
||||
static int vpIsMISFixTarget(const char *path) {
|
||||
if (!path)
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user