Fix first launch and package hook as a signed framework

This commit is contained in:
admin committed 2026-11-12 03:27:18 +08:00
1 parent c1d0293404
commit 97a4fad71f
6 files changed
+147 -20

No files matched your search

Binary file not shown.
+17 -6
View File
@@ -15,7 +15,7 @@
- 源码 / Source:[source/](source/)(iOS 27 Hook、内置文件浏览器集成与构建脚本 / iOS 27 hook, file-browser integration and build scripts) - 源码 / Source:[source/](source/)(iOS 27 Hook、内置文件浏览器集成与构建脚本 / iOS 27 hook, file-browser integration and build scripts)
```text ```text
eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa 3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa
``` ```
## 功能 / Features ## 功能 / Features
@@ -56,14 +56,25 @@ eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iO
**中文:** **中文:**
1. 使用 AltStore、SideStore 或其他签名工具安装 `AppsManager_iOS27.ipa`。 1. 使用 AltStore、SideStore 或其他签名工具安装 `AppsManager_iOS27.ipa`。
2. 当前 IPA 使用自用证书签名;其他设备安装时请使用自己的证书和描述文件重签。 2. 当前 IPA 使用自用证书签名。其他设备请使用自己的证书和描述文件重签;描述文件必须包含 **App Group**,且签名工具需要递归签名 `Frameworks`。
3. 首次启动会执行数据准备,等待进度条完成后再进入应用列表。 3. 首次启动会执行数据准备,等待进度完成后再进入应用列表。
**English:** **English:**
1. Install `AppsManager_iOS27.ipa` with AltStore, SideStore, or another signing tool. 1. Install `AppsManager_iOS27.ipa` with AltStore, SideStore, or another signing tool.
2. This IPA is signed with a personal certificate. Re-sign it with your own certificate and provisioning profile for other devices. 2. This IPA is signed with a personal certificate. Re-sign it with your own certificate and provisioning profile; the profile must include an **App Group**, and the tool must sign `Frameworks` recursively.
3. The first launch prepares app data; wait for the progress bar to finish before using the app list. 3. The first launch prepares app data; wait for the progress to finish before using the app list.
## 更新说明 / Updates
- 修复首次启动空白、一次性「开发者提示」遮挡列表的问题。
Fixed the blank first launch and the one-time developer alert covering the list.
- iOS 27 Hook 已移入 `Frameworks/AppsManagerHook.framework`,标准签名工具重签时会把它作为嵌套代码一并签名。
The iOS 27 hook now lives in `Frameworks/AppsManagerHook.framework`, so standard signing tools re-sign it as nested code.
- 备份、还原、抹掉后会重新计算对应 App 的数据大小。
The affected app's data size is recalculated after backup, restore, and wipe.
- 重签时必须使用包含 App Group 的证书和描述文件。
Re-signing requires a certificate and provisioning profile with an App Group entitlement.
## 说明与限制 / Notes & Limitations ## 说明与限制 / Notes & Limitations
@@ -83,5 +94,5 @@ eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iO
## 校验 / Checksum ## 校验 / Checksum
```text ```text
eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa 3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa
``` ```
+1 -1
View File
@@ -1 +1 @@
eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa 3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa
+19
View File
@@ -90,6 +90,25 @@ group.<your-team-id>.<your-app-id>.01
The exact value must match your own profile. Do not reuse someone else's certificate, profile, or App Group. The exact value must match your own profile. Do not reuse someone else's certificate, profile, or App Group.
The hook is stored as a nested framework at:
```text
Payload/AppsManager.app/Frameworks/AppsManagerHook.framework
```
Standard signing tools must sign `Frameworks` recursively. If a tool signs only the main executable, the app will fail to launch after re-signing. The published build uses the framework layout specifically so tools such as AltStore, SideStore, Sideloadly, and similar signers handle the hook as nested code.
### 重签名 / Re-signing
- 必须保留 App Group 权限;如果新的描述文件没有 App Group,App 会提示 `No usable App Group`,无法访问其他 App 容器。
The new provisioning profile must include an App Group. Without it the app reports `No usable App Group` and cannot access other app containers.
- 修改 Bundle ID 时,必须同时使用与该 Bundle ID 匹配的描述文件和 entitlements。
When changing the Bundle ID, use a matching provisioning profile and entitlements.
- 签名工具必须递归签名 `Frameworks` 目录;不要只签名主程序。
The signing tool must recursively sign the `Frameworks` directory, not only the main executable.
- 免费 Apple ID 通常不支持 App Group,因此无法让 worse_query 正常工作。
Free Apple ID profiles generally do not support App Groups, so worse_query will not work with them.
## 第三方代码 / Third-party code ## 第三方代码 / Third-party code
This source includes and links code from: This source includes and links code from:
+43 -3
View File
@@ -25,6 +25,7 @@ OUT_DIR=${OUT_DIR:-"$ROOT/out"}
BUNDLE_ID=${BUNDLE_ID:-com.jbjf.appsmanager} BUNDLE_ID=${BUNDLE_ID:-com.jbjf.appsmanager}
DYLIB="$ROOT/hook/AppsManagerHook.dylib" DYLIB="$ROOT/hook/AppsManagerHook.dylib"
APP="$OUT_DIR/AppsManager.app" APP="$OUT_DIR/AppsManager.app"
FRAMEWORK="$APP/Frameworks/AppsManagerHook.framework"
if [ ! -f "$DYLIB" ]; then if [ ! -f "$DYLIB" ]; then
echo "Missing $DYLIB; run build_dylib.sh first." >&2 echo "Missing $DYLIB; run build_dylib.sh first." >&2
@@ -42,17 +43,55 @@ fi
rm -rf "$APP" rm -rf "$APP"
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
cp -R "$APP_SOURCE" "$APP" cp -R "$APP_SOURCE" "$APP"
cp "$DYLIB" "$APP/AppsManagerHook.dylib" mkdir -p "$FRAMEWORK"
cp "$DYLIB" "$FRAMEWORK/AppsManagerHook"
cp "$PROVISION" "$APP/embedded.mobileprovision" cp "$PROVISION" "$APP/embedded.mobileprovision"
plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP/Info.plist" plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP/Info.plist"
plutil -replace CFBundleDisplayName -string "Apps Manager" "$APP/Info.plist" plutil -replace CFBundleDisplayName -string "Apps Manager" "$APP/Info.plist"
plutil -replace CFBundleName -string "AppsManager" "$APP/Info.plist" plutil -replace CFBundleName -string "AppsManager" "$APP/Info.plist"
cat > "$FRAMEWORK/Info.plist" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>AppsManagerHook</string>
<key>CFBundleIdentifier</key>
<string>com.jbjf.appsmanager.hook</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>AppsManagerHook</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
</dict>
</plist>
EOF
# Keep the hook in Frameworks so standard signing tools treat it as nested
# code and re-sign it together with the app.
install_name_tool -id "@rpath/AppsManagerHook.framework/AppsManagerHook" "$FRAMEWORK/AppsManagerHook"
cp "$APP/ADManager" "$APP/ADManager.orig" cp "$APP/ADManager" "$APP/ADManager.orig"
"$INSERT_DYLIB" --inplace --overwrite --strip-codesig --all-yes \ "$INSERT_DYLIB" --inplace --overwrite --strip-codesig --all-yes \
"@executable_path/AppsManagerHook.dylib" "$APP/ADManager" "@rpath/AppsManagerHook.framework/AppsManagerHook" "$APP/ADManager"
rm -f "$APP/ADManager.orig" rm -f "$APP/ADManager.orig"
if ! otool -l "$APP/ADManager" | grep -q "@executable_path/Frameworks"; then
install_name_tool -add_rpath "@executable_path/Frameworks" "$APP/ADManager"
fi
set -- -f -s "$SIGN_IDENTITY" set -- -f -s "$SIGN_IDENTITY"
if [ -n "$KEYCHAIN" ]; then if [ -n "$KEYCHAIN" ]; then
@@ -62,8 +101,9 @@ if [ -n "$ENTITLEMENTS" ]; then
set -- "$@" --entitlements "$ENTITLEMENTS" --generate-entitlement-der set -- "$@" --entitlements "$ENTITLEMENTS" --generate-entitlement-der
fi fi
codesign "$@" "$APP/AppsManagerHook.dylib" codesign "$@" "$FRAMEWORK"
codesign "$@" "$APP" codesign "$@" "$APP"
echo "built $APP" echo "built $APP"
otool -L "$APP/ADManager" | head -8 otool -L "$APP/ADManager" | head -8
otool -l "$APP/ADManager" | grep -A2 LC_RPATH | head -6
+67 -10
View File
@@ -36,6 +36,7 @@ static NSString *WBLocalized(NSString *key)
@"scanning_containers_short": @"Scanning system containers…", @"ls_found": @"LaunchServices database located", @"scanning_containers_short": @"Scanning system containers…", @"ls_found": @"LaunchServices database located",
@"init_failed": @"Initialization Failed", @"ok": @"OK", @"init_failed": @"Initialization Failed", @"ok": @"OK",
@"no_group": @"No usable App Group; worse_query cannot run", @"no_group": @"No usable App Group; worse_query cannot run",
@"no_group_hint": @"Re-sign with a certificate and provisioning profile that include an App Group entitlement.",
@"no_ls": @"LaunchServices database not found", @"no_ls": @"LaunchServices database not found",
@"no_containers": @"The LaunchServices database returned no containers", @"no_containers": @"The LaunchServices database returned no containers",
}, },
@@ -47,6 +48,7 @@ static NSString *WBLocalized(NSString *key)
@"scanning_containers_short": @"正在扫描系统容器…", @"ls_found": @"已定位 LaunchServices 数据库", @"scanning_containers_short": @"正在扫描系统容器…", @"ls_found": @"已定位 LaunchServices 数据库",
@"init_failed": @"初始化失败", @"ok": @"好", @"init_failed": @"初始化失败", @"ok": @"好",
@"no_group": @"未找到可用的 App Group,无法执行 worse_query", @"no_group": @"未找到可用的 App Group,无法执行 worse_query",
@"no_group_hint": @"请使用包含 App Group 权限的证书和描述文件重新签名。",
@"no_ls": @"未找到 LaunchServices 数据库", @"no_ls": @"未找到 LaunchServices 数据库",
@"no_containers": @"LaunchServices 数据库没有返回容器列表", @"no_containers": @"LaunchServices 数据库没有返回容器列表",
}, },
@@ -780,9 +782,17 @@ static void (*gOrigLoad)(id, SEL);
static NSString *(*gOrigFileSizeString)(id, SEL); static NSString *(*gOrigFileSizeString)(id, SEL);
static void WBStartPreparation(void); static void WBStartPreparation(void);
static BOOL (*gOrigDidFinishLaunching)(id, SEL, UIApplication *, NSDictionary *); static BOOL (*gOrigDidFinishLaunching)(id, SEL, UIApplication *, NSDictionary *);
static CFAbsoluteTime gLaunchStarted;
static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *application, NSDictionary *launchOptions) static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *application, NSDictionary *launchOptions)
{ {
gLaunchStarted = CFAbsoluteTimeGetCurrent();
// The original app shows a one-time "Message From Developer" alert that
// competes with the launch progress sheet and makes the first run look
// blank. The hook has its own launch status UI, so skip it.
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
[defaults setBool:YES forKey:@"FirstLaunch"];
[defaults synchronize];
BOOL result = gOrigDidFinishLaunching ? gOrigDidFinishLaunching(self, _cmd, application, launchOptions) : YES; BOOL result = gOrigDidFinishLaunching ? gOrigDidFinishLaunching(self, _cmd, application, launchOptions) : YES;
WBStartPreparation(); WBStartPreparation();
return result; return result;
@@ -790,13 +800,29 @@ static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *applicati
static __weak id gAppsTableViewController; static __weak id gAppsTableViewController;
static WBProgressViewController *gProgressController; static WBProgressViewController *gProgressController;
static dispatch_semaphore_t gReadySemaphore; static NSCondition *gReadyCondition;
static BOOL gPrepared; static BOOL gPrepared;
static BOOL gPreparing; static BOOL gPreparing;
static BOOL gWaitingForRoot;
static BOOL gReloadScheduled; static BOOL gReloadScheduled;
static BOOL WBWaitForRegistry(void)
{
if (WBRegistry.shared.ready) return YES;
if (!gReadyCondition) return NO;
[gReadyCondition lock];
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:120];
while (!WBRegistry.shared.ready && !gPrepared && deadline.timeIntervalSinceNow > 0) {
[gReadyCondition waitUntilDate:deadline];
}
BOOL ready = WBRegistry.shared.ready;
[gReadyCondition unlock];
return ready;
}
static NSArray *WBHookAllInstalled(id self, SEL _cmd) static NSArray *WBHookAllInstalled(id self, SEL _cmd)
{ {
if (!WBRegistry.shared.ready) WBWaitForRegistry();
NSArray *proxies = WBRegistry.shared.proxies; NSArray *proxies = WBRegistry.shared.proxies;
if (WBRegistry.shared.ready && proxies.count) return proxies; if (WBRegistry.shared.ready && proxies.count) return proxies;
return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[]; return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[];
@@ -804,6 +830,7 @@ static NSArray *WBHookAllInstalled(id self, SEL _cmd)
static NSArray *WBHookAllApplications(id self, SEL _cmd) static NSArray *WBHookAllApplications(id self, SEL _cmd)
{ {
if (!WBRegistry.shared.ready) WBWaitForRegistry();
NSArray *proxies = WBRegistry.shared.proxies; NSArray *proxies = WBRegistry.shared.proxies;
if (WBRegistry.shared.ready && proxies.count) return proxies; if (WBRegistry.shared.ready && proxies.count) return proxies;
return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[]; return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[];
@@ -872,9 +899,6 @@ static void WBHookDetailReload(id self, SEL _cmd)
static void WBHookLoad(id self, SEL _cmd) static void WBHookLoad(id self, SEL _cmd)
{ {
gAppsTableViewController = self; gAppsTableViewController = self;
if (gReadySemaphore && !WBRegistry.shared.ready) {
dispatch_semaphore_wait(gReadySemaphore, dispatch_time(DISPATCH_TIME_NOW, 120 * NSEC_PER_SEC));
}
gOrigLoad(self, _cmd); gOrigLoad(self, _cmd);
} }
@@ -1467,7 +1491,7 @@ static void WBInstallHooks(void)
method_setImplementation(didFinish, (IMP)WBHookDidFinishLaunching); method_setImplementation(didFinish, (IMP)WBHookDidFinishLaunching);
} }
gReadySemaphore = dispatch_semaphore_create(0); gReadyCondition = [NSCondition new];
[[NSNotificationCenter defaultCenter] addObserverForName:@"WBAppSizeDidUpdate" object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { [[NSNotificationCenter defaultCenter] addObserverForName:@"WBAppSizeDidUpdate" object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) {
if (gReloadScheduled) return; if (gReloadScheduled) return;
gReloadScheduled = YES; gReloadScheduled = YES;
@@ -1661,7 +1685,6 @@ static void WBRunDebugRefresh(void)
static void WBStartPreparation(void) static void WBStartPreparation(void)
{ {
if (gPreparing || gPrepared) return; if (gPreparing || gPrepared) return;
gPreparing = YES;
UIWindow *window = UIApplication.sharedApplication.keyWindow; UIWindow *window = UIApplication.sharedApplication.keyWindow;
if (!window) { if (!window) {
@@ -1673,25 +1696,58 @@ static void WBStartPreparation(void)
} }
} }
UIViewController *root = window.rootViewController; UIViewController *root = window.rootViewController;
if (root && !gProgressController) { if (!root) {
// On a cold launch the window/root view may not exist yet. Wait for it
// instead of running the preparation with no visible progress sheet.
if (!gWaitingForRoot && CFAbsoluteTimeGetCurrent() - gLaunchStarted < 10) {
gWaitingForRoot = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
gWaitingForRoot = NO;
WBStartPreparation();
});
return;
}
if (!gWaitingForRoot) {
// Last-resort fallback: start even without a presenter.
gPreparing = YES;
[WBRegistry.shared prepareWithProgress:nil completion:^(BOOL ok, NSString *message) {
gPrepared = YES;
[gReadyCondition lock];
[gReadyCondition broadcast];
[gReadyCondition unlock];
}];
}
return;
}
gPreparing = YES;
[root loadViewIfNeeded];
UIViewController *presenter = root;
while (presenter.presentedViewController) presenter = presenter.presentedViewController;
if (!gProgressController) {
gProgressController = [WBProgressViewController new]; gProgressController = [WBProgressViewController new];
gProgressController.modalPresentationStyle = UIModalPresentationOverFullScreen; gProgressController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[root presentViewController:gProgressController animated:YES completion:nil]; [presenter presentViewController:gProgressController animated:YES completion:nil];
} }
[WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) { [WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) {
[gProgressController updateFraction:fraction status:status]; [gProgressController updateFraction:fraction status:status];
} completion:^(BOOL ok, NSString *message) { } completion:^(BOOL ok, NSString *message) {
gPrepared = YES; gPrepared = YES;
if (gReadySemaphore) dispatch_semaphore_signal(gReadySemaphore); [gReadyCondition lock];
[gReadyCondition broadcast];
[gReadyCondition unlock];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (gProgressController) { if (gProgressController) {
[gProgressController dismissViewControllerAnimated:YES completion:nil]; [gProgressController dismissViewControllerAnimated:YES completion:nil];
gProgressController = nil; gProgressController = nil;
} }
if (!ok && message.length) { if (!ok && message.length) {
NSString *detail = message;
if ([message containsString:@"App Group"]) {
detail = [message stringByAppendingFormat:@"\n\n%@", WBLocalized(@"no_group_hint")];
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:WBLocalized(@"init_failed") UIAlertController *alert = [UIAlertController alertControllerWithTitle:WBLocalized(@"init_failed")
message:message message:detail
preferredStyle:UIAlertControllerStyleAlert]; preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:WBLocalized(@"ok") style:UIAlertActionStyleDefault handler:nil]]; [alert addAction:[UIAlertAction actionWithTitle:WBLocalized(@"ok") style:UIAlertActionStyleDefault handler:nil]];
UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow;
@@ -1711,6 +1767,7 @@ __attribute__((constructor))
static void WBHookInit(void) static void WBHookInit(void)
{ {
@autoreleasepool { @autoreleasepool {
gLaunchStarted = CFAbsoluteTimeGetCurrent();
WBInstallHooks(); WBInstallHooks();
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification