From 66a17bc27f5e47dc73c455e2ca2210d90aa11198 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 6 Feb 2023 17:36:05 -0600 Subject: [PATCH] Supports sideloading more than 3 apps via MacDirtyCow exploit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MacDirtyCow exploit allows users to remove the 3 active apps limit on iOS 16.1.2 and earlier. To support this, we’ve added a new (hidden) “Enforce 3-App Limit” setting that can be disabled to allow sideloading more than 3 apps. --- AltStore/My Apps/MyAppsViewController.swift | 8 ++- AltStore/Settings/Settings.storyboard | 41 ++++++++++++++ .../Settings/SettingsViewController.swift | 54 ++++++++++++++++--- .../Extensions/UserDefaults+AltStore.swift | 6 ++- AltStoreCore/Model/InstalledApp.swift | 18 ++++++- .../Policies/InstalledAppPolicy.swift | 2 +- 6 files changed, 118 insertions(+), 11 deletions(-) diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 9860cfd3..31d53d07 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -959,7 +959,7 @@ private extension MyAppsViewController @objc func presentInactiveAppsAlert() { - let message: String + var message: String if UserDefaults.standard.activeAppLimitIncludesExtensions { @@ -968,6 +968,12 @@ private extension MyAppsViewController else { message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "") + + if UserDefaults.standard.ignoreActiveAppsLimit + { + message += "\n\n" + message += NSLocalizedString("If you're using the MacDirtyCow exploit to remove the 3-app limit, you can install up to 10 apps and app extensions instead.", comment: "") + } } let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: message, preferredStyle: .alert) diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index abc0abf6..4f391ab2 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -590,6 +590,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -844,6 +884,7 @@ + diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index a3fd79fc..f8ffe4f4 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -26,6 +26,7 @@ extension SettingsViewController case instructions case techyThings case credits + case macDirtyCow case debug } @@ -88,6 +89,7 @@ final class SettingsViewController: UITableViewController @IBOutlet private var disableAppLimitSwitch: UISwitch! @IBOutlet private var refreshSideJITServer: UILabel! + @IBOutlet private var enforceThreeAppLimitSwitch: UISwitch! @IBOutlet private var versionLabel: UILabel! @@ -198,6 +200,7 @@ private extension SettingsViewController self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled + self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit if self.isViewLoaded { @@ -261,6 +264,16 @@ private extension SettingsViewController case .credits: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "") + case .macDirtyCow: + if isHeader + { + settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("MACDIRTYCOW", comment: "") + } + else + { + settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "") + } + case .debug: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "") } @@ -277,6 +290,20 @@ private extension SettingsViewController let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) return size.height } + + func isSectionHidden(_ section: Section) -> Bool + { + switch section + { + case .macDirtyCow: + let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0) + + let isMacDirtyCowExploitSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2) + return !(isMacDirtyCowExploitSupported && UserDefaults.standard.isDebugModeEnabled) + + default: return false + } + } } private extension SettingsViewController @@ -342,6 +369,16 @@ private extension SettingsViewController UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn } + @IBAction func toggleEnforceThreeAppLimit(_ sender: UISwitch) + { + UserDefaults.standard.ignoreActiveAppsLimit = !sender.isOn + + if UserDefaults.standard.activeAppsLimit != nil + { + UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit + } + } + @available(iOS 14, *) @IBAction func addRefreshAppsShortcut() { @@ -470,6 +507,7 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return 0 case .signIn: return (self.activeTeam == nil) ? 1 : 0 case .account: return (self.activeTeam == nil) ? 0 : 3 case .appRefresh: return AppRefreshRow.allCases.count @@ -516,9 +554,10 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return nil case .signIn where self.activeTeam != nil: return nil case .account where self.activeTeam == nil: return nil - case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .debug: + case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug: let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView self.prepare(headerView, for: section, isHeader: true) return headerView @@ -532,8 +571,9 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return nil case .signIn where self.activeTeam != nil: return nil - case .signIn, .patreon, .appRefresh: + case .signIn, .patreon, .appRefresh, .macDirtyCow: let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView self.prepare(footerView, for: section, isHeader: false) return footerView @@ -547,9 +587,10 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return 1.0 case .signIn where self.activeTeam != nil: return 1.0 case .account where self.activeTeam == nil: return 1.0 - case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .debug: + case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug: let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true) return height @@ -562,9 +603,10 @@ extension SettingsViewController let section = Section.allCases[section] switch section { + case _ where isSectionHidden(section): return 1.0 case .signIn where self.activeTeam != nil: return 1.0 - case .account where self.activeTeam == nil: return 1.0 - case .signIn, .patreon, .appRefresh: + case .account where self.activeTeam == nil: return 1.0 + case .signIn, .patreon, .appRefresh, .macDirtyCow: let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false) return height @@ -801,7 +843,7 @@ extension SettingsViewController } - case .account, .patreon, .instructions, .techyThings: break + case .account, .patreon, .instructions, .techyThings, .macDirtyCow: break } } } diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift index ecfd300d..4337319b 100644 --- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift +++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift @@ -52,6 +52,7 @@ public extension UserDefaults @NSManaged var trustedSourceIDs: [String]? @NSManaged var trustedServerURL: String? + @nonobjc var activeAppsLimit: Int? { get { return self._activeAppsLimit?.intValue @@ -69,6 +70,8 @@ public extension UserDefaults } @NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber? + @NSManaged var ignoreActiveAppsLimit: Bool + class func registerDefaults() { let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0) @@ -88,7 +91,8 @@ public extension UserDefaults #keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing, #keyPath(UserDefaults.requiresAppGroupMigration): true, #keyPath(UserDefaults.menuAnisetteList): "https://servers.sidestore.io/servers.json", - #keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io" + #keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io", + #keyPath(UserDefaults.ignoreActiveAppsLimit): false ] as [String : Any] UserDefaults.standard.register(defaults: defaults) diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift index b27c88e6..26d3871a 100644 --- a/AltStoreCore/Model/InstalledApp.swift +++ b/AltStoreCore/Model/InstalledApp.swift @@ -12,8 +12,22 @@ import CoreData import AltSign import SemanticVersion -// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1. -public let ALTActiveAppsLimit = 3 +extension InstalledApp +{ + public static var freeAccountActiveAppsLimit: Int { + if UserDefaults.standard.ignoreActiveAppsLimit + { + // MacDirtyCow exploit allows users to remove 3-app limit, so return 10 to match App ID limit per-week. + // Don't return nil because that implies there is no limit, which isn't quite true due to App ID limit. + return 10 + } + else + { + // Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1. + return 3 + } + } +} public protocol InstalledAppProtocol: Fetchable { diff --git a/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift b/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift index 880ae8fe..9c01f15f 100644 --- a/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift +++ b/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift @@ -50,7 +50,7 @@ class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy // We can assume there is an active app limit, // but will confirm next time user authenticates. - UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit + UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit } return NSNumber(value: isActive)