Fix first launch and package hook as a signed framework
This commit is contained in:
1 parent
c1d0293404
commit
97a4fad71f
6 files changed
+147
-20
No files matched your search
Binary file not shown.
@@ -15,7 +15,7 @@
|
||||
- 源码 / Source:[source/](source/)(iOS 27 Hook、内置文件浏览器集成与构建脚本 / iOS 27 hook, file-browser integration and build scripts)
|
||||
|
||||
```text
|
||||
eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa
|
||||
3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa
|
||||
```
|
||||
|
||||
## 功能 / Features
|
||||
@@ -56,14 +56,25 @@ eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iO
|
||||
**中文:**
|
||||
|
||||
1. 使用 AltStore、SideStore 或其他签名工具安装 `AppsManager_iOS27.ipa`。
|
||||
2. 当前 IPA 使用自用证书签名;其他设备安装时请使用自己的证书和描述文件重签。
|
||||
3. 首次启动会执行数据准备,等待进度条完成后再进入应用列表。
|
||||
2. 当前 IPA 使用自用证书签名。其他设备请使用自己的证书和描述文件重签;描述文件必须包含 **App Group**,且签名工具需要递归签名 `Frameworks`。
|
||||
3. 首次启动会执行数据准备,等待进度完成后再进入应用列表。
|
||||
|
||||
**English:**
|
||||
|
||||
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.
|
||||
3. The first launch prepares app data; wait for the progress bar to finish before using the app list.
|
||||
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 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
|
||||
|
||||
@@ -83,5 +94,5 @@ eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iO
|
||||
## 校验 / Checksum
|
||||
|
||||
```text
|
||||
eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa
|
||||
3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa
|
||||
```
|
||||
+1
-1
@@ -1 +1 @@
|
||||
eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa
|
||||
3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa
|
||||
@@ -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 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
|
||||
|
||||
This source includes and links code from:
|
||||
|
||||
+43
-3
@@ -25,6 +25,7 @@ OUT_DIR=${OUT_DIR:-"$ROOT/out"}
|
||||
BUNDLE_ID=${BUNDLE_ID:-com.jbjf.appsmanager}
|
||||
DYLIB="$ROOT/hook/AppsManagerHook.dylib"
|
||||
APP="$OUT_DIR/AppsManager.app"
|
||||
FRAMEWORK="$APP/Frameworks/AppsManagerHook.framework"
|
||||
|
||||
if [ ! -f "$DYLIB" ]; then
|
||||
echo "Missing $DYLIB; run build_dylib.sh first." >&2
|
||||
@@ -42,17 +43,55 @@ fi
|
||||
rm -rf "$APP"
|
||||
mkdir -p "$OUT_DIR"
|
||||
cp -R "$APP_SOURCE" "$APP"
|
||||
cp "$DYLIB" "$APP/AppsManagerHook.dylib"
|
||||
mkdir -p "$FRAMEWORK"
|
||||
cp "$DYLIB" "$FRAMEWORK/AppsManagerHook"
|
||||
cp "$PROVISION" "$APP/embedded.mobileprovision"
|
||||
|
||||
plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP/Info.plist"
|
||||
plutil -replace CFBundleDisplayName -string "Apps Manager" "$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"
|
||||
"$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"
|
||||
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"
|
||||
if [ -n "$KEYCHAIN" ]; then
|
||||
@@ -62,8 +101,9 @@ if [ -n "$ENTITLEMENTS" ]; then
|
||||
set -- "$@" --entitlements "$ENTITLEMENTS" --generate-entitlement-der
|
||||
fi
|
||||
|
||||
codesign "$@" "$APP/AppsManagerHook.dylib"
|
||||
codesign "$@" "$FRAMEWORK"
|
||||
codesign "$@" "$APP"
|
||||
|
||||
echo "built $APP"
|
||||
otool -L "$APP/ADManager" | head -8
|
||||
otool -l "$APP/ADManager" | grep -A2 LC_RPATH | head -6
|
||||
+67
-10
@@ -36,6 +36,7 @@ static NSString *WBLocalized(NSString *key)
|
||||
@"scanning_containers_short": @"Scanning system containers…", @"ls_found": @"LaunchServices database located",
|
||||
@"init_failed": @"Initialization Failed", @"ok": @"OK",
|
||||
@"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_containers": @"The LaunchServices database returned no containers",
|
||||
},
|
||||
@@ -47,6 +48,7 @@ static NSString *WBLocalized(NSString *key)
|
||||
@"scanning_containers_short": @"正在扫描系统容器…", @"ls_found": @"已定位 LaunchServices 数据库",
|
||||
@"init_failed": @"初始化失败", @"ok": @"好",
|
||||
@"no_group": @"未找到可用的 App Group,无法执行 worse_query",
|
||||
@"no_group_hint": @"请使用包含 App Group 权限的证书和描述文件重新签名。",
|
||||
@"no_ls": @"未找到 LaunchServices 数据库",
|
||||
@"no_containers": @"LaunchServices 数据库没有返回容器列表",
|
||||
},
|
||||
@@ -780,9 +782,17 @@ static void (*gOrigLoad)(id, SEL);
|
||||
static NSString *(*gOrigFileSizeString)(id, SEL);
|
||||
static void WBStartPreparation(void);
|
||||
static BOOL (*gOrigDidFinishLaunching)(id, SEL, UIApplication *, NSDictionary *);
|
||||
static CFAbsoluteTime gLaunchStarted;
|
||||
|
||||
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;
|
||||
WBStartPreparation();
|
||||
return result;
|
||||
@@ -790,13 +800,29 @@ static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *applicati
|
||||
|
||||
static __weak id gAppsTableViewController;
|
||||
static WBProgressViewController *gProgressController;
|
||||
static dispatch_semaphore_t gReadySemaphore;
|
||||
static NSCondition *gReadyCondition;
|
||||
static BOOL gPrepared;
|
||||
static BOOL gPreparing;
|
||||
static BOOL gWaitingForRoot;
|
||||
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)
|
||||
{
|
||||
if (!WBRegistry.shared.ready) WBWaitForRegistry();
|
||||
NSArray *proxies = WBRegistry.shared.proxies;
|
||||
if (WBRegistry.shared.ready && proxies.count) return proxies;
|
||||
return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[];
|
||||
@@ -804,6 +830,7 @@ static NSArray *WBHookAllInstalled(id self, SEL _cmd)
|
||||
|
||||
static NSArray *WBHookAllApplications(id self, SEL _cmd)
|
||||
{
|
||||
if (!WBRegistry.shared.ready) WBWaitForRegistry();
|
||||
NSArray *proxies = WBRegistry.shared.proxies;
|
||||
if (WBRegistry.shared.ready && proxies.count) return proxies;
|
||||
return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[];
|
||||
@@ -872,9 +899,6 @@ static void WBHookDetailReload(id self, SEL _cmd)
|
||||
static void WBHookLoad(id self, SEL _cmd)
|
||||
{
|
||||
gAppsTableViewController = self;
|
||||
if (gReadySemaphore && !WBRegistry.shared.ready) {
|
||||
dispatch_semaphore_wait(gReadySemaphore, dispatch_time(DISPATCH_TIME_NOW, 120 * NSEC_PER_SEC));
|
||||
}
|
||||
gOrigLoad(self, _cmd);
|
||||
}
|
||||
|
||||
@@ -1467,7 +1491,7 @@ static void WBInstallHooks(void)
|
||||
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) {
|
||||
if (gReloadScheduled) return;
|
||||
gReloadScheduled = YES;
|
||||
@@ -1661,7 +1685,6 @@ static void WBRunDebugRefresh(void)
|
||||
static void WBStartPreparation(void)
|
||||
{
|
||||
if (gPreparing || gPrepared) return;
|
||||
gPreparing = YES;
|
||||
|
||||
UIWindow *window = UIApplication.sharedApplication.keyWindow;
|
||||
if (!window) {
|
||||
@@ -1673,25 +1696,58 @@ static void WBStartPreparation(void)
|
||||
}
|
||||
}
|
||||
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.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
||||
[root presentViewController:gProgressController animated:YES completion:nil];
|
||||
[presenter presentViewController:gProgressController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
[WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) {
|
||||
[gProgressController updateFraction:fraction status:status];
|
||||
} completion:^(BOOL ok, NSString *message) {
|
||||
gPrepared = YES;
|
||||
if (gReadySemaphore) dispatch_semaphore_signal(gReadySemaphore);
|
||||
[gReadyCondition lock];
|
||||
[gReadyCondition broadcast];
|
||||
[gReadyCondition unlock];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (gProgressController) {
|
||||
[gProgressController dismissViewControllerAnimated:YES completion:nil];
|
||||
gProgressController = nil;
|
||||
}
|
||||
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")
|
||||
message:message
|
||||
message:detail
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:WBLocalized(@"ok") style:UIAlertActionStyleDefault handler:nil]];
|
||||
UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow;
|
||||
@@ -1711,6 +1767,7 @@ __attribute__((constructor))
|
||||
static void WBHookInit(void)
|
||||
{
|
||||
@autoreleasepool {
|
||||
gLaunchStarted = CFAbsoluteTimeGetCurrent();
|
||||
WBInstallHooks();
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
|
||||
|
||||
Reference in new issue
Block a user