mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
iOS 27 lsd gates app (re)registration behind -[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations], an XPC-peer entitlement check for one of three privileged entitlements (coreservices.lsaw / installcoordinationd.daemon / coreservices.can-register-install-results). Missing -> NSOSStatusErrorDomain -54 (permErr, LSDModifyService.mm:1639), so registerApplicationDictionary: and registerContainerizedApplicationWithInfoDictionaries: fail and no app can register — blocking vphoned's installer, TrollStore, and uicache/Sileo. The entitlement route is unsatisfiable: LS registration is proxied, so the peer lsd inspects is not the caller (even vphoned with all three in its validated csblob is rejected). Fix (two parts): - cfw_patch_lsd_embedded_reg.py: NOP the gate's final cbz so the check always returns YES + re-attest the 16 KiB page (TXM per-page). Wired into cfw.py (patch-lsd-embedded-reg) and cfw_install.sh (after patch-dsc-maxslide); self-gates on pre-27 userlands (method absent). Method resolved via the DSC's own .symbols in-image table (ipsw symaddr/a2s time out on this cache), gate located by control-flow shape (cbz/cbnz w0 whose fall-through sets the mov w<reg>,#1 result), NOP bytes from Keystone. New CDHash accepted by the JB always-true AMFI cdhash-trust patch. - vphoned vp_register_path: fall back from the (still-gated) plain registerApplicationDictionary: to the containerized registration API, which works once the gate is patched (treat a nil registrationError as success since it returns NO even when it registers). Verified on 17,3_27.0_24A5380h + cloudOS 26.4 (JB): -54 gone, Sileo registers (uicache -l 0->1), vphoned installs+registers a test IPA (com.vphone.vptest) to /var/containers/Bundle/Application end-to-end; clean boot (re-attest correct). Documented as item 12 in research/0_binary_patch_comparison.md. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
857 lines
36 KiB
Objective-C
857 lines
36 KiB
Objective-C
#import "vphoned_install.h"
|
|
#import "unarchive.h"
|
|
|
|
#import <Security/Security.h>
|
|
#include <dlfcn.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <mach-o/fat.h>
|
|
#include <mach-o/loader.h>
|
|
#include <spawn.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#import "vphoned_protocol.h"
|
|
|
|
typedef struct __SecCode const *SecStaticCodeRef;
|
|
typedef CF_OPTIONS(uint32_t, SecCSFlags) {
|
|
kSecCSDefaultFlags = 0
|
|
};
|
|
#define kSecCSRequirementInformation (1 << 2)
|
|
|
|
OSStatus SecStaticCodeCreateWithPathAndAttributes(
|
|
CFURLRef path,
|
|
SecCSFlags flags,
|
|
CFDictionaryRef attributes,
|
|
SecStaticCodeRef *staticCode
|
|
);
|
|
OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information);
|
|
extern CFStringRef kSecCodeInfoEntitlementsDict;
|
|
|
|
@interface LSApplicationProxy : NSObject
|
|
+ (instancetype)applicationProxyForIdentifier:(NSString *)identifier;
|
|
@property (nonatomic, readonly) NSString *bundleIdentifier;
|
|
@property (nonatomic, readonly) NSURL *bundleURL;
|
|
@property (getter=isInstalled, nonatomic, readonly) BOOL installed;
|
|
@end
|
|
|
|
@interface LSApplicationWorkspace : NSObject
|
|
+ (instancetype)defaultWorkspace;
|
|
- (BOOL)registerApplicationDictionary:(NSDictionary *)dict;
|
|
- (BOOL)registerContainerizedApplicationWithInfoDictionaries:(NSArray *)infos
|
|
operationUUID:(NSUUID *)uuid
|
|
requestContext:(id)context
|
|
saveObserver:(id)observer
|
|
registrationError:(NSError **)error;
|
|
- (BOOL)unregisterApplication:(id)arg1;
|
|
@end
|
|
|
|
@interface LSEnumerator : NSEnumerator
|
|
@property (nonatomic, copy) NSPredicate *predicate;
|
|
+ (instancetype)enumeratorForApplicationProxiesWithOptions:(NSUInteger)options;
|
|
@end
|
|
|
|
@interface MCMContainer : NSObject
|
|
+ (id)containerWithIdentifier:(id)arg1 createIfNecessary:(BOOL)arg2 existed:(BOOL *)arg3 error:(id *)arg4;
|
|
@property (nonatomic, readonly) NSURL *url;
|
|
@end
|
|
|
|
static NSString *const VPManagedMarker = @"_VPhone";
|
|
|
|
static void vp_load_private_frameworks(void) {
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
dlopen("/System/Library/PrivateFrameworks/MobileContainerManager.framework/MobileContainerManager", RTLD_NOW);
|
|
dlopen("/System/Library/Frameworks/CoreServices.framework/CoreServices", RTLD_NOW);
|
|
});
|
|
}
|
|
|
|
static NSString *vp_trimmed_output(NSString *string) {
|
|
NSString *trimmed = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
if (trimmed.length > 4000) {
|
|
return [trimmed substringToIndex:4000];
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
static NSDictionary *vp_info_dictionary_for_app_path(NSString *appPath) {
|
|
if (appPath.length == 0) return nil;
|
|
return [NSDictionary dictionaryWithContentsOfFile:[appPath stringByAppendingPathComponent:@"Info.plist"]];
|
|
}
|
|
|
|
static NSString *vp_app_id_for_app_path(NSString *appPath) {
|
|
return vp_info_dictionary_for_app_path(appPath)[@"CFBundleIdentifier"];
|
|
}
|
|
|
|
static NSString *vp_app_main_executable_path_for_app_path(NSString *appPath) {
|
|
NSDictionary *info = vp_info_dictionary_for_app_path(appPath);
|
|
NSString *executable = info[@"CFBundleExecutable"];
|
|
if (executable.length == 0) return nil;
|
|
return [appPath stringByAppendingPathComponent:executable];
|
|
}
|
|
|
|
static NSString *vp_find_app_name_in_bundle_path(NSString *bundlePath) {
|
|
NSArray<NSString *> *bundleItems = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:bundlePath error:nil];
|
|
for (NSString *bundleItem in bundleItems) {
|
|
if ([bundleItem.pathExtension isEqualToString:@"app"]) {
|
|
return bundleItem;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
static NSString *vp_find_app_path_in_bundle_path(NSString *bundlePath) {
|
|
NSString *appName = vp_find_app_name_in_bundle_path(bundlePath);
|
|
if (appName.length == 0) return nil;
|
|
return [bundlePath stringByAppendingPathComponent:appName];
|
|
}
|
|
|
|
static NSURL *vp_find_app_url_in_bundle_url(NSURL *bundleURL) {
|
|
NSString *appName = vp_find_app_name_in_bundle_path(bundleURL.path);
|
|
if (appName.length == 0) return nil;
|
|
return [bundleURL URLByAppendingPathComponent:appName];
|
|
}
|
|
|
|
static BOOL vp_is_macho_file(NSString *filePath) {
|
|
FILE *file = fopen(filePath.fileSystemRepresentation, "r");
|
|
if (!file) return NO;
|
|
|
|
uint32_t magic = 0;
|
|
fread(&magic, sizeof(uint32_t), 1, file);
|
|
fclose(file);
|
|
|
|
return magic == FAT_MAGIC || magic == FAT_CIGAM || magic == MH_MAGIC_64 || magic == MH_CIGAM_64;
|
|
}
|
|
|
|
static void vp_fix_permissions_of_app_bundle(NSString *appBundlePath) {
|
|
NSURL *fileURL = nil;
|
|
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
|
|
enumeratorAtURL:[NSURL fileURLWithPath:appBundlePath]
|
|
includingPropertiesForKeys:nil
|
|
options:0
|
|
errorHandler:nil];
|
|
while ((fileURL = [enumerator nextObject])) {
|
|
NSString *filePath = fileURL.path;
|
|
chown(filePath.fileSystemRepresentation, 33, 33);
|
|
chmod(filePath.fileSystemRepresentation, 0644);
|
|
}
|
|
|
|
enumerator = [[NSFileManager defaultManager]
|
|
enumeratorAtURL:[NSURL fileURLWithPath:appBundlePath]
|
|
includingPropertiesForKeys:nil
|
|
options:0
|
|
errorHandler:nil];
|
|
while ((fileURL = [enumerator nextObject])) {
|
|
NSString *filePath = fileURL.path;
|
|
BOOL isDir = NO;
|
|
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDir];
|
|
if (isDir || vp_is_macho_file(filePath)) {
|
|
chmod(filePath.fileSystemRepresentation, 0755);
|
|
}
|
|
}
|
|
}
|
|
|
|
static NSString *vp_read_all_from_fd(int fd) {
|
|
NSMutableData *data = [NSMutableData data];
|
|
uint8_t buf[4096];
|
|
ssize_t n = 0;
|
|
while ((n = read(fd, buf, sizeof(buf))) > 0) {
|
|
[data appendBytes:buf length:(NSUInteger)n];
|
|
}
|
|
if (data.length == 0) return @"";
|
|
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
return string ?: @"";
|
|
}
|
|
|
|
static int vp_run_process_with_output(NSString *path, NSArray<NSString *> *args, NSString **output) {
|
|
NSUInteger argc = args.count + 2;
|
|
char **argv = calloc(argc, sizeof(char *));
|
|
if (!argv) return ENOMEM;
|
|
|
|
argv[0] = strdup(path.fileSystemRepresentation);
|
|
for (NSUInteger i = 0; i < args.count; i++) {
|
|
argv[i + 1] = strdup(args[i].fileSystemRepresentation);
|
|
}
|
|
argv[argc - 1] = NULL;
|
|
|
|
int pipeFds[2] = {-1, -1};
|
|
if (pipe(pipeFds) != 0) {
|
|
for (NSUInteger i = 0; i < argc - 1; i++) free(argv[i]);
|
|
free(argv);
|
|
return errno;
|
|
}
|
|
|
|
posix_spawn_file_actions_t actions;
|
|
posix_spawn_file_actions_init(&actions);
|
|
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], STDOUT_FILENO);
|
|
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], STDERR_FILENO);
|
|
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
|
|
|
|
pid_t pid = 0;
|
|
int spawnError = posix_spawn(&pid, path.fileSystemRepresentation, &actions, NULL, argv, NULL);
|
|
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
close(pipeFds[1]);
|
|
|
|
NSString *captured = vp_read_all_from_fd(pipeFds[0]);
|
|
close(pipeFds[0]);
|
|
|
|
int status = 0;
|
|
if (spawnError == 0 && waitpid(pid, &status, 0) < 0) {
|
|
spawnError = errno;
|
|
}
|
|
|
|
for (NSUInteger i = 0; i < argc - 1; i++) free(argv[i]);
|
|
free(argv);
|
|
|
|
if (output) *output = captured;
|
|
|
|
if (spawnError != 0) return spawnError;
|
|
if (WIFEXITED(status)) return WEXITSTATUS(status);
|
|
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
|
|
return -1;
|
|
}
|
|
|
|
static NSString *vp_find_ldid_path(void) {
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
for (NSString *path in @[
|
|
@"/var/jb/usr/bin/ldid",
|
|
@"/iosbinpack64/usr/bin/ldid",
|
|
@"/usr/bin/ldid",
|
|
]) {
|
|
if ([fm isExecutableFileAtPath:path]) {
|
|
return path;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
|
|
static SecStaticCodeRef vp_get_static_code_ref(NSString *binaryPath) {
|
|
if (binaryPath.length == 0) return NULL;
|
|
|
|
CFURLRef binaryURL = CFURLCreateWithFileSystemPath(
|
|
kCFAllocatorDefault,
|
|
(__bridge CFStringRef)binaryPath,
|
|
kCFURLPOSIXPathStyle,
|
|
false
|
|
);
|
|
if (binaryURL == NULL) return NULL;
|
|
|
|
SecStaticCodeRef codeRef = NULL;
|
|
OSStatus result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef);
|
|
CFRelease(binaryURL);
|
|
if (result != errSecSuccess) {
|
|
return NULL;
|
|
}
|
|
return codeRef;
|
|
}
|
|
|
|
static NSDictionary *vp_dump_entitlements_from_binary_at_path(NSString *binaryPath) {
|
|
SecStaticCodeRef codeRef = vp_get_static_code_ref(binaryPath);
|
|
if (codeRef == NULL) return nil;
|
|
|
|
CFDictionaryRef signingInfo = NULL;
|
|
OSStatus result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo);
|
|
CFRelease(codeRef);
|
|
if (result != errSecSuccess || signingInfo == NULL) {
|
|
if (signingInfo) CFRelease(signingInfo);
|
|
return nil;
|
|
}
|
|
|
|
NSDictionary *entitlementsNSDict = nil;
|
|
CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict);
|
|
if (entitlements && CFGetTypeID(entitlements) == CFDictionaryGetTypeID()) {
|
|
entitlementsNSDict = [(__bridge NSDictionary *)entitlements copy];
|
|
}
|
|
|
|
CFRelease(signingInfo);
|
|
return entitlementsNSDict;
|
|
}
|
|
|
|
static int vp_sign_binary(
|
|
NSString *filePath,
|
|
NSDictionary *entitlements,
|
|
NSString *certPath,
|
|
NSString *ldidPath,
|
|
NSString **errorOutput
|
|
) {
|
|
if (ldidPath.length == 0) {
|
|
if (errorOutput) *errorOutput = @"ldid not found in guest or uploaded payload";
|
|
return ENOENT;
|
|
}
|
|
|
|
NSString *entitlementsPath = nil;
|
|
NSMutableArray<NSString *> *args = [NSMutableArray array];
|
|
if (entitlements) {
|
|
NSData *entitlementsXML = [NSPropertyListSerialization
|
|
dataWithPropertyList:entitlements
|
|
format:NSPropertyListXMLFormat_v1_0
|
|
options:0
|
|
error:nil];
|
|
if (entitlementsXML) {
|
|
entitlementsPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]
|
|
stringByAppendingPathExtension:@"plist"];
|
|
[entitlementsXML writeToFile:entitlementsPath atomically:NO];
|
|
[args addObject:[@"-S" stringByAppendingString:entitlementsPath]];
|
|
} else {
|
|
[args addObject:@"-S"];
|
|
}
|
|
} else {
|
|
[args addObject:@"-S"];
|
|
}
|
|
|
|
if (certPath.length > 0) {
|
|
[args addObject:@"-M"];
|
|
[args addObject:[@"-K" stringByAppendingString:certPath]];
|
|
}
|
|
|
|
[args addObject:filePath];
|
|
|
|
NSString *output = @"";
|
|
int ret = vp_run_process_with_output(ldidPath, args, &output);
|
|
if (entitlementsPath) {
|
|
[[NSFileManager defaultManager] removeItemAtPath:entitlementsPath error:nil];
|
|
}
|
|
if (errorOutput) *errorOutput = output;
|
|
return ret;
|
|
}
|
|
|
|
static int vp_sign_app(NSString *appPath, NSString *certPath, NSString *ldidPath, NSString **errorOutput) {
|
|
if (!vp_info_dictionary_for_app_path(appPath)) {
|
|
if (errorOutput) *errorOutput = @"missing app Info.plist";
|
|
return 172;
|
|
}
|
|
|
|
NSString *mainExecutablePath = vp_app_main_executable_path_for_app_path(appPath);
|
|
if (mainExecutablePath.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:mainExecutablePath]) {
|
|
if (errorOutput) *errorOutput = @"missing main executable";
|
|
return 174;
|
|
}
|
|
|
|
NSURL *fileURL = nil;
|
|
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
|
|
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
|
|
includingPropertiesForKeys:nil
|
|
options:0
|
|
errorHandler:nil];
|
|
while ((fileURL = [enumerator nextObject])) {
|
|
NSString *filePath = fileURL.path;
|
|
if (![filePath.lastPathComponent isEqualToString:@"Info.plist"]) {
|
|
continue;
|
|
}
|
|
|
|
NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
|
|
NSString *bundleId = infoDict[@"CFBundleIdentifier"];
|
|
NSString *bundleExecutable = infoDict[@"CFBundleExecutable"];
|
|
if (bundleId.length == 0 || bundleExecutable.length == 0) {
|
|
continue;
|
|
}
|
|
|
|
NSString *bundleMainExecutablePath = [[filePath stringByDeletingLastPathComponent]
|
|
stringByAppendingPathComponent:bundleExecutable];
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:bundleMainExecutablePath]) {
|
|
continue;
|
|
}
|
|
|
|
NSString *packageType = infoDict[@"CFBundlePackageType"];
|
|
if ([packageType isEqualToString:@"FMWK"]) {
|
|
continue;
|
|
}
|
|
|
|
NSMutableDictionary *entitlementsToUse = [vp_dump_entitlements_from_binary_at_path(bundleMainExecutablePath) mutableCopy];
|
|
if (!entitlementsToUse && [bundleMainExecutablePath isEqualToString:mainExecutablePath]) {
|
|
entitlementsToUse = [@{
|
|
@"application-identifier": @"TROLLTROLL.*",
|
|
@"com.apple.developer.team-identifier": @"TROLLTROLL",
|
|
@"get-task-allow": @YES,
|
|
@"keychain-access-groups": @[@"TROLLTROLL.*", @"com.apple.token"],
|
|
} mutableCopy];
|
|
}
|
|
if (!entitlementsToUse) {
|
|
entitlementsToUse = [NSMutableDictionary dictionary];
|
|
}
|
|
|
|
NSObject *containerRequired = entitlementsToUse[@"com.apple.private.security.container-required"];
|
|
BOOL shouldWriteContainerRequired = YES;
|
|
if ([containerRequired isKindOfClass:[NSString class]]) {
|
|
shouldWriteContainerRequired = NO;
|
|
} else if ([containerRequired isKindOfClass:[NSNumber class]]) {
|
|
shouldWriteContainerRequired = [(NSNumber *)containerRequired boolValue];
|
|
}
|
|
BOOL noContainer = [entitlementsToUse[@"com.apple.private.security.no-container"] respondsToSelector:@selector(boolValue)]
|
|
? [entitlementsToUse[@"com.apple.private.security.no-container"] boolValue]
|
|
: NO;
|
|
BOOL noSandbox = [entitlementsToUse[@"com.apple.private.security.no-sandbox"] respondsToSelector:@selector(boolValue)]
|
|
? [entitlementsToUse[@"com.apple.private.security.no-sandbox"] boolValue]
|
|
: NO;
|
|
if (shouldWriteContainerRequired && !noContainer && !noSandbox) {
|
|
entitlementsToUse[@"com.apple.private.security.container-required"] = bundleId;
|
|
}
|
|
entitlementsToUse[@"jb.pmap_cs_custom_trust"] = @"PMAP_CS_APP_STORE";
|
|
|
|
NSString *signOutput = @"";
|
|
int ret = vp_sign_binary(bundleMainExecutablePath, entitlementsToUse, certPath, ldidPath, &signOutput);
|
|
if (ret != 0) {
|
|
if (errorOutput) *errorOutput = signOutput;
|
|
return 173;
|
|
}
|
|
}
|
|
|
|
NSString *recursiveOutput = @"";
|
|
int recursiveRet = vp_sign_binary(appPath, nil, certPath, ldidPath, &recursiveOutput);
|
|
if (recursiveRet != 0) {
|
|
if (errorOutput) *errorOutput = recursiveOutput;
|
|
return 173;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static NSDictionary *vp_construct_groups_containers_for_entitlements(NSDictionary *entitlements, BOOL systemGroups) {
|
|
if (!entitlements) return nil;
|
|
|
|
NSString *entitlementForGroups = systemGroups
|
|
? @"com.apple.security.system-groups"
|
|
: @"com.apple.security.application-groups";
|
|
Class mcmClass = NSClassFromString(systemGroups ? @"MCMSystemDataContainer" : @"MCMSharedDataContainer");
|
|
if (!mcmClass) return nil;
|
|
|
|
NSArray *groupIDs = entitlements[entitlementForGroups];
|
|
if (![groupIDs isKindOfClass:[NSArray class]]) return nil;
|
|
|
|
NSMutableDictionary *groupContainers = [NSMutableDictionary dictionary];
|
|
for (NSString *groupID in groupIDs) {
|
|
MCMContainer *container = [mcmClass containerWithIdentifier:groupID createIfNecessary:YES existed:nil error:nil];
|
|
if (container.url.path.length > 0) {
|
|
groupContainers[groupID] = container.url.path;
|
|
}
|
|
}
|
|
return groupContainers.count > 0 ? groupContainers.copy : nil;
|
|
}
|
|
|
|
static BOOL vp_construct_containerization_for_entitlements(NSDictionary *entitlements, NSString **customContainerOut) {
|
|
NSNumber *noContainer = entitlements[@"com.apple.private.security.no-container"];
|
|
if ([noContainer isKindOfClass:[NSNumber class]] && noContainer.boolValue) {
|
|
return NO;
|
|
}
|
|
|
|
NSObject *containerRequired = entitlements[@"com.apple.private.security.container-required"];
|
|
if ([containerRequired isKindOfClass:[NSNumber class]] && ![(NSNumber *)containerRequired boolValue]) {
|
|
return NO;
|
|
}
|
|
if ([containerRequired isKindOfClass:[NSString class]]) {
|
|
*customContainerOut = (NSString *)containerRequired;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
static NSString *vp_construct_team_identifier_for_entitlements(NSDictionary *entitlements) {
|
|
NSString *teamIdentifier = entitlements[@"com.apple.developer.team-identifier"];
|
|
return [teamIdentifier isKindOfClass:[NSString class]] ? teamIdentifier : nil;
|
|
}
|
|
|
|
static NSDictionary *vp_construct_environment_variables_for_container_path(NSString *containerPath, BOOL isContainerized) {
|
|
NSString *homeDir = isContainerized ? containerPath : @"/var/mobile";
|
|
NSString *tmpDir = isContainerized ? [containerPath stringByAppendingPathComponent:@"tmp"] : @"/var/tmp";
|
|
return @{
|
|
@"CFFIXED_USER_HOME": homeDir,
|
|
@"HOME": homeDir,
|
|
@"TMPDIR": tmpDir,
|
|
};
|
|
}
|
|
|
|
static NSSet<NSString *> *vp_immutable_app_bundle_identifiers(void) {
|
|
NSMutableSet<NSString *> *systemAppIdentifiers = [NSMutableSet set];
|
|
LSEnumerator *enumerator = [LSEnumerator enumeratorForApplicationProxiesWithOptions:0];
|
|
LSApplicationProxy *appProxy = nil;
|
|
while ((appProxy = [enumerator nextObject])) {
|
|
if (appProxy.installed && ![appProxy.bundleURL.path hasPrefix:@"/private/var/containers"]) {
|
|
[systemAppIdentifiers addObject:appProxy.bundleIdentifier.lowercaseString];
|
|
}
|
|
}
|
|
return systemAppIdentifiers.copy;
|
|
}
|
|
|
|
static BOOL vp_register_path(NSString *path, BOOL unregister, BOOL forceSystem) {
|
|
if (path.length == 0) return NO;
|
|
|
|
LSApplicationWorkspace *workspace = [LSApplicationWorkspace defaultWorkspace];
|
|
if (unregister && ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
LSApplicationProxy *app = [LSApplicationProxy applicationProxyForIdentifier:path];
|
|
if (app.bundleURL.path.length > 0) {
|
|
path = app.bundleURL.path;
|
|
}
|
|
}
|
|
|
|
path = path.stringByResolvingSymlinksInPath.stringByStandardizingPath;
|
|
NSDictionary *appInfoPlist = [NSDictionary dictionaryWithContentsOfFile:[path stringByAppendingPathComponent:@"Info.plist"]];
|
|
NSString *appBundleID = appInfoPlist[@"CFBundleIdentifier"];
|
|
if (appBundleID.length == 0) return NO;
|
|
if ([vp_immutable_app_bundle_identifiers() containsObject:appBundleID.lowercaseString]) return NO;
|
|
|
|
if (!unregister) {
|
|
NSString *appExecutablePath = [path stringByAppendingPathComponent:appInfoPlist[@"CFBundleExecutable"]];
|
|
NSDictionary *entitlements = vp_dump_entitlements_from_binary_at_path(appExecutablePath);
|
|
|
|
NSString *appDataContainerID = appBundleID;
|
|
BOOL appContainerized = vp_construct_containerization_for_entitlements(entitlements ?: @{}, &appDataContainerID);
|
|
|
|
Class appDataContainerClass = NSClassFromString(@"MCMAppDataContainer");
|
|
MCMContainer *appDataContainer = [appDataContainerClass
|
|
containerWithIdentifier:appDataContainerID
|
|
createIfNecessary:YES
|
|
existed:nil
|
|
error:nil];
|
|
NSString *containerPath = appDataContainer.url.path;
|
|
|
|
BOOL isRemovableSystemApp = [[NSFileManager defaultManager]
|
|
fileExistsAtPath:[@"/System/Library/AppSignatures" stringByAppendingPathComponent:appBundleID]];
|
|
BOOL registerAsUser = [path hasPrefix:@"/var/containers"] && !isRemovableSystemApp && !forceSystem;
|
|
|
|
NSMutableDictionary *dictToRegister = [NSMutableDictionary dictionary];
|
|
if (entitlements) {
|
|
dictToRegister[@"Entitlements"] = entitlements;
|
|
}
|
|
|
|
dictToRegister[@"ApplicationType"] = registerAsUser ? @"User" : @"System";
|
|
dictToRegister[@"CFBundleIdentifier"] = appBundleID;
|
|
dictToRegister[@"CodeInfoIdentifier"] = appBundleID;
|
|
dictToRegister[@"CompatibilityState"] = @0;
|
|
dictToRegister[@"IsContainerized"] = @(appContainerized);
|
|
if (containerPath.length > 0) {
|
|
dictToRegister[@"Container"] = containerPath;
|
|
dictToRegister[@"EnvironmentVariables"] =
|
|
vp_construct_environment_variables_for_container_path(containerPath, appContainerized);
|
|
}
|
|
dictToRegister[@"IsDeletable"] = @YES;
|
|
dictToRegister[@"Path"] = path;
|
|
dictToRegister[@"SignerOrganization"] = @"Apple Inc.";
|
|
dictToRegister[@"SignatureVersion"] = @132352;
|
|
dictToRegister[@"SignerIdentity"] = @"Apple iPhone OS Application Signing";
|
|
dictToRegister[@"IsAdHocSigned"] = @YES;
|
|
dictToRegister[@"LSInstallType"] = @1;
|
|
dictToRegister[@"HasMIDBasedSINF"] = @0;
|
|
dictToRegister[@"MissingSINF"] = @0;
|
|
dictToRegister[@"FamilyID"] = @0;
|
|
dictToRegister[@"IsOnDemandInstallCapable"] = @0;
|
|
|
|
NSString *teamIdentifier = vp_construct_team_identifier_for_entitlements(entitlements ?: @{});
|
|
if (teamIdentifier.length > 0) {
|
|
dictToRegister[@"TeamIdentifier"] = teamIdentifier;
|
|
}
|
|
|
|
NSDictionary *appGroupContainers = vp_construct_groups_containers_for_entitlements(entitlements, NO);
|
|
NSDictionary *systemGroupContainers = vp_construct_groups_containers_for_entitlements(entitlements, YES);
|
|
NSMutableDictionary *groupContainers = [NSMutableDictionary dictionary];
|
|
[groupContainers addEntriesFromDictionary:appGroupContainers];
|
|
[groupContainers addEntriesFromDictionary:systemGroupContainers];
|
|
if (groupContainers.count > 0) {
|
|
if (appGroupContainers.count > 0) {
|
|
dictToRegister[@"HasAppGroupContainers"] = @YES;
|
|
}
|
|
if (systemGroupContainers.count > 0) {
|
|
dictToRegister[@"HasSystemGroupContainers"] = @YES;
|
|
}
|
|
dictToRegister[@"GroupContainers"] = groupContainers.copy;
|
|
}
|
|
|
|
NSString *pluginsPath = [path stringByAppendingPathComponent:@"PlugIns"];
|
|
NSArray<NSString *> *plugins = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pluginsPath error:nil];
|
|
NSMutableDictionary *bundlePlugins = [NSMutableDictionary dictionary];
|
|
for (NSString *pluginName in plugins) {
|
|
NSString *pluginPath = [pluginsPath stringByAppendingPathComponent:pluginName];
|
|
NSDictionary *pluginInfoPlist =
|
|
[NSDictionary dictionaryWithContentsOfFile:[pluginPath stringByAppendingPathComponent:@"Info.plist"]];
|
|
NSString *pluginBundleID = pluginInfoPlist[@"CFBundleIdentifier"];
|
|
NSString *pluginExecutable = pluginInfoPlist[@"CFBundleExecutable"];
|
|
if (pluginBundleID.length == 0 || pluginExecutable.length == 0) {
|
|
continue;
|
|
}
|
|
NSString *pluginExecutablePath = [pluginPath stringByAppendingPathComponent:pluginExecutable];
|
|
|
|
NSDictionary *pluginEntitlements = vp_dump_entitlements_from_binary_at_path(pluginExecutablePath);
|
|
NSString *pluginDataContainerID = pluginBundleID;
|
|
BOOL pluginContainerized =
|
|
vp_construct_containerization_for_entitlements(pluginEntitlements ?: @{}, &pluginDataContainerID);
|
|
|
|
Class pluginContainerClass = NSClassFromString(@"MCMPluginKitPluginDataContainer");
|
|
MCMContainer *pluginContainer = [pluginContainerClass
|
|
containerWithIdentifier:pluginDataContainerID
|
|
createIfNecessary:YES
|
|
existed:nil
|
|
error:nil];
|
|
NSString *pluginContainerPath = pluginContainer.url.path;
|
|
|
|
NSMutableDictionary *pluginDict = [NSMutableDictionary dictionary];
|
|
if (pluginEntitlements) {
|
|
pluginDict[@"Entitlements"] = pluginEntitlements;
|
|
}
|
|
pluginDict[@"ApplicationType"] = @"PluginKitPlugin";
|
|
pluginDict[@"CFBundleIdentifier"] = pluginBundleID;
|
|
pluginDict[@"CodeInfoIdentifier"] = pluginBundleID;
|
|
pluginDict[@"CompatibilityState"] = @0;
|
|
pluginDict[@"IsContainerized"] = @(pluginContainerized);
|
|
if (pluginContainerPath.length > 0) {
|
|
pluginDict[@"Container"] = pluginContainerPath;
|
|
pluginDict[@"EnvironmentVariables"] =
|
|
vp_construct_environment_variables_for_container_path(pluginContainerPath, pluginContainerized);
|
|
}
|
|
pluginDict[@"Path"] = pluginPath;
|
|
pluginDict[@"PluginOwnerBundleID"] = appBundleID;
|
|
pluginDict[@"SignerOrganization"] = @"Apple Inc.";
|
|
pluginDict[@"SignatureVersion"] = @132352;
|
|
pluginDict[@"SignerIdentity"] = @"Apple iPhone OS Application Signing";
|
|
|
|
NSString *pluginTeamIdentifier = vp_construct_team_identifier_for_entitlements(pluginEntitlements ?: @{});
|
|
if (pluginTeamIdentifier.length > 0) {
|
|
pluginDict[@"TeamIdentifier"] = pluginTeamIdentifier;
|
|
}
|
|
|
|
NSDictionary *pluginAppGroupContainers =
|
|
vp_construct_groups_containers_for_entitlements(pluginEntitlements, NO);
|
|
NSDictionary *pluginSystemGroupContainers =
|
|
vp_construct_groups_containers_for_entitlements(pluginEntitlements, YES);
|
|
NSMutableDictionary *pluginGroupContainers = [NSMutableDictionary dictionary];
|
|
[pluginGroupContainers addEntriesFromDictionary:pluginAppGroupContainers];
|
|
[pluginGroupContainers addEntriesFromDictionary:pluginSystemGroupContainers];
|
|
if (pluginGroupContainers.count > 0) {
|
|
if (pluginAppGroupContainers.count > 0) {
|
|
pluginDict[@"HasAppGroupContainers"] = @YES;
|
|
}
|
|
if (pluginSystemGroupContainers.count > 0) {
|
|
pluginDict[@"HasSystemGroupContainers"] = @YES;
|
|
}
|
|
pluginDict[@"GroupContainers"] = pluginGroupContainers.copy;
|
|
}
|
|
|
|
bundlePlugins[pluginBundleID] = pluginDict;
|
|
}
|
|
dictToRegister[@"_LSBundlePlugins"] = bundlePlugins;
|
|
|
|
if ([workspace registerApplicationDictionary:dictToRegister]) {
|
|
return YES;
|
|
}
|
|
// iOS 27+: the plain registerApplicationDictionary path is gated off in lsd
|
|
// (returns NO). Fall back to the containerized registration path, which
|
|
// works once lsd's clientIsEntitledForEmbeddedRegistrationOperations gate
|
|
// is patched (cfw_patch_lsd_embedded_reg). It returns NO even on success,
|
|
// so treat a nil registrationError as success.
|
|
SEL containerizedSel = @selector(registerContainerizedApplicationWithInfoDictionaries:operationUUID:requestContext:saveObserver:registrationError:);
|
|
if ([workspace respondsToSelector:containerizedSel]) {
|
|
NSError *regError = nil;
|
|
[workspace registerContainerizedApplicationWithInfoDictionaries:@[dictToRegister]
|
|
operationUUID:[NSUUID UUID]
|
|
requestContext:nil
|
|
saveObserver:nil
|
|
registrationError:®Error];
|
|
if (regError == nil) {
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
NSURL *url = [NSURL fileURLWithPath:path];
|
|
return [workspace unregisterApplication:url];
|
|
}
|
|
|
|
static BOOL vp_container_has_known_marker(NSString *containerPath) {
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
for (NSString *marker in @[VPManagedMarker, @"_TrollStoreLite", @"_TrollStore"]) {
|
|
if ([fm fileExistsAtPath:[containerPath stringByAppendingPathComponent:marker]]) {
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
static BOOL vp_mark_container_as_managed(NSString *containerPath) {
|
|
NSString *markerPath = [containerPath stringByAppendingPathComponent:VPManagedMarker];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:markerPath]) {
|
|
return YES;
|
|
}
|
|
return [@"" writeToFile:markerPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
}
|
|
|
|
static int vp_install_app_from_package(
|
|
NSString *appPackagePath,
|
|
BOOL forceSystem,
|
|
NSString *certPath,
|
|
NSString *ldidPath,
|
|
NSString **detailOutput
|
|
) {
|
|
NSString *appPayloadPath = [appPackagePath stringByAppendingPathComponent:@"Payload"];
|
|
NSString *appBundleToInstallPath = vp_find_app_path_in_bundle_path(appPayloadPath);
|
|
if (appBundleToInstallPath.length == 0) {
|
|
if (detailOutput) *detailOutput = @"IPA does not contain an .app payload";
|
|
return 167;
|
|
}
|
|
|
|
NSString *appId = vp_app_id_for_app_path(appBundleToInstallPath);
|
|
if (appId.length == 0) {
|
|
if (detailOutput) *detailOutput = @"missing CFBundleIdentifier";
|
|
return 176;
|
|
}
|
|
|
|
if ([vp_immutable_app_bundle_identifiers() containsObject:appId.lowercaseString]) {
|
|
if (detailOutput) *detailOutput = @"cannot overwrite immutable system app";
|
|
return 179;
|
|
}
|
|
|
|
NSString *signOutput = @"";
|
|
int signRet = vp_sign_app(appBundleToInstallPath, certPath, ldidPath, &signOutput);
|
|
if (signRet != 0) {
|
|
if (detailOutput) *detailOutput = signOutput;
|
|
return signRet;
|
|
}
|
|
|
|
Class appContainerClass = NSClassFromString(@"MCMAppContainer");
|
|
if (!appContainerClass) {
|
|
if (detailOutput) *detailOutput = @"MCMAppContainer unavailable";
|
|
return 170;
|
|
}
|
|
|
|
MCMContainer *appContainer = [appContainerClass containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil];
|
|
if (appContainer) {
|
|
NSURL *bundleContainerURL = appContainer.url;
|
|
NSURL *appBundleURL = vp_find_app_url_in_bundle_url(bundleContainerURL);
|
|
if (appBundleURL.path.length > 0 && !vp_container_has_known_marker(bundleContainerURL.path)) {
|
|
if (detailOutput) *detailOutput = @"a non-managed app with the same bundle identifier is already installed";
|
|
return 171;
|
|
}
|
|
if (appBundleURL.path.length > 0) {
|
|
[[NSFileManager defaultManager] removeItemAtURL:appBundleURL error:nil];
|
|
}
|
|
} else {
|
|
NSError *mcmError = nil;
|
|
appContainer = [appContainerClass containerWithIdentifier:appId createIfNecessary:YES existed:nil error:&mcmError];
|
|
if (!appContainer || mcmError) {
|
|
if (detailOutput) *detailOutput = mcmError.localizedDescription ?: @"failed to create app container";
|
|
return 170;
|
|
}
|
|
}
|
|
|
|
NSString *newAppBundlePath = [appContainer.url.path stringByAppendingPathComponent:appBundleToInstallPath.lastPathComponent];
|
|
NSError *copyError = nil;
|
|
if (![[NSFileManager defaultManager] copyItemAtPath:appBundleToInstallPath toPath:newAppBundlePath error:©Error]) {
|
|
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"failed to copy app bundle";
|
|
return 178;
|
|
}
|
|
|
|
if (!vp_mark_container_as_managed(appContainer.url.path)) {
|
|
if (detailOutput) *detailOutput = @"installed app but failed to write management marker";
|
|
return 177;
|
|
}
|
|
|
|
NSURL *updatedAppURL = vp_find_app_url_in_bundle_url(appContainer.url);
|
|
if (updatedAppURL.path.length == 0) {
|
|
if (detailOutput) *detailOutput = @"installed app but failed to resolve final app path";
|
|
return 178;
|
|
}
|
|
|
|
vp_fix_permissions_of_app_bundle(updatedAppURL.path);
|
|
if (!vp_register_path(updatedAppURL.path, NO, forceSystem)) {
|
|
if (detailOutput) *detailOutput = @"install copied files but LaunchServices registration failed";
|
|
return 181;
|
|
}
|
|
|
|
if (detailOutput) {
|
|
*detailOutput = [NSString stringWithFormat:@"%@ (%@)", updatedAppURL.lastPathComponent, appId];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int vp_extract_package_to_directory(
|
|
NSString *fileToExtract,
|
|
NSString *extractionPath,
|
|
NSString **detailOutput
|
|
) {
|
|
NSString *archiveError = nil;
|
|
int ret = vp_extract_archive(fileToExtract, extractionPath, &archiveError);
|
|
if (ret != 0) {
|
|
if (detailOutput) *detailOutput = archiveError ?: @"libarchive extraction failed";
|
|
return 168;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
BOOL vp_custom_installer_available(void) {
|
|
vp_load_private_frameworks();
|
|
return NSClassFromString(@"MCMAppContainer") != Nil
|
|
&& NSClassFromString(@"LSApplicationWorkspace") != Nil;
|
|
}
|
|
|
|
NSDictionary *vp_handle_custom_install(NSDictionary *msg) {
|
|
vp_load_private_frameworks();
|
|
id reqId = msg[@"id"];
|
|
NSString *ipaPath = msg[@"path"];
|
|
NSString *registration = msg[@"registration"];
|
|
NSString *certPath = msg[@"cert_path"];
|
|
NSString *ldidPath = vp_find_ldid_path();
|
|
BOOL forceSystem = [registration isEqualToString:@"System"];
|
|
|
|
if (ipaPath.length == 0) {
|
|
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
|
response[@"msg"] = @"missing ipa path";
|
|
return response;
|
|
}
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:ipaPath]) {
|
|
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
|
response[@"msg"] = [NSString stringWithFormat:@"IPA not found: %@", ipaPath];
|
|
return response;
|
|
}
|
|
if (!vp_custom_installer_available()) {
|
|
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
|
NSMutableArray<NSString *> *missing = [NSMutableArray array];
|
|
if (NSClassFromString(@"MCMAppContainer") == Nil) [missing addObject:@"MCMAppContainer"];
|
|
if (NSClassFromString(@"LSApplicationWorkspace") == Nil) [missing addObject:@"LSApplicationWorkspace"];
|
|
NSString *detail = missing.count > 0 ? [missing componentsJoinedByString:@", "] : @"unknown";
|
|
response[@"msg"] = [NSString stringWithFormat:@"Built-in IPA installer prerequisites are missing: %@", detail];
|
|
return response;
|
|
}
|
|
if (ldidPath.length == 0) {
|
|
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
|
response[@"msg"] = @"Built-in IPA installer could not find a guest-side iOS ldid.";
|
|
return response;
|
|
}
|
|
if (certPath.length > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:certPath]) {
|
|
certPath = nil;
|
|
}
|
|
|
|
NSString *tmpPackagePath = [[NSTemporaryDirectory() stringByResolvingSymlinksInPath] stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
|
if (![[NSFileManager defaultManager] createDirectoryAtPath:tmpPackagePath withIntermediateDirectories:NO attributes:nil error:nil]) {
|
|
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
|
response[@"msg"] = @"failed to create temporary extraction directory";
|
|
return response;
|
|
}
|
|
|
|
NSString *detail = @"";
|
|
int extractRet = vp_extract_package_to_directory(ipaPath, tmpPackagePath, &detail);
|
|
int installRet = 0;
|
|
if (extractRet == 0) {
|
|
installRet = vp_install_app_from_package(tmpPackagePath, forceSystem, certPath, ldidPath, &detail);
|
|
}
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil];
|
|
[[NSFileManager defaultManager] removeItemAtPath:ipaPath error:nil];
|
|
if (certPath.length > 0) {
|
|
[[NSFileManager defaultManager] removeItemAtPath:certPath error:nil];
|
|
}
|
|
if (extractRet != 0 || installRet != 0) {
|
|
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
|
int retCode = extractRet != 0 ? extractRet : installRet;
|
|
NSString *trimmed = vp_trimmed_output(detail ?: @"");
|
|
response[@"msg"] = trimmed.length > 0
|
|
? [NSString stringWithFormat:@"built-in installer failed (%d)\n%@", retCode, trimmed]
|
|
: [NSString stringWithFormat:@"built-in installer failed (%d)", retCode];
|
|
return response;
|
|
}
|
|
|
|
NSMutableDictionary *response = vp_make_response(@"ok", reqId);
|
|
response[@"msg"] = forceSystem
|
|
? [NSString stringWithFormat:@"Installed via built-in installer as System: %@", detail]
|
|
: [NSString stringWithFormat:@"Installed via built-in installer as User: %@", detail];
|
|
return response;
|
|
}
|