Skip to content

Commit bc5bbdb

Browse files
committed
Implement installd hooking dylib, automatic injection via daemon
1 parent bf152bc commit bc5bbdb

File tree

16 files changed

+675
-17
lines changed

16 files changed

+675
-17
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@import Foundation;
2+
3+
@interface AppRegistrarInstallDaemonSwizzles : NSObject
4+
5+
@end
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#import "AppRegistrarInstallDaemonSwizzles.h"
2+
3+
#import "CodesigningHelper.h"
4+
5+
#import "installd.h"
6+
#import "InstalledContentLibrary.h"
7+
8+
#import "SwizzlingHelper.h"
9+
10+
@import os.log;
11+
12+
os_log_t logger(void)
13+
{
14+
static dispatch_once_t onceToken;
15+
static os_log_t _log;
16+
dispatch_once(&onceToken, ^{
17+
_log = os_log_create("codes.rambo.research.appregistrard", "AppRegistrarInstallDaemonSwizzles");
18+
});
19+
return _log;
20+
}
21+
22+
/// Declarations for things we'll need from the runtime.
23+
@interface NSObject ()
24+
25+
@property (readonly) BOOL isPlaceholderInstall;
26+
@property (strong) MIExecutableBundle *bundle;
27+
@property (strong) MICodeSigningInfo *bundleSigningInfo;
28+
29+
- (BOOL)__original_performVerificationWithError:(NSError *__autoreleasing *)outError;
30+
31+
@end
32+
33+
@implementation AppRegistrarInstallDaemonSwizzles
34+
35+
+ (void)load
36+
{
37+
static dispatch_once_t onceToken;
38+
dispatch_once(&onceToken, ^{
39+
if (strcmp(getprogname(), "installd")) return;
40+
41+
os_log_debug(logger(), "🚀 Loaded into installd, hooking!");
42+
43+
SwizzleInstanceMethod(MIInstallableBundle, performVerificationWithError:, AppRegistrarInstallDaemonSwizzles);
44+
SwizzleInstanceMethod(MIDaemonConfiguration, codeSigningEnforcementIsDisabled, AppRegistrarInstallDaemonSwizzles);
45+
});
46+
}
47+
48+
/**
49+
This is the swizzle for the method that verifies the code signature and certain special entitlements on bundles being installed.
50+
51+
It used to be done in `MICodeSigningVerifier` before iOS 18.4, but since then this method in `MIInstallableBundle`
52+
is what does the heavy lifting and `MICodeSigningVerifier` has lost a lot of its importance.
53+
54+
Since this cryptex technically only supports iOS 26+, only this override is left, and it has been reimplemented
55+
to completely fake the verification by extracting the code signature information using the most lenient validation possible,
56+
which skips a bunch of the special entitlement checks performed by the original implementation.
57+
58+
The most important step is actually setting `bundleSigningInfo` on the `MIInstallableBundle` to the signing info
59+
extracted from `MIExecutableBundle` by calling `codeSigningInfoByValidatingResources:...`.
60+
61+
The presence of that signing info and the return value of `YES` is what concludes the verification successfully.
62+
63+
Of course none of that would work before first performing the TSS signing and trust cache load for all executables present within the bundle,
64+
but even then the original implementation has some annoying validations that are skipped by doing the above.
65+
66+
To get ad-hoc binaries to pass validation, it's also crucial to override `-[MIDaemonConfiguration codeSigningEnforcementIsDisabled]` so that it returns `YES`, which this class also does.
67+
*/
68+
- (BOOL)__override_performVerificationWithError:(NSError *__autoreleasing *)outError
69+
{
70+
os_log_t log = logger();
71+
72+
BOOL isPlaceholder = self.isPlaceholderInstall;
73+
NSString *logFunctionName = [NSString stringWithFormat:@"%@%@", NSStringFromSelector(_cmd), (isPlaceholder ? @"[placeholder bundle]" : @"")];
74+
75+
MIExecutableBundle *bundle = self.bundle;
76+
77+
os_log(log, "%{public}@ called with bundle %@", logFunctionName, self.bundle);
78+
79+
NSError *myError;
80+
BOOL result;
81+
82+
if (isPlaceholder) {
83+
os_log_debug(log, "Bundle is placeholder install, delegating to original performVerificationWithError implementation");
84+
result = [self __original_performVerificationWithError:&myError];
85+
} else {
86+
os_log_debug(log, "Bundle is real install, will perform lenient verification.");
87+
88+
NSString *bundleName = bundle.bundleURL.lastPathComponent;
89+
90+
os_log_debug(log, "Checking if %@ is adhoc-signed...", bundleName);
91+
92+
BOOL isAdHoc = [CodeSigningHelper isAdHocSignedBundleAtURL:bundle.bundleURL];
93+
94+
if (!isAdHoc) {
95+
os_log(log, "%@ is not adhoc-signed, proceeding with regular installd flow...", bundleName);
96+
return [self __original_performVerificationWithError:outError];
97+
}
98+
99+
if (!bundle) {
100+
os_log_fault(log, "MIInstallableBundle has no bundle!");
101+
myError = [NSError errorWithDomain:@"codes.rambo.appregistrard" code:0 userInfo:@{NSLocalizedFailureReasonErrorKey: @"MIInstallableBundle has no bundle."}];
102+
result = NO;
103+
} else {
104+
/// Create signing info with most lenient validation possible.
105+
/// Regular verification by original implementation would work for most cases, but doing this allows us to skip some annoying checks
106+
/// such as checks for forbidden entitlement combinations that are only checked during installation.
107+
MICodeSigningInfo *signingInfo = [bundle codeSigningInfoByValidatingResources:NO
108+
performingOnlineAuthorization:NO
109+
ignoringCachedSigningInfo:NO
110+
checkingTrustCacheIfApplicable:NO
111+
skippingProfileIDValidation:YES
112+
error:&myError];
113+
114+
/// This is the crucial step: we must set the signing info on the installable bundle,
115+
/// otherwise installation will fail even if we return a success response from this method.
116+
self.bundleSigningInfo = signingInfo;
117+
118+
if (signingInfo) {
119+
os_log(log, "Successfully obtained signing info for bundle - %{public}@", signingInfo.dictionaryRepresentation);
120+
result = YES;
121+
} else {
122+
os_log_error(log, "Error obtaining signing info for bundle - %{public}@", myError);
123+
result = NO;
124+
}
125+
}
126+
}
127+
128+
if (result) {
129+
os_log(log, "%{public}@ OK", logFunctionName);
130+
return YES;
131+
} else {
132+
os_log_error(log, "%{public}@ FAILED: %{public}@", logFunctionName, myError);
133+
if (outError) *outError = myError;
134+
return NO;
135+
}
136+
}
137+
138+
/// This override is on `MIDaemonConfiguration`, it's required so that ad-hoc signed binaries are accepted.
139+
- (BOOL)__override_codeSigningEnforcementIsDisabled
140+
{
141+
return YES;
142+
}
143+
144+
@end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@import Foundation;
2+
3+
@interface CodeSigningHelper : NSObject
4+
5+
+ (BOOL)isAdHocSignedBundleAtURL:(NSURL *)bundleURL;
6+
+ (BOOL)isAdHocSignedExecutableAtURL:(NSURL *)executableURL;
7+
8+
@end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#import "CodesigningHelper.h"
2+
3+
@import os.log;
4+
5+
#import "SecuritySPI.h"
6+
7+
os_log_t csLogger(void)
8+
{
9+
static dispatch_once_t onceToken;
10+
static os_log_t _log;
11+
dispatch_once(&onceToken, ^{
12+
_log = os_log_create("codes.rambo.research.appregistrard", "CodeSigningHelper");
13+
});
14+
return _log;
15+
}
16+
17+
@implementation CodeSigningHelper
18+
19+
+ (BOOL)isAdHocSignedBundleAtURL:(NSURL *)bundleURL
20+
{
21+
os_log_t log = csLogger();
22+
23+
os_log_debug(log, "Checking adhoc status for bundle: %@", bundleURL.lastPathComponent);
24+
25+
NSBundle *bundle = [NSBundle bundleWithURL:bundleURL];
26+
if (!bundle) {
27+
os_log_error(log, "Failed to construct NSBundle with bundle at %@", bundleURL.path);
28+
return NO;
29+
}
30+
31+
NSURL *effectiveExecutableURL = nil;
32+
if (bundle.executableURL) {
33+
effectiveExecutableURL = bundle.executableURL;
34+
} else {
35+
effectiveExecutableURL = [bundleURL URLByAppendingPathComponent:bundleURL.URLByDeletingPathExtension.lastPathComponent];
36+
os_log_error(log, "[WARN] Bundle has no executable URL, using default: %@", effectiveExecutableURL.path);
37+
}
38+
39+
return [self isAdHocSignedExecutableAtURL:effectiveExecutableURL];
40+
}
41+
42+
+ (BOOL)isAdHocSignedExecutableAtURL:(NSURL *)executableURL
43+
{
44+
os_log_t log = csLogger();
45+
46+
NSString *name = executableURL.lastPathComponent;
47+
48+
os_log_debug(log, "Checking adhoc status for executable: %@", executableURL.lastPathComponent);
49+
50+
SecStaticCodeRef code;
51+
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)executableURL, kSecCSDefaultFlags, &code);
52+
if (status != errSecSuccess) {
53+
os_log_error(log, "SecStaticCodeCreateWithPath failed with %{public}d (%{public}@)", status, SecCopyErrorMessageString(status, NULL));
54+
return NO;
55+
}
56+
57+
os_log_debug(log, "Successfully obtained static code reference for %@", name);
58+
59+
CFDictionaryRef signingInfo;
60+
status = SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &signingInfo);
61+
if (status != errSecSuccess) {
62+
os_log_error(log, "SecCodeCopySigningInformation failed with %{public}d (%{public}@)", status, SecCopyErrorMessageString(status, NULL));
63+
return NO;
64+
}
65+
66+
os_log_debug(log, "Successfully obtained signing info for %@", name);
67+
68+
NSDictionary <NSString *, id> *infoDict = (__bridge NSDictionary *)signingInfo;
69+
NSNumber *flags = infoDict[(__bridge NSString *)kSecCodeInfoFlags];
70+
if (!flags) {
71+
os_log_error(log, "Signing info for %@ is missing kSecCodeInfoFlags", name);
72+
return NO;
73+
}
74+
75+
BOOL isAdHoc = (flags.unsignedIntValue & kSecCodeSignatureAdhoc) != 0;
76+
77+
os_log(log, "%@ is adhoc? %{public}@", name, ((isAdHoc) ? @"YES" : @"NO"));
78+
79+
return isAdHoc;
80+
}
81+
82+
@end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@import Foundation;
2+
3+
@interface MIBundle : NSObject
4+
5+
@property (readonly) NSURL *bundleURL;
6+
@property (readonly) NSString *identifier;
7+
@property (readonly) BOOL isPlaceholder;
8+
9+
@end
10+
11+
@interface MIExecutableBundle : MIBundle
12+
13+
@property (readonly) NSURL *executableURL;
14+
15+
- (MICodeSigningInfo *)codeSigningInfoByValidatingResources:(BOOL)resources
16+
performingOnlineAuthorization:(BOOL)authorization
17+
ignoringCachedSigningInfo:(BOOL)info
18+
checkingTrustCacheIfApplicable:(BOOL)applicable
19+
skippingProfileIDValidation:(BOOL)idvalidation
20+
error:(NSError **)outError;
21+
22+
@end
23+
24+
@interface MIDaemonConfiguration : NSObject
25+
26+
- (BOOL)codeSigningEnforcementIsDisabled;
27+
28+
@end
29+
30+
@interface MICodeSigningInfo : NSObject
31+
32+
@property (readonly, copy, nonatomic) NSDictionary *dictionaryRepresentation;
33+
34+
@end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma clang diagnostic ignored "-Wincomplete-umbrella"
2+
3+
@import Foundation;
4+
@import Security;
5+
6+
#import <TargetConditionals.h>
7+
8+
#if TARGET_OS_IOS
9+
10+
extern const CFStringRef _Nonnull kSecCodeInfoFlags;
11+
12+
typedef CF_OPTIONS(uint32_t, SecCodeSignatureFlags) {
13+
kSecCodeSignatureAdhoc = 0x0002,
14+
};
15+
16+
typedef CF_OPTIONS(uint32_t, SecCSFlags) {
17+
kSecCSDefaultFlags = 0,
18+
};
19+
20+
typedef struct CF_BRIDGED_TYPE(id) __SecCode const *SecStaticCodeRef;
21+
22+
OSStatus SecStaticCodeCreateWithPath(CFURLRef __nonnull path, SecCSFlags flags, SecStaticCodeRef * __nonnull CF_RETURNS_RETAINED staticCode);
23+
24+
OSStatus SecCodeCopySigningInformation(SecStaticCodeRef __nonnull code, SecCSFlags flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
25+
26+
#endif
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@import Foundation;
2+
3+
@class MIExecutableBundle, MICodeSigningInfo;
4+
5+
@interface MIInstallable : NSObject
6+
7+
- (BOOL)performVerificationWithError:(NSError **)outError;
8+
9+
@end
10+
11+
@interface MIInstallableBundle : MIInstallable
12+
13+
@property (readonly) MIExecutableBundle *bundle;
14+
@property (readonly) BOOL isPlaceholderInstall;
15+
@property (strong) MICodeSigningInfo *bundleSigningInfo;
16+
17+
@end

AppRegistrarHooks/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# libAppRegistrarHooks
2+
3+
This dynamic library is injected into `installd` on SRD to allow ad-hoc binaries to pass its code signature validation checks.
4+
5+
With this library injected, `appregistrard` can use the InstallCoordination-based installation method, which installs
6+
apps exactly the same way as those installed via Xcode or the App Store, ensuring that all app extension types work as expected.
7+
8+
The daemon will automatically set up injection by copying this dylib into a place that `installd` can load from
9+
and setting the `DYLD_INSERT_LIBRARIES` environment via `launchd` to include it. The library does nothing unless loaded into `installd`.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@import Foundation;
2+
3+
NS_ASSUME_NONNULL_BEGIN
4+
5+
#define SwizzleInstanceMethod( className, methodName, overridingType ) \
6+
[SwizzlingHelper swizzleClass:NSClassFromString(@#className) method:NSSelectorFromString(@#methodName) overridingClass:[overridingType class] isClassMethod:NO];
7+
8+
#define SwizzleClassMethod( className, methodName, overridingType ) \
9+
[SwizzlingHelper swizzleClass:NSClassFromString(@#className) method:NSSelectorFromString(@#methodName) overridingClass:[overridingType class] isClassMethod:YES];
10+
11+
@interface SwizzlingHelper : NSObject
12+
13+
/// Swizzles the method on the provided class with an instance method named
14+
/// according to the pattern in the override class.
15+
/// The pattern is as follows:
16+
/// Let's say the original selector is `removeObjectAtIndex:`
17+
/// The override class should have a method with the selector `__override_removeObjectAtIndex:`
18+
/// Swizzling also introduces the original method's implementation that can be called from the override,
19+
/// in this example it would be `__original_removeObjectAtIndex:`.
20+
/// The override and original methods will be either instance or class methods depending upon the `isClassMethod` argument.
21+
+ (BOOL)swizzleClass:(Class)aClass
22+
method:(SEL)methodSelector
23+
overridingClass:(Class)overrideClass
24+
isClassMethod:(BOOL)isClassMethod;
25+
26+
@end
27+
28+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)