Supports sideloading more than 3 apps via MacDirtyCow exploit

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.
This commit is contained in:
Riley Testut
2023-02-06 17:36:05 -06:00
parent 90b4fa7605
commit eeea64f780
7 changed files with 118 additions and 11 deletions

View File

@@ -991,7 +991,7 @@ private extension MyAppsViewController
@objc func presentInactiveAppsAlert()
{
let message: String
var message: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
@@ -1000,6 +1000,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)

View File

@@ -245,7 +245,7 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
else
{

View File

@@ -518,6 +518,46 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="" id="2em-H5-kgS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="n3X-OX-idC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="957" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="n3X-OX-idC" id="IVp-7k-KdM">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enforce 3-App Limit" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IY0-94-5LN">
<rect key="frame" x="30" y="15.5" width="163" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Oie-te-KSQ">
<rect key="frame" x="296" y="10" width="51" height="31"/>
<connections>
<action selector="toggleEnforceThreeAppLimit:" destination="aMk-Xp-UL8" eventType="valueChanged" id="tfb-kk-C17"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="IY0-94-5LN" firstAttribute="leading" secondItem="IVp-7k-KdM" secondAttribute="leadingMargin" id="07y-eS-INC"/>
<constraint firstItem="Oie-te-KSQ" firstAttribute="centerY" secondItem="IVp-7k-KdM" secondAttribute="centerY" id="1dS-uM-gb1"/>
<constraint firstItem="IY0-94-5LN" firstAttribute="centerY" secondItem="IVp-7k-KdM" secondAttribute="centerY" id="FyZ-BM-Ss0"/>
<constraint firstAttribute="trailingMargin" secondItem="Oie-te-KSQ" secondAttribute="trailing" id="I1v-Ub-eJJ"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="0"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="" id="OMa-EK-hRI">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
@@ -603,6 +643,7 @@
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
<outlet property="enforceThreeAppLimitSwitch" destination="Oie-te-KSQ" id="jKn-t1-gyk"/>
<outlet property="versionLabel" destination="bUR-rp-Nw2" id="85I-5R-hqz"/>
</connections>
</tableViewController>

View File

@@ -25,6 +25,7 @@ extension SettingsViewController
case instructions
case techyThings
case credits
case macDirtyCow
case debug
}
@@ -70,6 +71,7 @@ class SettingsViewController: UITableViewController
@IBOutlet private var accountTypeLabel: UILabel!
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
@IBOutlet private var enforceThreeAppLimitSwitch: UISwitch!
@IBOutlet private var versionLabel: UILabel!
@@ -146,6 +148,7 @@ private extension SettingsViewController
}
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit
if self.isViewLoaded
{
@@ -209,6 +212,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: "")
}
@@ -225,6 +238,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
@@ -279,6 +306,16 @@ private extension SettingsViewController
UserDefaults.standard.isBackgroundRefreshEnabled = 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()
{
@@ -376,6 +413,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
@@ -404,9 +442,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
@@ -420,8 +459,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
@@ -435,9 +475,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
@@ -450,9 +491,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 .signIn, .patreon, .appRefresh, .macDirtyCow:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
@@ -520,7 +562,7 @@ extension SettingsViewController
case .refreshAttempts: break
}
case .account, .patreon, .instructions, .techyThings: break
case .account, .patreon, .instructions, .techyThings, .macDirtyCow: break
}
}
}

View File

@@ -41,6 +41,7 @@ public extension UserDefaults
@NSManaged var trustedSourceIDs: [String]?
@nonobjc
var activeAppsLimit: Int? {
get {
return self._activeAppsLimit?.intValue
@@ -58,6 +59,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)
@@ -72,7 +75,8 @@ public extension UserDefaults
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
#keyPath(UserDefaults.requiresAppGroupMigration): true
#keyPath(UserDefaults.requiresAppGroupMigration): true,
#keyPath(UserDefaults.ignoreActiveAppsLimit): false,
]
UserDefaults.standard.register(defaults: defaults)

View File

@@ -11,8 +11,22 @@ import CoreData
import AltSign
// 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
{

View File

@@ -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)