Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #25: setProxy() function for iOS
Browse files Browse the repository at this point in the history
Unlike on Android, this current only supports setting a regular proxy.
See tweaselORG/meta#21 for WireGuard support.
baltpeter authored and zner0L committed Mar 21, 2023

Verified

This commit was signed with the committer’s verified signature.
zner0L Lorenz Sieben
1 parent eb373b9 commit 16e1c5b
Showing 5 changed files with 143 additions and 26 deletions.
36 changes: 18 additions & 18 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ An ID of a known permission on Android.

#### Defined in

[android.ts:639](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L639)
[android.ts:639](https://github.com/tweaselORG/platform-apis/blob/main/src/android.ts#L639)

___

@@ -58,7 +58,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the

#### Defined in

[index.ts:324](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L324)
[index.ts:327](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L327)

___

@@ -77,7 +77,7 @@ The options for each attribute available through the `getDeviceAttribute()` func

#### Defined in

[index.ts:330](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L330)
[index.ts:333](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L333)

___

@@ -89,7 +89,7 @@ An ID of a known permission on iOS.

#### Defined in

[ios.ts:222](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L222)
[ios.ts:329](https://github.com/tweaselORG/platform-apis/blob/main/src/ios.ts#L329)

___

@@ -126,14 +126,14 @@ Functions that are available for the platforms.
| `setAppBackgroundBatteryUsage` | `Platform` extends ``"android"`` ? (`appId`: `string`, `state`: ``"unrestricted"`` \| ``"optimized"`` \| ``"restricted"``) => `Promise`<`void`\> : `never` | Configure whether the app's background battery usage should be restricted. Currently only supported on Android. **`Param`** The app ID of the app to configure the background battery usage settings for. **`Param`** The state to set the background battery usage to. On Android, the possible values are: - `unrestricted`: "Allow battery usage in background without restrictions. May use more battery." - `optimized`: "Optimize based on your usage. Recommended for most apps." (default after installation) - `restricted`: "Restrict battery usage while in background. Apps may not work as expected. Notifications may be delayed." |
| `setAppPermissions` | (`appId`: `string`, `permissions?`: `Platform` extends ``"ios"`` ? { [p in IosPermission]?: "unset" \| "allow" \| "deny" } & { `location?`: ``"ask"`` \| ``"never"`` \| ``"always"`` \| ``"while-using"`` } : `Partial`<`Record`<`LiteralUnion`<[`AndroidPermission`](README.md#androidpermission), `string`\>, ``"allow"`` \| ``"deny"``\>\>) => `Promise`<`void`\> | Set the permissions for the app with the given app ID. By default, it will grant all known permissions (including dangerous permissions on Android) and set the location permission on iOS to `always`. You can specify which permissions to grant/deny using the `permissions` argument. Requires the `ssh` and `frida` capabilities on iOS. |
| `setClipboard` | (`text`: `string`) => `Promise`<`void`\> | Set the clipboard to the given text. Requires the `frida` capability on Android and iOS. |
| `setProxy` | `Platform` extends ``"android"`` ? (`proxy`: ``"wireguard"`` extends `Capability` ? [`WireGuardConfig`](README.md#wireguardconfig) : [`Proxy`](README.md#proxy) \| ``null``) => `Promise`<`void`\> : `never` | Set or disable the proxy on the device. If you have enabled the `wireguard` capability, this will start or stop a WireGuard tunnel. Otherwise, it will set the global proxy on the device. Currently only supported on Android. Enabling a WireGuard tunnel requires the `root` capability on Android. **`Remarks`** The WireGuard integration will create a new tunnel in the app called `appstraction` and delete it when the proxy is stopped. If you have an existing tunnel with the same name, it will be overridden. **`Param`** The proxy to set, or `null` to disable the proxy. If you have enabled the `wireguard` capability, this is a string of the full WireGuard configuration to use. |
| `setProxy` | `Platform` extends ``"android"`` ? (`proxy`: ``"wireguard"`` extends `Capability` ? [`WireGuardConfig`](README.md#wireguardconfig) : [`Proxy`](README.md#proxy) \| ``null``) => `Promise`<`void`\> : `Platform` extends ``"ios"`` ? (`proxy`: [`Proxy`](README.md#proxy) \| ``null``) => `Promise`<`void`\> : `never` | Set or disable the proxy on the device. If you have enabled the `wireguard` capability, this will start or stop a WireGuard tunnel. Otherwise, it will set the global proxy on the device. On iOS, the proxy is set for the current WiFi network. It won't apply for other networks or for cellular data connections. WireGuard is currently only supported on Android. Enabling a WireGuard tunnel requires the `root` capability. **`Remarks`** The WireGuard integration will create a new tunnel in the app called `appstraction` and delete it when the proxy is stopped. If you have an existing tunnel with the same name, it will be overridden. **`Param`** The proxy to set, or `null` to disable the proxy. If you have enabled the `wireguard` capability, this is a string of the full WireGuard configuration to use. |
| `startApp` | (`appId`: `string`) => `Promise`<`void`\> | Start the app with the given app ID. Doesn't wait for the app to be ready. Also enables the certificate pinning bypass if enabled. Requires the `frida` or `ssh` capability on iOS. On Android, this will start the app with or without a certificate pinning bypass depending on the `certificate-pinning-bypass` capability. |
| `stopApp` | `Platform` extends ``"android"`` ? (`appId`: `string`) => `Promise`<`void`\> : `never` | Force-stop the app with the given app ID. **`Param`** The app ID of the app to stop. |
| `uninstallApp` | (`appId`: `string`) => `Promise`<`void`\> | Uninstall the app with the given app ID. Will not fail if the app is not installed. This also removes any data stored by the app. |

#### Defined in

[index.ts:18](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L18)
[index.ts:18](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L18)

___

@@ -153,7 +153,7 @@ The options for the `platformApi()` function.

#### Defined in

[index.ts:262](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L262)
[index.ts:265](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L265)

___

@@ -172,7 +172,7 @@ Connection details for a proxy.

#### Defined in

[index.ts:338](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L338)
[index.ts:341](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L341)

___

@@ -202,7 +202,7 @@ The options for a specific platform/run target combination.

#### Defined in

[index.ts:289](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L289)
[index.ts:292](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L292)

___

@@ -220,7 +220,7 @@ A capability for the `platformApi()` function.

#### Defined in

[index.ts:317](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L317)
[index.ts:320](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L320)

___

@@ -232,7 +232,7 @@ A platform that is supported by this library.

#### Defined in

[index.ts:9](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L9)
[index.ts:9](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L9)

___

@@ -250,7 +250,7 @@ A run target that is supported by this library for the given platform.

#### Defined in

[index.ts:11](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L11)
[index.ts:11](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L11)

___

@@ -262,7 +262,7 @@ Configuration string for WireGuard.

#### Defined in

[index.ts:345](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L345)
[index.ts:348](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L348)

## Variables

@@ -274,7 +274,7 @@ The IDs of known permissions on Android.

#### Defined in

[android.ts:508](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L508)
[android.ts:508](https://github.com/tweaselORG/platform-apis/blob/main/src/android.ts#L508)

___

@@ -286,7 +286,7 @@ The IDs of known permissions on iOS.

#### Defined in

[ios.ts:205](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L205)
[ios.ts:312](https://github.com/tweaselORG/platform-apis/blob/main/src/ios.ts#L312)

## Functions

@@ -312,7 +312,7 @@ The an object with the app ID and version, or `undefined` if the file doesn't ex

#### Defined in

[util.ts:49](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L49)
[util.ts:50](https://github.com/tweaselORG/platform-apis/blob/main/src/util.ts#L50)

___

@@ -334,7 +334,7 @@ Pause for a given duration.

#### Defined in

[util.ts:35](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L35)
[util.ts:36](https://github.com/tweaselORG/platform-apis/blob/main/src/util.ts#L36)

___

@@ -366,4 +366,4 @@ The API object for the given platform and run target.

#### Defined in

[index.ts:354](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L354)
[index.ts:357](https://github.com/tweaselORG/platform-apis/blob/main/src/index.ts#L357)
8 changes: 7 additions & 1 deletion examples/ios-device.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
import { parseAppMeta, pause, platformApi } from '../src/index';

// You can pass the following command line arguments:
// `npx tsx examples/ios-device.ts <ip> <app path>`
// `npx tsx examples/ios-device.ts <ip> <app path> <proxy host?> <proxy port?>`

(async () => {
const ios = platformApi({
@@ -15,9 +15,13 @@ import { parseAppMeta, pause, platformApi } from '../src/index';
});

const appPath = process.argv[3] || '/path/to/app-files';
const proxyHost = process.argv[4];
const proxyPort = process.argv[5];

await ios.ensureDevice();

if (proxyHost && proxyPort) await ios.setProxy({ host: proxyHost, port: +proxyPort });

await ios.setClipboard('I copied this.');

const appMeta = await parseAppMeta(appPath);
@@ -44,5 +48,7 @@ import { parseAppMeta, pause, platformApi } from '../src/index';
// `clearStuckModals()` is currently broken on iOS (see https://github.com/tweaselORG/appstraction/issues/12).
// await ios.clearStuckModals();
await ios.uninstallApp(appId);

if (proxyHost && proxyPort) await ios.setProxy(null);
})();
/* eslint-enable no-console */
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -224,9 +224,10 @@ export type PlatformApi<
* Set or disable the proxy on the device. If you have enabled the `wireguard` capability, this will start or stop a
* WireGuard tunnel. Otherwise, it will set the global proxy on the device.
*
* Currently only supported on Android.
* On iOS, the proxy is set for the current WiFi network. It won't apply for other networks or for cellular data
* connections.
*
* Enabling a WireGuard tunnel requires the `root` capability on Android.
* WireGuard is currently only supported on Android. Enabling a WireGuard tunnel requires the `root` capability.
*
* @remarks
* The WireGuard integration will create a new tunnel in the app called `appstraction` and delete it when the proxy
@@ -236,6 +237,8 @@ export type PlatformApi<
*/
setProxy: Platform extends 'android'
? (proxy: ('wireguard' extends Capability ? WireGuardConfig : Proxy) | null) => Promise<void>
: Platform extends 'ios'
? (proxy: Proxy | null) => Promise<void>
: never;

/** @ignore */
111 changes: 109 additions & 2 deletions src/ios.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execa } from 'execa';
import frida from 'frida';
import type { PlatformApi, PlatformApiOptions, SupportedCapability, SupportedRunTarget } from '.';
import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.';
import { asyncUnimplemented, getObjFromFridaScript, isRecord } from './util';

const fridaScripts = {
@@ -26,6 +26,92 @@ send({ name: "get_obj_from_frida_script", payload: idfv });`,
`ObjC.classes.CLLocationManager.setAuthorizationStatusByType_forBundleIdentifier_(${value}, "${appId}");`,
startApp: (appId: string) =>
`ObjC.classes.LSApplicationWorkspace.defaultWorkspace().openApplicationWithBundleID_("${appId}");`,
/**
* @param options If `options` is falsy, the proxy will be disabled. Otherwise, it will be set according to the
* properties and enabled. If both `options.username` and `options.password` are provided, the proxy will be
* configured to use authentication.
*/
setProxy: (options: Proxy | null) => `function setProxySettingsForCurrentWifiNetwork(options) {
var NSString = ObjC.classes.NSString;
var NSNumber = ObjC.classes.NSNumber;
var authenticated = options && options.username && options.password;
var defaultProxySettings = ObjC.classes.WFSettingsProxy.defaultProxyConfiguration();
// Sometimes, currentNetwork() returns null, so we have to try a few times.
// See: https://github.com/tweaselORG/appstraction/issues/25#issuecomment-1447996021
var ssid;
for (let i = 0; i < 100; i++) {
var currentNetwork = ObjC.classes.WFClient.sharedInstance().interface().currentNetwork();
if (currentNetwork) {
ssid = currentNetwork.ssid();
break;
}
}
var newSettingsDict = ObjC.classes.NSMutableDictionary.alloc().initWithDictionary_(defaultProxySettings);
if (options) {
newSettingsDict.setObject_forKey_(NSNumber.numberWithInt_(1), NSString.stringWithString_('HTTPEnable'));
newSettingsDict.setObject_forKey_(NSNumber.numberWithInt_(options.port), NSString.stringWithString_('HTTPPort'));
newSettingsDict.setObject_forKey_(NSString.stringWithString_(options.host), NSString.stringWithString_('HTTPProxy'));
newSettingsDict.setObject_forKey_(NSNumber.numberWithInt_(1), NSString.stringWithString_('HTTPSEnable'));
newSettingsDict.setObject_forKey_(NSNumber.numberWithInt_(options.port), NSString.stringWithString_('HTTPSPort'));
newSettingsDict.setObject_forKey_(NSString.stringWithString_(options.host), NSString.stringWithString_('HTTPSProxy'));
if (authenticated) {
newSettingsDict.setObject_forKey_(NSNumber.numberWithInt_(1), NSString.stringWithString_('HTTPProxyAuthenticated'));
newSettingsDict.setObject_forKey_(NSString.stringWithString_(options.username), NSString.stringWithString_('HTTPProxyUsername'));
}
}
var newSettings = ObjC.classes.WFSettingsProxy.alloc().initWithDictionary_(newSettingsDict);
if (authenticated) newSettings.setPassword_(options.password);
var arrayWithNewSettings = ObjC.classes.NSMutableArray.alloc().init();
arrayWithNewSettings.addObject_(newSettings);
var saveSettingsOperation = ObjC.classes.WFSaveSettingsOperation.alloc().initWithSSID_settings_(ssid, arrayWithNewSettings);
saveSettingsOperation.setCurrentNetwork_(1);
saveSettingsOperation.start();
}
setProxySettingsForCurrentWifiNetwork(${JSON.stringify(options)});`,
getProxy: `// Taken from: https://codeshare.frida.re/@dki/ios-app-info/
function dictFromNSDictionary(nsDict) {
var jsDict = {};
var keys = nsDict.allKeys();
var count = keys.count();
for (var i = 0; i < count; i++) {
var key = keys.objectAtIndex_(i);
var value = nsDict.objectForKey_(key);
jsDict[key.toString()] = value.toString();
}
return jsDict;
}
function getProxySettingsForCurrentWifiNetwork() {
var ssid;
for (let i = 0; i < 100; i++) {
var currentNetwork = ObjC.classes.WFClient.sharedInstance().interface().currentNetwork();
if (currentNetwork) {
ssid = currentNetwork.ssid();
break;
}
}
var gso = ObjC.classes.WFGetSettingsOperation.alloc().initWithSSID_(ssid);
gso.start();
var settings = gso.settings().proxySettings();
var dict = dictFromNSDictionary(settings.items());
if (settings.password()) dict.HTTPProxyPassword = settings.password().toString();
return dict;
}
send({ name: "get_obj_from_frida_script", payload: getProxySettingsForCurrentWifiNetwork() });`,
} as const;

export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
@@ -198,7 +284,28 @@ export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(

installCertificateAuthority: asyncUnimplemented('installCertificateAuthority') as never,
removeCertificateAuthority: asyncUnimplemented('removeCertificateAuthority') as never,
setProxy: asyncUnimplemented('setProxy') as never,
async setProxy(proxy) {
if (!options.capabilities.includes('frida')) throw new Error('Frida is required for configuring a proxy.');

// Set proxy settings.
const session = await frida.getUsbDevice().then((f) => f.attach('SpringBoard'));
const script = await session.createScript(fridaScripts.setProxy(proxy));
await script.load();
await session.detach();

// Verify that the proxy settings were set.
const proxySettings = await getObjFromFridaScript('SpringBoard', fridaScripts.getProxy);
if (
!isRecord(proxySettings) ||
(proxy === null && (proxySettings['HTTPProxy'] || proxySettings['HTTPSProxy'])) ||
(proxy !== null &&
(proxySettings['HTTPProxy'] !== proxy?.host ||
proxySettings['HTTPPort'] !== proxy?.port + '' ||
proxySettings['HTTPSProxy'] !== proxy?.host ||
proxySettings['HTTPSPort'] !== proxy?.port + ''))
)
throw new Error('Failed to set proxy.');
},
});

/** The IDs of known permissions on iOS. */
7 changes: 4 additions & 3 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { execa } from 'execa';
import type { TargetProcess } from 'frida';
import frida from 'frida';
import fs from 'fs-extra';
import _ipaInfo from 'ipa-extract-info';
@@ -78,10 +79,10 @@ export const ipaInfo = async (ipaPath: string) => {
return await _ipaInfo(fd);
};

export const getObjFromFridaScript = async (pid: number | undefined, script: string) => {
if (!pid) throw new Error('Must provide pid.');
export const getObjFromFridaScript = async (targetProcess: TargetProcess | undefined, script: string) => {
if (!targetProcess) throw new Error('Must provide targetProcess.');
const fridaDevice = await frida.getUsbDevice();
const fridaSession = await fridaDevice.attach(pid);
const fridaSession = await fridaDevice.attach(targetProcess);
const fridaScript = await fridaSession.createScript(script);
const resultPromise = new Promise<unknown>((res, rej) => {
fridaScript.message.connect((message) => {

0 comments on commit 16e1c5b

Please sign in to comment.