mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
Give libmisfix one route: the spawn hooks, SpringBoard included
SpringBoard did carry SystemHook; what it lacked was libmisfix beside it.
Which libraries a process gets is decided in its parent, and launchd starts
SpringBoard itself, so SystemHook's list naming it was never consulted. A
probe reading KERN_PROCARGS2 showed DYLD_INSERT_LIBRARIES held SystemHook
alone in SpringBoard and both libraries in installd and misagent.
- vpIsMISFixTarget moves to InjectionEnvironment.h and the launchd hook
asks it too, inserting libmisfix only when the dylib exists.
- vpInsertHooks dropped the extra library when the environment already
named SystemHook; fixed, with make test-injection-environment.
- No guest binary carries a libmisfix load command. The three declarations
that added them are gone, cfw install puts each .bak back and removes
/mf, and inject-dylib --reclaim-source-version goes with its only caller.
- SystemHook's logs are world-writable: a root-created 0644 file silently
dropped every line from a mobile process, which is what made SpringBoard
look as though it never carried the hook.
Measured on test-26.4 and test-27.0, both recreated from local IPSWs: from
the first boot SpringBoard, installd and misagent carry both libraries, and
AirBuild installed through installd opens on both.
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
@@ -136,7 +136,10 @@ linked call, no options -> 0x0 (0xE8008014 without the hook)
|
||||
|
||||
`system-installd-cfw-adhoc_signature` — `VPhoneGuestComponents/MISFix/MISFix-vphone.c`,
|
||||
built as `/usr/lib/libmisfix.dylib`, attached to `/usr/libexec/installd` by a
|
||||
`LC_LOAD_WEAK_DYLIB` that `cfw install` inserts. It interposes
|
||||
`LC_LOAD_WEAK_DYLIB` that `cfw install` inserts. (Superseded 2026-09-30: the
|
||||
declaration is gone, and the spawn hooks insert libmisfix into installd,
|
||||
misagent and SpringBoard — see "One route for libmisfix" in
|
||||
`Research/0_binary_patch_comparison.md`.) It interposes
|
||||
`MISValidateSignatureAndCopyInfo` and
|
||||
`MISValidateSignatureAndCopyInfoWithProgress`, adds `AllowAdHocSigning` to the
|
||||
caller's options, and calls through. Nothing in the dyld shared cache is
|
||||
@@ -252,7 +255,8 @@ again.
|
||||
## What was built
|
||||
|
||||
`system-misagent-cfw-device_identity` — the same `libmisfix.dylib`, attached to
|
||||
`/usr/libexec/misagent`, interposing `MGCopyAnswer` and `MGCopyAnswerWithError`
|
||||
`/usr/libexec/misagent` (now inserted at spawn, like installd's; the
|
||||
declaration is gone), interposing `MGCopyAnswer` and `MGCopyAnswerWithError`
|
||||
and answering `UniqueDeviceID` with the value in `libmisfix.plist`. Set it to a
|
||||
device the team has already registered and that team's profiles install here,
|
||||
with no portal round trip and nothing to redo after a rebuild. Absent or empty,
|
||||
|
||||
@@ -24,7 +24,7 @@ import Foundation
|
||||
/// `+[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:]`
|
||||
/// inside installd, which calls `MISValidateSignatureAndCopyInfo` and evaluates
|
||||
/// the profile's `ProvisionedDevices` itself. Two processes answer the UDID
|
||||
/// question independently — which is why `cfw install` injects libmisfix into
|
||||
/// question independently — which is why the spawn hooks insert libmisfix into
|
||||
/// both — so refreshing one and not the other leaves them disagreeing.
|
||||
///
|
||||
/// Measured on test-26.4 (2026-09-30): with the override changed to a UDID in
|
||||
@@ -43,12 +43,11 @@ extension GuestAPI {
|
||||
static let udidConfigPaths = ["/var/db/vphone/misfix.plist", "/usr/lib/libmisfix.plist"]
|
||||
static let udidConfigKey = "UniqueDeviceID"
|
||||
|
||||
/// 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`, 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.
|
||||
/// The daemons libmisfix is inserted into, and so the ones holding a UDID
|
||||
/// answer that a change has to invalidate. Keep in step with
|
||||
/// `vpIsMISFixTarget` in `VPhoneGuestComponents/Shared/InjectionEnvironment.h`,
|
||||
/// bar SpringBoard: it carries the hook for the launch check alone, never
|
||||
/// asks for the UDID, and stopping it would take the home screen down with it.
|
||||
static let udidHookedDaemons = ["misagent", "installd"]
|
||||
|
||||
static func executeDeviceIdentity(_ method: String, _ params: [String: Any]) throws -> [String: Any]? {
|
||||
|
||||
@@ -78,8 +78,8 @@ extension GuestAPI {
|
||||
}
|
||||
// Same for the MIS hook: a running daemon keeps the copy it mapped at
|
||||
// launch, so replacing the file changes nothing until it restarts.
|
||||
// SystemHook is what inserts libmisfix into these, so a new SystemHook
|
||||
// matters to them too. SpringBoard is deliberately not in this list —
|
||||
// SystemHook (through xpcproxy) is what inserts libmisfix into these,
|
||||
// so a new SystemHook matters to them too. SpringBoard is deliberately not in this list —
|
||||
// restarting it is a respring, which is `system.respring` to ask for
|
||||
// and not something an environment update should do behind the back of
|
||||
// whoever is looking at the screen.
|
||||
|
||||
-46
@@ -22,13 +22,6 @@
|
||||
// `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
|
||||
@@ -69,10 +62,6 @@ 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
|
||||
}
|
||||
@@ -94,8 +83,6 @@ 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]
|
||||
}
|
||||
@@ -127,9 +114,6 @@ 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
|
||||
|
||||
@@ -150,7 +134,6 @@ 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)
|
||||
@@ -162,7 +145,6 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: weak,
|
||||
policy: policy,
|
||||
allowNonEmptyPadding: allowNonEmptyPadding,
|
||||
reclaimsSourceVersion: reclaimsSourceVersion,
|
||||
)
|
||||
try data.write(to: url)
|
||||
return injections
|
||||
@@ -176,7 +158,6 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: Bool = true,
|
||||
policy: CodeSignaturePolicy = .strip,
|
||||
allowNonEmptyPadding: Bool = false,
|
||||
reclaimsSourceVersion: Bool = false,
|
||||
) throws -> [CustomFirmwareDylibInjection] {
|
||||
if data.startIndex != 0 {
|
||||
data = Data(data)
|
||||
@@ -188,7 +169,6 @@ public enum CustomFirmwareInjectDylib {
|
||||
weak: weak,
|
||||
policy: policy,
|
||||
allowNonEmptyPadding: allowNonEmptyPadding,
|
||||
reclaimsSourceVersion: reclaimsSourceVersion,
|
||||
)
|
||||
switch data.loadBEValue(UInt32.self, at: 0) {
|
||||
case fatMagic:
|
||||
@@ -216,7 +196,6 @@ public enum CustomFirmwareInjectDylib {
|
||||
let weak: Bool
|
||||
let policy: CodeSignaturePolicy
|
||||
let allowNonEmptyPadding: Bool
|
||||
let reclaimsSourceVersion: Bool
|
||||
}
|
||||
|
||||
// MARK: Universal Binaries
|
||||
@@ -332,27 +311,6 @@ 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 {
|
||||
@@ -412,7 +370,6 @@ public enum CustomFirmwareInjectDylib {
|
||||
loadCommandSize: commandSize,
|
||||
isWeak: options.weak,
|
||||
removedCodeSignature: removedCodeSignature,
|
||||
removedSourceVersion: removedSourceVersion,
|
||||
rehashedSlots: rehashed,
|
||||
)
|
||||
}
|
||||
@@ -430,7 +387,6 @@ 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
|
||||
@@ -468,8 +424,6 @@ 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")
|
||||
|
||||
+7
-49
@@ -82,9 +82,7 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
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, 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.
|
||||
cache for it stops an iOS 27 guest booting. Not offered on iOS 27.
|
||||
""",
|
||||
target: .dyldSharedCache,
|
||||
applicability: misTrustAuthBases,
|
||||
@@ -127,51 +125,6 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
target: .guestExecutable(path: "/sbin/launchd"),
|
||||
bootEssential: true,
|
||||
),
|
||||
VPhonePatchDeclaration(
|
||||
identifier: "system-installd-cfw-adhoc_signature",
|
||||
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 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 turns on the ad-hoc switch \
|
||||
MICodeSigningVerifier carries and installd never sets. Nothing in the dyld \
|
||||
shared cache is written on disk: the 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 profile scope",
|
||||
summary: """
|
||||
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"),
|
||||
),
|
||||
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",
|
||||
@@ -212,7 +165,12 @@ public enum FirmwareGuestSystemPatchSet {
|
||||
VPhonePatchDeclaration(
|
||||
identifier: "system-launchdaemons-boot-environment",
|
||||
title: "Guest environment",
|
||||
summary: "Installs the launchd environment and plists the guest tools read.",
|
||||
summary: """
|
||||
Installs the launchd environment and plists the guest tools read, and the hooks \
|
||||
launchd and SystemHook insert at spawn. Among them is libmisfix.dylib, inserted \
|
||||
into installd, misagent and SpringBoard, which lets Xcode install and launch an \
|
||||
app signed for someone else's team, or ad hoc, without writing the shared cache.
|
||||
""",
|
||||
target: .guestFile(path: "/Library/LaunchDaemons"),
|
||||
bootEssential: true,
|
||||
),
|
||||
|
||||
+5
-8
@@ -68,8 +68,7 @@ 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`, `misagent` and
|
||||
/// `SpringBoard`.
|
||||
/// The spawn hooks insert it 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
|
||||
@@ -78,12 +77,10 @@ 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.
|
||||
///
|
||||
/// 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.
|
||||
/// Why it stays declared rather than being deleted: on a 26.x guest whose
|
||||
/// environment predates SpringBoard getting the hook, an installed app is
|
||||
/// refused at launch, and this box is an alternative to updating the
|
||||
/// environment. On 27 it is not offered.
|
||||
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,77 +614,4 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
+27
-47
@@ -569,8 +569,8 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
}
|
||||
// Version-agnostic: the guest is hacktivated on every base, so the
|
||||
// profile check this opens fails on every base too. Off in `standard`,
|
||||
// because libmisfix declines the same check from userspace in installd
|
||||
// and misagent without touching the cache — see
|
||||
// because libmisfix declines the same check from userspace in installd,
|
||||
// misagent and SpringBoard without touching the cache — see
|
||||
// FirmwarePatchSetCatalog.misTrustAuthPatch for why editing the cache
|
||||
// is the worse trade on 27.
|
||||
if on(FirmwarePatchSetCatalog.misTrustAuthPatch) {
|
||||
@@ -655,46 +655,7 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
injectedDylibPath: "/vh",
|
||||
)
|
||||
}
|
||||
if on("system-installd-cfw-adhoc_signature") {
|
||||
// No bytes of installd's own change: the hook rides in on a weak
|
||||
// load command and does its work through dyld interposition.
|
||||
try patchMachO(
|
||||
system: system,
|
||||
work: work,
|
||||
path: "usr/libexec/installd",
|
||||
identifier: "com.apple.installd",
|
||||
preserveEntitlements: true,
|
||||
injectedDylibPath: "/usr/lib/libmisfix.dylib",
|
||||
)
|
||||
}
|
||||
if on("system-misagent-cfw-device_identity") {
|
||||
try patchMachO(
|
||||
system: system,
|
||||
work: work,
|
||||
path: "usr/libexec/misagent",
|
||||
identifier: "com.apple.misagent",
|
||||
preserveEntitlements: true,
|
||||
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,
|
||||
)
|
||||
}
|
||||
try restoreMISFixTargets(system: system)
|
||||
if on("system-debugserver-cfw-install") {
|
||||
try patchDebugserver(system: system, work: work)
|
||||
}
|
||||
@@ -955,6 +916,29 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
/// Put back the Apple binaries earlier installs linked libmisfix into.
|
||||
///
|
||||
/// installd, misagent and SpringBoard now get the hook the way every other
|
||||
/// guest process gets SystemHook: the spawn hooks insert it (see
|
||||
/// `vpIsMISFixTarget` in `VPhoneGuestComponents/Shared/InjectionEnvironment.h`).
|
||||
/// A guest installed before that still carries a load command in each, and
|
||||
/// `patchMachO` left the original beside it as `.bak`. The `/mf` alias
|
||||
/// existed only for SpringBoard's.
|
||||
private func restoreMISFixTargets(system: VPhoneConfinedDirectory) throws {
|
||||
let targets = [
|
||||
"usr/libexec/installd",
|
||||
"usr/libexec/misagent",
|
||||
"System/Library/CoreServices/SpringBoard.app/SpringBoard",
|
||||
]
|
||||
for path in targets where try system.isRegularFile("\(path).bak") {
|
||||
try system.rename("\(path).bak", to: path)
|
||||
print(" [+] \(path): restored the original, libmisfix is inserted at spawn")
|
||||
}
|
||||
if try system.isSymlink("mf"), try system.readLink("mf") == "/usr/lib/libmisfix.dylib" {
|
||||
try system.removeItem("mf")
|
||||
}
|
||||
}
|
||||
|
||||
/// libmisfix's settings file, and only when the guest has none.
|
||||
///
|
||||
/// Unlike the libraries above this is not the bundle's to own: it carries a
|
||||
@@ -1081,7 +1065,6 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
identifier: String? = nil,
|
||||
preserveEntitlements: Bool = false,
|
||||
injectedDylibPath: String? = nil,
|
||||
reclaimsSourceVersion: Bool = false,
|
||||
) throws {
|
||||
let backup = "\(path).bak"
|
||||
if try !system.exists(backup) {
|
||||
@@ -1099,10 +1082,7 @@ struct VPhoneCustomFirmwareInstaller {
|
||||
try patch(verb, [staged.path])
|
||||
}
|
||||
if let injectedDylibPath {
|
||||
try patch(
|
||||
"inject-dylib",
|
||||
[staged.path, injectedDylibPath] + (reclaimsSourceVersion ? ["--reclaim-source-version"] : []),
|
||||
)
|
||||
try patch("inject-dylib", [staged.path, injectedDylibPath])
|
||||
}
|
||||
try VPhoneSigner.sign(
|
||||
fileAt: staged,
|
||||
|
||||
+1
-6
@@ -164,22 +164,17 @@ 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)\(reclaimed))")
|
||||
+ "(slice +0x\(String(injection.sliceOffset, radix: 16))\(stripped))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +91,14 @@ static int vpSpawnWith(VPSpawnFunction spawn, pid_t *restrict pid, const char *r
|
||||
// loading ElleKit. Only launchd re-executing itself is left alone.
|
||||
if (!path || strcmp(path, "/sbin/launchd") == 0)
|
||||
return spawn(pid, path, actions, attributes, argv, envp);
|
||||
VPInjectionEnvironment injected = vpInsertHook(envp, vpBootRoot);
|
||||
// launchd starts some jobs itself rather than through xpcproxy — SpringBoard
|
||||
// is one — so the MIS hook has to be decided here as well as in SystemHook.
|
||||
const char *misFix = vpMISFixFor(path);
|
||||
VPInjectionEnvironment injected = vpInsertHooks(envp, vpBootRoot, misFix);
|
||||
int status = spawn(pid, path, actions, attributes, argv, injected.values ? injected.values : envp);
|
||||
if (bootstrapProgram || appProgram || strcmp(path, "/usr/libexec/xpcproxy") == 0) {
|
||||
if (bootstrapProgram || appProgram || misFix || strcmp(path, "/usr/libexec/xpcproxy") == 0) {
|
||||
const char *event = !injected.values ? "unchanged" :
|
||||
misFix ? "inserted+misfix" :
|
||||
vpInjectionDisabled(envp) ? "inserted-tweaks-disabled" : "inserted";
|
||||
vpLogInjection(event, path, status);
|
||||
vpLogSpawn(event, path, status == 0 && pid ? *pid : -1, status);
|
||||
|
||||
@@ -73,7 +73,7 @@ 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
|
||||
/// The same dylib is inserted 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.
|
||||
|
||||
@@ -188,7 +188,7 @@ static BOOL vpAllowAdhocSigning(id self, SEL selector) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
/// installd only. The same dylib is linked into misagent and SpringBoard, and
|
||||
/// installd only. The same dylib is inserted 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) {
|
||||
|
||||
@@ -80,7 +80,7 @@ GPU_PROVENANCE := $(STAGE)/gpu/README.md
|
||||
|
||||
# Directory timestamps also catch files removed from the wildcard inputs.
|
||||
|
||||
.PHONY: all gpu package test-loader-links test-vcam-dataplane clean
|
||||
.PHONY: all gpu package test-loader-links test-injection-environment test-vcam-dataplane clean
|
||||
|
||||
all: $(ARTIFACTS) $(PLISTS) $(GPU_PROVENANCE)
|
||||
|
||||
@@ -97,6 +97,12 @@ test-loader-links:
|
||||
Tests/RootHideLoaderLinksTests.c Shared/RootHideLoaderLinks.c
|
||||
@$(OUT)/RootHideLoaderLinksTests
|
||||
|
||||
test-injection-environment:
|
||||
@mkdir -p $(OUT)
|
||||
/usr/bin/clang -Wall -Wextra -Werror -Wno-unused-function -Wno-write-strings \
|
||||
-o $(OUT)/InjectionEnvironmentTests Tests/InjectionEnvironmentTests.c
|
||||
@$(OUT)/InjectionEnvironmentTests
|
||||
|
||||
# Host proof harness for the camera data plane the guest hooks share.
|
||||
test-vcam-dataplane:
|
||||
@mkdir -p $(OUT)
|
||||
|
||||
@@ -4,26 +4,56 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define VP_SYSTEM_HOOK "/usr/lib/SystemHook-vphone.dylib"
|
||||
|
||||
// The MIS hook. Inserted only into the processes that evaluate a code
|
||||
// signature or a provisioning profile — see `vpIsMISFixTarget` in
|
||||
// SystemHook-vphone.c — rather than carried by every spawn.
|
||||
//
|
||||
// It is inserted rather than linked, and that distinction is the point.
|
||||
// `cfw install` used to give installd and misagent an LC_LOAD_WEAK_DYLIB, which
|
||||
// makes the hook a dependency of the main executable. That is enough to
|
||||
// interpose calls the main executable makes itself, which is why misagent's
|
||||
// UDID override worked, and it is *not* enough for a call made between two
|
||||
// shared-cache images: installd's profile check is
|
||||
// `+[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:]`
|
||||
// in MobileInstallation calling libmis, with installd's own image not involved,
|
||||
// and that one kept seeing the guest's real UDID. DYLD_INSERT_LIBRARIES loads
|
||||
// the hook ahead of everything else, which is where an interpose covers the
|
||||
// cache's own uses of a symbol too.
|
||||
// signature or a provisioning profile — see `vpIsMISFixTarget` below —
|
||||
// rather than carried by every spawn. This insertion is the only way it gets
|
||||
// there: no guest binary carries a load command for it.
|
||||
#define VP_MIS_FIX "/usr/lib/libmisfix.dylib"
|
||||
|
||||
static int vpPathHasSuffix(const char *path, const char *suffix) {
|
||||
size_t length = path ? strlen(path) : 0;
|
||||
size_t want = strlen(suffix);
|
||||
return length >= want && strcmp(path + length - want, suffix) == 0;
|
||||
}
|
||||
|
||||
// The processes that evaluate a code signature or a provisioning profile, and
|
||||
// so the ones that have to agree about what device this is and what signatures
|
||||
// are acceptable. Everything else spawns without libmisfix.
|
||||
//
|
||||
// installd runs `+[MICodeSigningVerifier
|
||||
// _validateSignatureAndCopyInfoForURL:withOptions:error:]`, which
|
||||
// is in MobileInstallation and calls libmis. This is the install.
|
||||
// misagent installs the embedded profile and checks ProvisionedDevices.
|
||||
// SpringBoard asks MIS again at launch; without the hook an installed app is
|
||||
// refused there with 0xE8008026.
|
||||
//
|
||||
// Both spawn hooks ask this, because the targets do not share a parent:
|
||||
// installd and misagent are started through xpcproxy, which carries
|
||||
// SystemHook, and SpringBoard (`POSIXSpawnType` App) is started by launchd
|
||||
// itself, which carries only the launchd hook. Asking in one of them alone is
|
||||
// what left SpringBoard without libmisfix.
|
||||
//
|
||||
// Matched on the end of the path so a bootstrap or cryptex copy of the same
|
||||
// binary is caught too.
|
||||
static int vpIsMISFixTarget(const char *path) {
|
||||
if (!path)
|
||||
return 0;
|
||||
return vpPathHasSuffix(path, "/usr/libexec/installd") ||
|
||||
vpPathHasSuffix(path, "/usr/libexec/misagent") ||
|
||||
vpPathHasSuffix(path, "/SpringBoard.app/SpringBoard");
|
||||
}
|
||||
|
||||
// The library to insert alongside SystemHook for `path`, or NULL. NULL as well
|
||||
// when the dylib is not installed, so a guest without it never gets a
|
||||
// DYLD_INSERT_LIBRARIES entry naming a missing file.
|
||||
static const char *vpMISFixFor(const char *path) {
|
||||
return vpIsMISFixTarget(path) && access(VP_MIS_FIX, R_OK) == 0 ? VP_MIS_FIX : NULL;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char **values;
|
||||
char *hook;
|
||||
@@ -130,7 +160,10 @@ static VPInjectionEnvironment vpInsertHooks(char *const env[], const char *root,
|
||||
}
|
||||
snprintf(result.root, size, "VPHONE_JB_ROOT=%s", root);
|
||||
}
|
||||
result.values = calloc(count + (addHook && dyld == (size_t)-1) +
|
||||
// Either addition rewrites the whole DYLD_INSERT_LIBRARIES entry, so an
|
||||
// environment that already names SystemHook still gets the extra library.
|
||||
const int addLibraries = addHook || addExtra;
|
||||
result.values = calloc(count + (addLibraries && dyld == (size_t)-1) +
|
||||
(addRoot && jbRoot == (size_t)-1) + 1, sizeof(char *));
|
||||
if (!result.values) {
|
||||
free(result.hook);
|
||||
@@ -138,20 +171,15 @@ static VPInjectionEnvironment vpInsertHooks(char *const env[], const char *root,
|
||||
return (VPInjectionEnvironment){0};
|
||||
}
|
||||
for (size_t i = 0; i < count; i++)
|
||||
result.values[i] = addHook && i == dyld ? result.hook :
|
||||
result.values[i] = addLibraries && i == dyld ? result.hook :
|
||||
addRoot && i == jbRoot ? result.root : env[i];
|
||||
if (addHook && dyld == (size_t)-1)
|
||||
if (addLibraries && dyld == (size_t)-1)
|
||||
result.values[count++] = result.hook;
|
||||
if (addRoot && jbRoot == (size_t)-1)
|
||||
result.values[count] = result.root;
|
||||
return result;
|
||||
}
|
||||
|
||||
// The system hook alone, which is what every spawn gets.
|
||||
static VPInjectionEnvironment vpInsertHook(char *const env[], const char *root) {
|
||||
return vpInsertHooks(env, root, NULL);
|
||||
}
|
||||
|
||||
static void vpFreeEnvironment(VPInjectionEnvironment *environment) {
|
||||
free(environment->values);
|
||||
free(environment->hook);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int vpInXPCProxy;
|
||||
@@ -45,51 +46,21 @@ static int vpIsInjectionTarget(const char *path) {
|
||||
(vpInBootstrap && path[0] != '/');
|
||||
}
|
||||
|
||||
static int vpPathHasSuffix(const char *path, const char *suffix) {
|
||||
size_t length = path ? strlen(path) : 0;
|
||||
size_t want = strlen(suffix);
|
||||
return length >= want && strcmp(path + length - want, suffix) == 0;
|
||||
}
|
||||
|
||||
// The processes that evaluate a code signature or a provisioning profile, and
|
||||
// so the ones that have to agree about what device this is and what signatures
|
||||
// are acceptable. Everything else spawns without libmisfix.
|
||||
//
|
||||
// installd runs `+[MICodeSigningVerifier
|
||||
// _validateSignatureAndCopyInfoForURL:withOptions:error:]`, which
|
||||
// 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 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;
|
||||
return vpPathHasSuffix(path, "/usr/libexec/installd") ||
|
||||
vpPathHasSuffix(path, "/usr/libexec/misagent") ||
|
||||
vpPathHasSuffix(path, "/SpringBoard.app/SpringBoard");
|
||||
}
|
||||
|
||||
// Every process loads this hook, root or not, so its logs must be writable by
|
||||
// all of them. The first writer is always a root xpcproxy, and a file it
|
||||
// creates 0644 silently drops every line from a mobile process — SpringBoard
|
||||
// and every app looked as though they never carried the hook. The owner makes
|
||||
// it world-writable on each open; for anyone else fchmod fails harmlessly.
|
||||
static int vpOpenLog(const char *name) {
|
||||
char path[PATH_MAX];
|
||||
int used = snprintf(path, sizeof(path), "/var/mobile/Library/Caches/%s", name);
|
||||
int fd = used > 0 && (size_t)used < sizeof(path)
|
||||
? open(path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644)
|
||||
? open(path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0666)
|
||||
: -1;
|
||||
if (fd >= 0)
|
||||
if (fd >= 0) {
|
||||
fchmod(fd, 0666);
|
||||
return fd;
|
||||
}
|
||||
const char *home = getenv("CFFIXED_USER_HOME");
|
||||
if (!home)
|
||||
home = getenv("HOME");
|
||||
@@ -128,8 +99,7 @@ static void vpPrepareLoaderLink(const char *path) {
|
||||
// get their loader links prepared.
|
||||
static VPInjectionEnvironment vpPrepareChild(const char *path, char *const envp[], const char *kind) {
|
||||
const int misFix = vpIsMISFixTarget(path);
|
||||
VPInjectionEnvironment injected =
|
||||
vpInsertHooks(envp, getenv("VPHONE_JB_ROOT"), misFix ? VP_MIS_FIX : NULL);
|
||||
VPInjectionEnvironment injected = vpInsertHooks(envp, getenv("VPHONE_JB_ROOT"), vpMISFixFor(path));
|
||||
if (vpIsInjectionTarget(path) || misFix) {
|
||||
vpPrepareLoaderLink(path);
|
||||
char decision[80];
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "../Shared/InjectionEnvironment.h"
|
||||
#include <assert.h>
|
||||
|
||||
#define EXTRA "/usr/lib/extra.dylib"
|
||||
|
||||
static const char *value(char *const env[], const char *name) { return vpEnvValue(env, name); }
|
||||
|
||||
static size_t count(char *const env[]) {
|
||||
size_t total = 0;
|
||||
while (env[total])
|
||||
total++;
|
||||
return total;
|
||||
}
|
||||
|
||||
// A spawn with no DYLD_INSERT_LIBRARIES gets both, SystemHook first.
|
||||
static void insertsBoth(void) {
|
||||
char *env[] = {"HOME=/var/mobile", NULL};
|
||||
VPInjectionEnvironment result = vpInsertHooks(env, NULL, EXTRA);
|
||||
assert(result.values);
|
||||
assert(count(result.values) == 2);
|
||||
assert(strcmp(value(result.values, "DYLD_INSERT_LIBRARIES"), VP_SYSTEM_HOOK ":" EXTRA) == 0);
|
||||
vpFreeEnvironment(&result);
|
||||
}
|
||||
|
||||
// The environment already names SystemHook — launchd passes its own to a job
|
||||
// it starts directly — and the extra library must still be added.
|
||||
static void addsExtraBesideAnExistingHook(void) {
|
||||
char *env[] = {"DYLD_INSERT_LIBRARIES=" VP_SYSTEM_HOOK, "HOME=/var/mobile", NULL};
|
||||
VPInjectionEnvironment result = vpInsertHooks(env, NULL, EXTRA);
|
||||
assert(result.values);
|
||||
assert(count(result.values) == 2);
|
||||
assert(strcmp(value(result.values, "DYLD_INSERT_LIBRARIES"), EXTRA ":" VP_SYSTEM_HOOK) == 0);
|
||||
vpFreeEnvironment(&result);
|
||||
}
|
||||
|
||||
static void leavesACompleteEnvironmentAlone(void) {
|
||||
char *env[] = {"DYLD_INSERT_LIBRARIES=" VP_SYSTEM_HOOK ":" EXTRA, NULL};
|
||||
VPInjectionEnvironment result = vpInsertHooks(env, NULL, EXTRA);
|
||||
assert(!result.values);
|
||||
vpFreeEnvironment(&result);
|
||||
}
|
||||
|
||||
static void namesTheMISFixTargets(void) {
|
||||
assert(vpIsMISFixTarget("/usr/libexec/installd"));
|
||||
assert(vpIsMISFixTarget("/usr/libexec/misagent"));
|
||||
assert(vpIsMISFixTarget("/System/Library/CoreServices/SpringBoard.app/SpringBoard"));
|
||||
assert(!vpIsMISFixTarget("/usr/libexec/xpcproxy"));
|
||||
assert(!vpIsMISFixTarget("/usr/libexec/installdx"));
|
||||
assert(!vpIsMISFixTarget(NULL));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
insertsBoth();
|
||||
addsExtraBesideAnExistingHook();
|
||||
leavesACompleteEnvironmentAlone();
|
||||
namesTheMISFixTargets();
|
||||
puts("InjectionEnvironmentTests: ok");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user