Skip to content

Commit

Permalink
Fix JIT-less code sign error due to using wrong keychan group
Browse files Browse the repository at this point in the history
For the whole time, LiveContainer has been thinking bundle ID = application ID, leading to chaos of codesign errors. As the root cause has been found, this issue should no longer occur.
Additionally, a validation stage has been added to JIT-less setup, ensuring the signed library loads (passes amfid)

Added some text to Settings
  • Loading branch information
khanhduytran0 committed Aug 8, 2024
1 parent ff8b71d commit 4b79b39
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 93 deletions.
48 changes: 46 additions & 2 deletions LCJITLessSetupViewController.m
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import Darwin;
#import "LCJITLessSetupViewController.h"
#import "LCUtils.h"
#import "UIKitPrivate.h"
Expand All @@ -15,6 +16,7 @@ - (void)showDialogTitle:(NSString *)title message:(NSString *)message handler:(v

- (void)loadView {
[super loadView];

self.view.backgroundColor = UIColor.systemBackgroundColor;
self.title = @"LiveContainer JIT-less setup";

Expand All @@ -25,7 +27,7 @@ - (void)loadView {
*/
NSData *certData = [LCUtils keychainItem:@"signingCertificate" ofStore:@"com.SideStore.SideStore"];
if (!certData) {
[self showDialogTitle:@"Error" message:@"Failed to find certificate data" handler:nil];
[self showDialogTitle:@"Error" message:@"Failed to find certificate data. Refresh app in SideStore and try again." handler:nil];
return;
}
LCUtils.certificateData = certData;
Expand All @@ -37,13 +39,55 @@ - (void)loadView {
}
LCUtils.certificatePassword = [NSString stringWithUTF8String:certPassword.bytes];

// Verify that the certificate is usable
// Create a test app bundle
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"CertificateValidation"];
[NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
NSString *tmpExecPath = [path stringByAppendingPathComponent:@"LiveContainer.tmp"];
NSString *tmpLibPath = [path stringByAppendingPathComponent:@"TestJITLess.dylib"];
NSString *tmpInfoPath = [path stringByAppendingPathComponent:@"Info.plist"];
[NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil];
[NSFileManager.defaultManager copyItemAtPath:[NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"Frameworks/TestJITLess.dylib"] toPath:tmpLibPath error:nil];
NSMutableDictionary *info = NSBundle.mainBundle.infoDictionary.mutableCopy;
info[@"CFBundleExecutable"] = @"LiveContainer.tmp";
[info writeToFile:tmpInfoPath atomically:YES];

// Sign the test app bundle
[LCUtils signAppBundle:[NSURL fileURLWithPath:path]
completionHandler:^(BOOL success, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!success) {
[self showDialogTitle:@"Error while performing signing test" message:error.localizedDescription handler:nil];
} else {
// Attempt to load the signed library
void *handle = dlopen(tmpLibPath.UTF8String, RTLD_LAZY);
[self validateSigningTest:(handle != NULL)];
}
});
}];
}

