-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update IPA Installation Techniques and Tools (by @NVISOSecurity) #3100
base: master
Are you sure you want to change the base?
Changes from 24 commits
f3da3a7
566208d
d39142b
522c1af
f6f8dca
679e62e
e9ff817
d95283f
462784d
6ec2102
52dd898
e228714
ef4fabf
d28f7ae
2ce8cbd
a2611b6
90f5347
1d36e50
5f77407
7f02dd7
cc66983
8f8cc97
317b37e
697bc76
6710970
3d5bc8b
1441ca0
c9f9d73
ff73a79
4781846
bebc38e
58aab16
27b0adb
2929ad3
db19d8e
85d3825
3953ec9
182ab62
430b2c2
c062048
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,19 +3,204 @@ title: Launching a Repackaged App in Debug Mode | |
platform: ios | ||
--- | ||
|
||
After the app has been installed on the device, it needs to be launched in debug mode. This is not the case when launching the app via springboard (the application will crash), but it is possible with various tools as explained in @MASTG-TECH-0056. When the application is running in debug mode, Frida can be injected into the process with name `Gadget`: | ||
If you've repackaged an application with a Frida Gadget, or if you want to attach @MASTG-TOOL-0057 to the application, you have to launch the application in debug mode. When you launch the application via SpringBoard, it will not launch in debug mode and the application will crash. | ||
|
||
After the application has been installed using @MASTG-TECH-0056, you can launch it in debug mode using the following commands. | ||
|
||
> Note that the commands that are part of @MASTG-TOOL-0126 refer to the latest version available from Github. If you installed them via brew or other package managers, you may have an older version with different command line flags. | ||
|
||
## iOS17 and newer | ||
|
||
First, make sure you know the correct Bundle Identifier. Depending on how you signed the application, the actual Bundle Identifier might be different from the original Bundle Identifier. To get an overview of the installed applications, use the `ideviceinstaller` tool (see @MASTG-TOOL-0126): | ||
|
||
```bash | ||
$ ideviceinstaller list | ||
CFBundleIdentifier, CFBundleShortVersionString, CFBundleDisplayName | ||
sg.vp.UnCrackable1.QH868V5764, "1.0", "UnCrackable1" | ||
org.owasp.mastestapp.MASTestApp, "3.0.0", "Adyen3DS2Demo" | ||
com.apple.TestFlight, "3.5.2", "TestFlight" | ||
``` | ||
|
||
In this example, @MASTG-TOOL-0118 appended the team identifier (`QH868V5764`) to the original Bundle Identifier. | ||
|
||
Next, we need to get the correct device identifier, which we can get using `idevice_id` (see @MASTG-TOOL-0126): | ||
|
||
```bash | ||
$ idevice_id | ||
00008101-1234567890123456 (USB) | ||
00008101-1234567890123456 (Network) | ||
``` | ||
|
||
Now that we have the correct Bundle Identifier and device ID, we can launch the app using `xrun` (see @MASTG-TOOL-0071): | ||
|
||
```bash | ||
xcrun devicectl device process launch --device 00008101-1234567890123456 --start-stopped sg.vp.UnCrackable1.QH868V5764 | ||
13:00:43 Enabling developer disk image services. | ||
13:00:43 Acquired usage assertion. | ||
Launched application with sg.vp.UnCrackable1.QH868V5764 bundle identifier. | ||
``` | ||
|
||
Finally, you can attach `lldb` using the following commands: | ||
|
||
```bash | ||
# Execute the lldb debugger | ||
$ lldb | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Select the iOS device you want to interact with | ||
(lldb) device select 00008101-1234567890123456 | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Query the processes on a device. | ||
(lldb) device process list | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
PID PARENT USER TRIPLE NAME | ||
====== ====== ========== ============================== ============================ | ||
1 0 launchd | ||
... | ||
771 0 <anonymous> | ||
774 0 <anonymous> | ||
781 0 ReportCrash | ||
783 0 UnCrackable Level 1 | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Attach to a specific process by their process ID | ||
(lldb) device process attach --pid 783 | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Process 783 stopped | ||
* thread #1, stop reason = signal SIGSTOP | ||
frame #0: 0x0000000104312920 dyld`_dyld_start | ||
dyld`_dyld_start: | ||
-> 0x104312920 <+0>: mov x0, sp | ||
0x104312924 <+4>: and sp, x0, #0xfffffffffffffff0 | ||
0x104312928 <+8>: mov x29, #0x0 ; =0 | ||
0x10431292c <+12>: mov x30, #0x0 ; =0 | ||
Target 0: (UnCrackable Level 1) stopped. | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Continue execution of all threads in the current process. | ||
(lldb) c | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Process 783 resuming | ||
(lldb) | ||
``` | ||
|
||
If you manually injected a Frida Gadget, Frida will now be waiting for you to attach to it. Until you do so, the application will appear frozen. | ||
|
||
```bash | ||
$ frida-ps -Ua | ||
PID Name Identifier | ||
--- ------------- ------------------------------- | ||
389 Calendar com.apple.mobilecal | ||
783 Gadget re.frida.Gadget | ||
336 TestFlight com.apple.TestFlight | ||
783 UnCrackable1 sg.vp.UnCrackable1.QH868V5764 | ||
339 Weather com.apple.weather | ||
``` | ||
|
||
The `783` process has launched a new thread called Gadget to which you can attach: | ||
|
||
```bash | ||
$ frida -U -n Gadget | ||
____ | ||
/ _ | Frida 16.5.9 - A world-class dynamic instrumentation toolkit | ||
| (_| | | ||
> _ | Commands: | ||
/_/ |_| help -> Displays the help system | ||
. . . . object? -> Display information about 'object' | ||
. . . . exit/quit -> Exit | ||
. . . . | ||
. . . . More info at https://frida.re/docs/home/ | ||
. . . . | ||
. . . . Connected to iPhone (id=00008101-000628803A69001E) | ||
|
||
[iPhone::Gadget ]-> ObjC.available | ||
true | ||
``` | ||
|
||
After attaching, the application will continue executing as normal. | ||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## iOS16 and older | ||
|
||
On older versions of iOS, you can use either `idevicedebug` (see @MASTG-TOOL-0126) or @MASTG-TOOL-0054 to launch the app in debug mode. | ||
|
||
### Using idevicedebug | ||
|
||
```bash | ||
idevicedebug -d run sg.vp.UnCrackable1 | ||
# Get the package name | ||
$ ideviceinstaller list | ||
CFBundleIdentifier, CFBundleShortVersionString, CFBundleDisplayName | ||
org.sec575.CoinGame, "1.0", "CoinGame" | ||
sg.vp.UnCrackable1.QH868V5764, "1.0", "UnCrackable1" | ||
com.apple.TestFlight, "3.7.0", "TestFlight" | ||
com.google.Maps, "24.50.0", "Google Maps" | ||
|
||
# Run in debug mode | ||
$ idevicedebug -d run sg.vp.UnCrackable1.QH868V5764 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately this always failed for my iOS 15.8.3 device (jailbroken). I couldn't find a DeveloperDiskImage for 15.8.3, but one for 15.7 but couldn't mount it. Were you testing this on macOS? I am using macOS sequoia 15.1.1 and Xcode 16.2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It works on my machine / device:
In installed via Sideloadly idevicedebug -d run sg.vp.UnCrackable12
working_directory: /private/var/mobile/Containers/Data/Application/9C89C011-33D7-421B-9934-B92488F49486
Setting logging bitmask...
Setting maximum packet size...
Setting working directory...
Setting argv...
app_argv[0] = /private/var/containers/Bundle/Application/F7FDC3A7-2055-46B8-A88B-2764BF84ACD2/UnCrackable Level 1.app/UnCrackable Level 1
Checking if launch succeeded...
Setting thread...
Continue running process... |
||
working_directory: /private/var/mobile/Containers/Data/Application/438DE865-2714-4BD9-B1EE-881AD4E54AD1 | ||
|
||
Setting logging bitmask... | ||
Setting maximum packet size... | ||
Setting working directory... | ||
Setting argv... | ||
app_argv[0] = /private/var/containers/Bundle/Application/E21B5B13-DD85-4C83-9A0E-03FCEBF95CF5/UnCrackable Level 1.app/UnCrackable Level 1 | ||
Checking if launch succeeded... | ||
Setting thread... | ||
Continue running process... | ||
``` | ||
|
||
### Using ios-deploy | ||
|
||
# In a new terminal | ||
frida -U -n Gadget | ||
To use @MASTG-TOOL-0054, you first have to unzip the IPA file: | ||
|
||
```bash | ||
$ unzip Uncrackable1-frida-codesigned.ipa -d unzipped | ||
``` | ||
|
||
Next, use ios-deploy with the path of the app folder inside of the unzipped IPA: | ||
|
||
```bash | ||
$ ios-deploy --bundle 'unzipped/Payload/UnCrackable Level 1.app' -W -d -v | ||
ios-deploy --bundle 'pram/Payload/UnCrackable Level 1.app' -W -d -v | ||
[....] Waiting for iOS device to be connected | ||
Handling device type: 1 | ||
Already found device? 0 | ||
Hardware Model: D211AP | ||
Device Name: NVISO’s iPhone JBE | ||
Model Name: iPhone 8 Plus | ||
SDK Name: iphoneos | ||
Architecture Name: arm64 | ||
Product Version: 16.6.1 | ||
Build Version: 20G81 | ||
[....] Using 593ad60af30ad045b9cb99d2901031226c1b8c84 (D211AP, iPhone 8 Plus, iphoneos, arm64, 16.6.1, 20G81) a.k.a. '**NVISO**’s iPhone JBE'. | ||
------ Install phase ------ | ||
[ 0%] Found 593ad60af30ad045b9cb99d2901031226c1b8c84 (D211AP, iPhone 8 Plus, iphoneos, arm64, 16.6.1, 20G81) a.k.a. 'NVISO’s iPhone JBE' connected through USB, beginning install | ||
[ 5%] Copying /Users/MAS/unzipped/Payload/UnCrackable Level 1.app/META-INF/ to device | ||
[ 5%] Copying /Users/MAS/unzipped/Payload/UnCrackable Level 1.app/META-INF/com.apple.ZipMetadata.plist to device | ||
[ 6%] Copying /Users/MAS/unzipped/Payload/UnCrackable Level 1.app/META-INF/com.apple.ZipMetadata.plist to device | ||
... | ||
[iPhone::Gadget ]-> | ||
``` | ||
|
||
## Starting with iOS 17 and Xcode 15 | ||
### Attaching Frida | ||
|
||
If your application was repackaged with a Frida Gadget, the application will wait for you to attach to it before it continues launching. | ||
|
||
Since Xcode 15 and iOS 17 the tool @MASTG-TOOL-0054 will [not work anymore to start an app in debug mode](https://github.com/ios-control/ios-deploy/issues/588). | ||
In a new terminal window, connect to the Frida gadget, just like in the iOS17 scenario: | ||
|
||
A workaround to start the re-packaged app with the `FridaGadget.dylib` in debug mode (without using @MASTG-TOOL-0054) can be found [here](https://github.com/ios-control/ios-deploy/issues/588#issuecomment-1907913430). | ||
```bash | ||
$ frida-ps -Ua | ||
PID Name Identifier | ||
--- ------------- ----------------------------- | ||
... | ||
468 Gadget re.frida.Gadget | ||
... | ||
468 UnCrackable1 sg.vp.UnCrackable1.QH868V5764 | ||
|
||
|
||
$ frida -U -n Gadget | ||
____ | ||
/ _ | Frida 16.5.9 - A world-class dynamic instrumentation toolkit | ||
| (_| | | ||
> _ | Commands: | ||
/_/ |_| help -> Displays the help system | ||
. . . . object? -> Display information about 'object' | ||
. . . . exit/quit -> Exit | ||
. . . . | ||
. . . . More info at https://frida.re/docs/home/ | ||
. . . . | ||
. . . . Connected to iPhone (id=593ad60af30ad045b9cb99d2901031226c1b8c84) | ||
[iPhone::Gadget ]-> ObjC.available | ||
true | ||
``` |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -3,51 +3,38 @@ title: Installing Apps | |||||
platform: ios | ||||||
--- | ||||||
|
||||||
When you install an application without using Apple's App Store, this is called sideloading. There are various ways of sideloading which are described below. On the iOS device, the actual installation process is then handled by the installd daemon, which will unpack and install the application. To integrate app services or be installed on an iOS device, all applications must be signed with a certificate issued by Apple. This means that the application can be installed only after successful code signature verification. On a jailbroken phone, however, you can circumvent this security feature with [AppSync](https://github.com/akemin-dayo/AppSync "AppSync"), a package available in the Cydia store. It contains numerous useful applications that leverage jailbreak-provided root privileges to execute advanced functionality. AppSync is a tweak that patches installd, allowing the installation of fake-signed IPA packages. | ||||||
When you install an application without using Apple's App Store, this is called sideloading. There are various ways of sideloading which are described below. On the iOS device, the actual installation process is then handled by the installd daemon, which will unpack and install the application. To integrate app services or be installed on an iOS device, all applications must be signed with a certificate issued by Apple. This means that the application can be installed only after successful code signature verification, which is explained in @MASTG-TECH-0092. | ||||||
|
||||||
Different methods exist for installing an IPA package onto an iOS device, which are described in detail below. | ||||||
On a jailbroken device, you can circumvent this requirement using @MASTG-TOOL-0127, allowing you to install IPA files without obtaining a valid signature. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
> Please note that iTunes is no longer available in macOS Catalina. If you are using an older version of macOS, iTunes is still available but since iTunes 12.7 it is not possible to install apps. | ||||||
Different methods exist for installing an IPA package onto an iOS device, which are described in detail below. | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please add |
||||||
## Sideloadly | ||||||
|
||||||
@MASTG-TOOL-0118 is a GUI tool that can automate all required steps for you. It requires valid Apple developer credentials, as it will obtain a valid signature from Apple servers. | ||||||
|
||||||
!!! warning "Do not use your personal Apple account" | ||||||
To sign an IPA file, you will need a valid iOS developer account, either free or paid. Both types come with certain restrictions, as explained on the Sideloadly website. We recommend creating a dedicated developer account for signing test applications, and **not** using your personal Apple account. | ||||||
|
||||||
## libimobiledevice | ||||||
|
||||||
On Linux and also macOS, you can alternatively use [libimobiledevice](https://www.libimobiledevice.org/ "libimobiledevice"), a cross-platform software protocol library and a set of tools for native communication with iOS devices. This allows you to install apps over a USB connection by executing ideviceinstaller. The connection is implemented with the USB multiplexing daemon [usbmuxd](https://www.theiphonewiki.com/wiki/Usbmux "Usbmux"), which provides a TCP tunnel over USB. | ||||||
Simply connect your device via USB, enter your Apple ID and drag-and-drop the IPA file onto SideLoadly. Click start to automatically sign and install the given IPA. | ||||||
|
||||||
The package for libimobiledevice will be available in your Linux package manager. On macOS you can install libimobiledevice via brew: | ||||||
<img src="Images/Techniques/0056-Sideloadly.png" width="400px" /> | ||||||
|
||||||
```bash | ||||||
brew install libimobiledevice | ||||||
brew install ideviceinstaller | ||||||
``` | ||||||
## libimobiledevice | ||||||
|
||||||
If you have any issues, try installing the libraries from source, as the precompiled version may be outdated. | ||||||
On Linux and also macOS, you can alternatively use @MASTG-TOOL-0126. This allows you to install apps over a USB connection by executing ideviceinstaller. The connection is implemented with the USB multiplexing daemon [usbmuxd](https://www.theiphonewiki.com/wiki/Usbmux "Usbmux"), which provides a TCP tunnel over USB. | ||||||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
After the installation you have several new command line tools available, such as `ideviceinfo`, `ideviceinstaller` or `idevicedebug`. Let's install and debug the @MASTG-APP-0028 app with the following commands: | ||||||
Let's install and debug the @MASTG-APP-0028 app with the following commands: | ||||||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
```bash | ||||||
# The following command will show detailed information about the iOS device connected via USB. | ||||||
$ ideviceinfo | ||||||
# The following command will install the IPA to your iOS device. | ||||||
$ ideviceinstaller -i iGoat-Swift_v1.0-frida-codesigned.ipa | ||||||
$ ideviceinstaller -i Uncrackable.ipa | ||||||
... | ||||||
Install: Complete | ||||||
# The following command will start the app in debug mode, by providing the bundle name. The bundle name can be found in the previous command after "Installing". | ||||||
$ idevicedebug -d run OWASP.iGoat-Swift | ||||||
``` | ||||||
|
||||||
## ipainstaller | ||||||
|
||||||
The IPA can also be directly installed on the iOS device via the command line with [ipainstaller](https://github.com/autopear/ipainstaller "IPA Installer"). After copying the file over to the device, for example via scp, you can execute ipainstaller with the IPA's filename: | ||||||
The IPA can also be directly installed on the iOS device via the command line with [ipainstaller](https://github.com/autopear/ipainstaller "IPA Installer"). Naturally, this requires a jailbroken device, as otherwise you cannot SSH into the device. After copying the file over to the device, for example via scp, you can execute ipainstaller with the IPA's filename: | ||||||
|
||||||
```bash | ||||||
ipainstaller App_name.ipa | ||||||
ipainstaller Uncrackable.ipa | ||||||
``` | ||||||
|
||||||
## ios-deploy | ||||||
|
@@ -56,18 +43,35 @@ On macOS you can also use the @MASTG-TOOL-0054 tool to install iOS apps from the | |||||
|
||||||
```bash | ||||||
unzip Name.ipa | ||||||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
ios-deploy --bundle 'Payload/Name.app' -W -d -v | ||||||
ios-deploy --bundle 'Payload/UnCrackable Level 1.app' -W -v | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
``` | ||||||
|
||||||
After the app is installed on the iOS device, you can simply start it by adding the `-m` flag which will directly start debugging without installing the app again. | ||||||
## xcrun | ||||||
|
||||||
After installing @MASTG-TOOL-0071, you can execute the following command to install a signed IPA: | ||||||
|
||||||
```bash | ||||||
ios-deploy --bundle 'Payload/Name.app' -W -d -v -m | ||||||
# Get the correct device id | ||||||
$ idevice_id | ||||||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
00008101-00FF28803FF9001E (USB) | ||||||
TheDauntless marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
$ xcrun devicectl device install app --device 00008101-00FF28803FF9001E ~/signed.ipa | ||||||
11:59:04 Acquired tunnel connection to device. | ||||||
11:59:04 Enabling developer disk image services. | ||||||
11:59:04 Acquired usage assertion. | ||||||
4%... 12%... 28%... 30%... 31%... 32%... 33%... 35%... 36%... 37%... 39%... 40%... 42%... 43%... 45%... 49%... 51%... 52%... 54%... 55%... 57%... 59%... 60%... 62%... 66%... 68%... 72%... 76%... 80%... 84%... 88%... 92%... 96%... Complete! | ||||||
App installed: | ||||||
• bundleID: org.mas.myapp | ||||||
• installationURL: file:///private/var/containers/Bundle/Application/DFC99D25-FC36-462E-91D2-18CDE717ED21/UnCrackable%20Level%201.app/ | ||||||
• launchServicesIdentifier: unknown | ||||||
• databaseUUID: DA52A5EB-5D39-4628-810E-8F42A5561CDF | ||||||
• databaseSequenceNumber: 1516 | ||||||
• options: | ||||||
``` | ||||||
|
||||||
## Xcode | ||||||
|
||||||
It is also possible to use the Xcode IDE to install iOS apps by doing the following steps: | ||||||
It is also possible to use the Xcode IDE to install iOS apps by executing the following steps: | ||||||
|
||||||
1. Start Xcode | ||||||
2. Select **Window/Devices and Simulators** | ||||||
|
@@ -89,20 +93,11 @@ Sometimes an application can require to be used on an iPad device. If you only h | |||||
</array> | ||||||
|
||||||
</dict> | ||||||
</plist> | ||||||
</plist> | ||||||
``` | ||||||
|
||||||
It is important to note that changing this value will break the original signature of the IPA file so you need to re-sign the IPA, after the update, in order to install it on a device on which the signature validation has not been disabled. | ||||||
|
||||||
This bypass might not work if the application requires capabilities that are specific to modern iPads while your iPhone or iPod is a bit older. | ||||||
|
||||||
Possible values for the property [UIDeviceFamily](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html#//apple_ref/doc/uid/TP40009252-SW11 "UIDeviceFamily property") can be found in the Apple Developer documentation. | ||||||
|
||||||
One fundamental step when analyzing apps is information gathering. This can be done by inspecting the app package on your host computer or remotely by accessing the app data on the device. You'll find more advance techniques in the subsequent chapters but, for now, we will focus on the basics: getting a list of all installed apps, exploring the app package and accessing the app data directories on the device itself. This should give you a bit of context about what the app is all about without even having to reverse engineer it or perform more advanced analysis. We will be answering questions such as: | ||||||
|
||||||
- Which files are included in the package? | ||||||
- Which Frameworks does the app use? | ||||||
- Which capabilities does the app require? | ||||||
- Which permissions does the app request to the user and for what reason? | ||||||
- Does the app allow any unsecured connections? | ||||||
- Does the app create any new files when being installed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TheDauntless I see that iOS 17 requires you to attach a debugger first to use the Gadget properly, right? Maybe we could add a sentence or two at the beginning of this paragraph to give this background. Then maybe we could have a subsection about LLDB and then another one about attaching to a process with Frida gadget?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not entirely. If the app is running in debug mode, Frida can automatically inject into it and the app doesn't need to have the gadget included. However, if you did include the gadget yourself (e.g. Objection / Sideloadly), then the frida gadget library is loaded at startup, and then you do need to connect with lldb followed by Frida.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this technique we include the gadget ourselves, so the LLDB will always be required, right? Would be nice to write a short context e.g.
I didn't test it myself but it looks like
iOS17 and newer
works for all iOS versions, no? In this case, maybe it's easier to skip theiOS16 and older
? Or at least make it clear thatiOS17 and newer
is universal?