- (void)validateSigningTest:(BOOL)loaded {
BOOL success = loaded && getenv("LC_JITLESS_TEST_LOADED");

NSError *error;
NSURL *url = [LCUtils archiveIPAWithSetupMode:NO error:&error];
NSURL *url = [LCUtils archiveIPAWithSetupMode:!success error:&error];
if (!url) {
[self showDialogTitle:@"Error" message:error.localizedDescription handler:nil];
return;
}

if (!success) {
[self showDialogTitle:@"Error" message:@"The test library has failed to load. This means your certificate may be having issue. LiveContainer will try to repair it. SideStore will refresh LiveContainer and then you will try again. Press OK to continue."
handler:^(UIAlertAction * action) {
// Erase signingCertificate
[LCUtils deleteKeychainItem:@"signingCertificate" ofStore:@"com.rileytestut.AltStore"];
[LCUtils deleteKeychainItem:@"signingCertificate" ofStore:@"com.SideStore.SideStore"];
[UIApplication.sharedApplication openURL:[NSURL URLWithString:[NSString stringWithFormat:@"sidestore://install?url=%@", url]] options:@{} completionHandler:nil];
}];
return;
}

[self showDialogTitle:@"Instruction" message:@"Done. Press OK to finish setting up."
handler:^(UIAlertAction * action) {
[UIApplication.sharedApplication openURL:[NSURL URLWithString:[NSString stringWithFormat:@"sidestore://install?url=%@", url]] options:@{} completionHandler:nil];
Expand Down
12 changes: 9 additions & 3 deletions LCTweakListViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ @interface LCTweakListViewController()

@implementation LCTweakListViewController

- (instancetype)init {
return self = [super initWithStyle:UITableViewStyleGrouped];
}

- (void)loadView {
[super loadView];

Expand Down Expand Up @@ -171,11 +175,13 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}

/*
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
return @"N items";
if (self.navigationController.viewControllers.firstObject == self) {
return @"This is the global folder. All tweaks put here will be injected to all guest apps. Create a new folder if you use app-specific tweaks.";
} else {
return @"This is the app-specific folder. Set the tweak folder and the guest app will pick them up recursively.";
}
}
*/

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.objects.count;
Expand Down
1 change: 1 addition & 0 deletions LCUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header);
+ (NSString *)certificatePassword;
+ (void)setCertificateData:(NSData *)data;
+ (void)setCertificatePassword:(NSString *)password;
+ (BOOL)deleteKeychainItem:(NSString *)key ofStore:(NSString *)store;
+ (NSData *)keychainItem:(NSString *)key ofStore:(NSString *)store;

+ (BOOL)launchToGuestApp;
Expand Down
133 changes: 55 additions & 78 deletions LCUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ + (NSString *)appGroupPath {
return [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:self.appGroupID].path;
}

+ (BOOL)deleteKeychainItem:(NSString *)key ofStore:(NSString *)store {
NSDictionary *dict = @{
(id)kSecClass: (id)kSecClassGenericPassword,
(id)kSecAttrService: store,
(id)kSecAttrAccount: key,
(id)kSecAttrSynchronizable: (id)kSecAttrSynchronizableAny
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dict);
return status == errSecSuccess;
}

+ (NSData *)keychainItem:(NSString *)key ofStore:(NSString *)store {
NSDictionary *dict = @{
(id)kSecClass: (id)kSecClassGenericPassword,
Expand Down Expand Up @@ -78,6 +89,24 @@ + (BOOL)launchToGuestAppWithURL:(NSURL *)url {

#pragma mark Code signing

+ (void)loadStoreFrameworksWithError:(NSError **)error {
NSArray *signerFrameworks = @[@"OpenSSL.framework", @"Roxas.framework", @"AltStoreCore.framework"];
NSURL *storeFrameworksPath = [self.storeBundlePath URLByAppendingPathComponent:@"Frameworks"];
for (NSString *framework in signerFrameworks) {
NSBundle *frameworkBundle = [NSBundle bundleWithURL:[storeFrameworksPath URLByAppendingPathComponent:framework]];
if (!frameworkBundle) {
//completionHandler(NO, error);
abort();
}
[frameworkBundle loadAndReturnError:error];
}
}

+ (NSURL *)storeBundlePath {
NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:self.appGroupID];
return [appGroupPath URLByAppendingPathComponent:@"Apps/com.SideStore.SideStore/App.app"];
}

+ (void)removeCodeSignatureFromBundleURL:(NSURL *)appURL {
int32_t cpusubtype;
sysctlbyname("hw.cpusubtype", &cpusubtype, NULL, NULL, 0);
Expand All @@ -103,65 +132,24 @@ + (void)removeCodeSignatureFromBundleURL:(NSURL *)appURL {
continue;
}

// We cannot use NSMutableData as it copies the whole file data instead of directly mapping
int fd = open(fileURL.path.UTF8String, O_RDWR, (mode_t)0600);
struct stat s;
fstat(fd, &s);
void *map = mmap(NULL, s.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

uint32_t magic = *(uint32_t *)map;
struct mach_header_64 *header = NULL;

if (magic == FAT_CIGAM) {
// Find compatible slice
struct fat_header *fatHeader = (struct fat_header *)map;
struct fat_arch *arch = (struct fat_arch *)(map + sizeof(struct fat_header));
struct fat_arch *chosenArch = NULL;
for (int i = 0; i < OSSwapInt32(fatHeader->nfat_arch); i++) {
if (OSSwapInt32(arch->cputype) == CPU_TYPE_ARM64) {
header = (struct mach_header_64 *)(map + OSSwapInt32(arch->offset));
chosenArch = arch;
if (OSSwapInt32(arch->cpusubtype) == cpusubtype) {
break;
}
// Remove LC_CODE_SIGNATURE
NSString *error = LCParseMachO(fileURL.path.UTF8String, ^(const char *path, struct mach_header_64 *header) {
uint8_t *imageHeaderPtr = (uint8_t *)header + sizeof(struct mach_header_64);
struct load_command *command = (struct load_command *)imageHeaderPtr;
for(int i = 0; i < header->ncmds > 0; i++) {
if (command->cmd == LC_CODE_SIGNATURE) {
struct linkedit_data_command *csCommand = (struct linkedit_data_command *)command;
void *csData = (void *)((uint8_t *)header + csCommand->dataoff);
// Nuke it.
NSLog(@"Removing code signature of %@", fileURL);
bzero(csData, csCommand->datasize);
break;
}
arch = (struct fat_arch *)((void *)arch + sizeof(struct fat_arch));
}
if (header) {
// Extract slice
uint32_t offset = OSSwapInt32(chosenArch->offset);
uint32_t size = OSSwapInt32(chosenArch->size);
memmove(map, (void *)((uint64_t)map + offset), size);
msync(map, size, MS_SYNC);
ftruncate(fd, size);
fstat(fd, &s);
header = (struct mach_header_64 *)map;
command = (struct load_command *)((void *)command + command->cmdsize);
}
} else if (magic == MH_MAGIC_64) {
header = (struct mach_header_64 *)map;
}

if (!header || header->cputype != CPU_TYPE_ARM64 || header->filetype != MH_DYLIB) {
munmap(map, s.st_size);
close(fd);
continue;
}

uint8_t *imageHeaderPtr = (uint8_t *)header + sizeof(struct mach_header_64);
struct load_command *command = (struct load_command *)imageHeaderPtr;
for(int i = 0; i < header->ncmds > 0; i++) {
if (command->cmd == LC_CODE_SIGNATURE) {
struct linkedit_data_command *csCommand = (struct linkedit_data_command *)command;
void *csData = (void *)((uint8_t *)header + csCommand->dataoff);
// Nuke it.
NSLog(@"Removing code signature of %@", fileURL);
bzero(csData, csCommand->datasize);
msync(map, s.st_size, MS_SYNC);
munmap(map, s.st_size);
close(fd);
break;
}
command = (struct load_command *)((void *)command + command->cmdsize);
});
if (error) {
NSLog(@"[Error] %@ (%@)", error, fileURL);
}
}
}
Expand All @@ -171,26 +159,10 @@ + (NSProgress *)signAppBundle:(NSURL *)path completionHandler:(void (^)(BOOL suc

// I'm too lazy to reimplement signer, so let's borrow everything from SideStore
// For sure this will break in the future as SideStore team planned to rewrite it
NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:self.appGroupID];
NSURL *storeBundlePath = [appGroupPath URLByAppendingPathComponent:@"Apps/com.SideStore.SideStore/App.app"];
NSURL *storeFrameworksPath = [storeBundlePath URLByAppendingPathComponent:@"Frameworks"];
NSURL *profilePath = [NSBundle.mainBundle URLForResource:@"embedded" withExtension:@"mobileprovision"];

// Load libraries from Documents, yeah
NSArray *signerFrameworks = @[@"OpenSSL.framework", @"Roxas.framework", @"AltStoreCore.framework"];
for (NSString *framework in signerFrameworks) {
NSBundle *frameworkBundle = [NSBundle bundleWithURL:[storeFrameworksPath URLByAppendingPathComponent:framework]];
if (!frameworkBundle) {
//completionHandler(NO, error);
abort();
return nil;
}
[frameworkBundle loadAndReturnError:&error];
if (error) {
completionHandler(NO, error);
return nil;
}
}
[self loadStoreFrameworksWithError:&error];

ALTCertificate *cert = [[NSClassFromString(@"ALTCertificate") alloc] initWithP12Data:self.certificateData password:self.certificatePassword];
ALTProvisioningProfile *profile = [[NSClassFromString(@"ALTProvisioningProfile") alloc] initWithURL:profilePath];
Expand All @@ -210,9 +182,7 @@ + (NSString *)appGroupID {

+ (BOOL)isAppGroupSideStore {
if (![self.appGroupID containsString:@"com.SideStore.SideStore"]) return NO;
NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:self.appGroupID];
NSURL *storeBundlePath = [appGroupPath URLByAppendingPathComponent:@"Apps/com.SideStore.SideStore/App.app"];
return [NSFileManager.defaultManager fileExistsAtPath:storeBundlePath.path];
return [NSFileManager.defaultManager fileExistsAtPath:self.storeBundlePath.path];
}

+ (void)changeMainExecutableTo:(NSString *)exec error:(NSError **)error {
Expand All @@ -231,12 +201,19 @@ + (void)writeStoreIDToSetupExecutableWithError:(NSError **)error {
NSMutableData *data = [NSMutableData dataWithContentsOfURL:execPath options:0 error:error];
if (!data) return;

// We must get SideStore's exact application-identifier, otherwise JIT-less setup will bug out to hell for using the wrong, expired certificate
[self loadStoreFrameworksWithError:nil];
NSURL *profilePath = [self.storeBundlePath URLByAppendingPathComponent:@"embedded.mobileprovision"];
ALTProvisioningProfile *profile = [[NSClassFromString(@"ALTProvisioningProfile") alloc] initWithURL:profilePath];
NSString *storeKeychainID = profile.entitlements[@"application-identifier"];
assert(storeKeychainID);

NSData *findPattern = [@"KeychainAccessGroupWillBeWrittenByLiveContainerAAAAAAAAAAAAAAAAAAAA</string>" dataUsingEncoding:NSUTF8StringEncoding];
NSRange range = [data rangeOfData:findPattern options:0 range:NSMakeRange(0, data.length)];
if (range.location == NSNotFound) return;

memset((char *)data.mutableBytes + range.location, ' ', range.length);
NSString *replacement = [NSString stringWithFormat:@"%@</string>", self.appGroupID];
NSString *replacement = [NSString stringWithFormat:@"%@</string>", storeKeychainID];
assert(replacement.length < range.length);
memcpy((char *)data.mutableBytes + range.location, replacement.UTF8String, replacement.length);
[data writeToURL:execPath options:0 error:error];
Expand Down
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ LiveContainerUI_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks

# Build the tweak loader
TweakLoader_FILES = TweakLoader.m
TweakLoader_CFLAGS = \
-fobjc-arc
TweakLoader_FRAMEWORKS = Foundation
TweakLoader_CFLAGS = -fobjc-arc
TweakLoader_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks

LIBRARY_NAME = LiveContainerUI TweakLoader
# Build the test library
TestJITLess_FILES = TestJITLess.m
TestJITLess_CFLAGS = -fobjc-arc
TestJITLess_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks

LIBRARY_NAME = LiveContainerUI TweakLoader TestJITLess
include $(THEOS_MAKE_PATH)/library.mk

# Build the app
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ To install tweaks, you can use the built-in tweak manager in LiveContainer, whic
- File picker might be broken for unknown reasons.

## TODO
- Auto lock orientation
- Simulate App Group(?)
- More(?)
- Isolate Keychain per app
- Use Ch0ma instead of custom MachO parser

## License
[Apache License 2.0](https://github.com/khanhduytran0/LiveContainer/blob/main/LICENSE)
Expand Down
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.1</string>
<string>2.1.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
Expand All @@ -61,7 +61,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>2.1</string>
<string>2.1.1</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>sidestore</string>
Expand Down
4 changes: 4 additions & 0 deletions Resources/Root.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
<string>PSGroupCell</string>
<key>label</key>
<string>Miscellaneous</string>
<key>footerText</key>
<string>Frame shortcut icons with LiveContainer icon.</string>
</dict>
<dict>
<key>cell</key>
Expand Down Expand Up @@ -81,6 +83,8 @@
<string>PSGroupCell</string>
<key>label</key>
<string>About me</string>
<key>footerText</key>
<string>Don't use LiveContainer for piracy.</string>
</dict>
<dict>
<key>action</key>
Expand Down
7 changes: 7 additions & 0 deletions TestJITLess.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import Foundation;

__attribute__((constructor))
static void TestJITLessConstructor() {
NSLog(@"JIT-less test succeed");
setenv("LC_JITLESS_TEST_LOADED", "1", 1);
}
2 changes: 1 addition & 1 deletion control
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: com.kdt.livecontainer
Name: livecontainer
Version: 2.1
Version: 2.1.1
Architecture: iphoneos-arm
Description: Run iOS app without actually installing it!
Maintainer: khanhduytran0
Expand Down

0 comments on commit 4b79b39

Please sign in to comment.