From 474c2f163f4705020529b7122e0fdda2434e66ad Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Tue, 11 Apr 2023 16:49:31 +0200 Subject: [PATCH] Fixes #10: Implement traffic collection on iOS --- docs/README.md | 36 +++++------ src/index.ts | 170 +++++++++++++++++++++++++------------------------ src/util.ts | 4 +- 3 files changed, 106 insertions(+), 104 deletions(-) diff --git a/docs/README.md b/docs/README.md index 31f8022..90e4baa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,7 +57,7 @@ Functions that can be used to instrument the device and analyze apps. | `platform` | [`PlatformApi`](README.md#platformapi)<`Platform`, `RunTarget`, `Capabilities`\> | A raw platform API object as returned by [appstraction](https://github.com/tweaselORG/appstraction). | | `resetDevice` | () => `Promise`<`void`\> | Reset the specified device to the snapshot specified in `targetOptions.snapshotName`. | | `startAppAnalysis` | (`appPath`: `Platform` extends ``"android"`` ? `string` \| `string`[] : `string`, `options?`: { `noSigint?`: `boolean` ; `resetApp?`: `boolean` }) => `Promise`<[`AppAnalysis`](README.md#appanalysis)<`Platform`, `RunTarget`, `Capabilities`\>\> | Start an app analysis. The app analysis is controlled through the returned object. Remember to call `stop()` on the object when you are done with the app to clean up and retrieve the analysis data. | -| `startTrafficCollection` | (`options?`: [`TrafficCollectionOptions`](README.md#trafficcollectionoptions)) => `Promise`<`void`\> | Start collecting the device's traffic. This will start a WireGuard proxy on the host computer on port `51820`. It will automatically configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. You can configure which apps to include using the `options` parameter. Only available on Android. Only one traffic collection can be active at a time. | +| `startTrafficCollection` | (`options?`: `Platform` extends ``"android"`` ? [`TrafficCollectionOptions`](README.md#trafficcollectionoptions) : `never`) => `Promise`<`void`\> | Start collecting the device's traffic. On Android, this will start a WireGuard proxy on the host computer on port `51820`. It will automatically configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. You can configure which apps to include using the `options` parameter. On iOS, this will start a mitmproxy HTTP(S) proxy on the host computer on port `8080`. It will automatically configure the target to use the proxy and trust the mitmproxy TLS certificate. You can not restrict the traffic collection to specific apps. Only one traffic collection can be active at a time. | | `stop` | () => `Promise`<`void`\> | Stop the analysis. This is important for clean up, e.g. stopping the emulator if it is managed by this library. | | `stopTrafficCollection` | () => `Promise`<`Har`\> | Stop collecting the device's traffic. This will stop the proxy on the host computer. | @@ -69,7 +69,7 @@ ___ ### AnalysisOptions -Ƭ **AnalysisOptions**<`Platform`, `RunTarget`, `Capabilities`\>: { `capabilities`: `Capabilities` ; `platform`: `Platform` ; `runTarget`: `RunTarget` } & [`RunTargetOptions`](README.md#runtargetoptions)<`Capabilities`\>[`Platform`][`RunTarget`] extends `object` ? { `targetOptions`: [`RunTargetOptions`](README.md#runtargetoptions)<`Capabilities`\>[`Platform`][`RunTarget`] } : { `targetOptions?`: `Record`<`string`, `never`\> } +Ƭ **AnalysisOptions**<`Platform`, `RunTarget`, `Capabilities`\>: { `capabilities`: `Capabilities` ; `platform`: `Platform` ; `runTarget`: `RunTarget` } & [`RunTargetOptions`](README.md#runtargetoptions)[`Platform`][`RunTarget`] extends `object` ? { `targetOptions`: [`RunTargetOptions`](README.md#runtargetoptions)[`Platform`][`RunTarget`] } : { `targetOptions?`: `Record`<`string`, `never`\> } The options for the `startAnalysis()` function. @@ -83,7 +83,7 @@ The options for the `startAnalysis()` function. #### Defined in -[cyanoacrylate/src/index.ts:249](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L249) +[cyanoacrylate/src/index.ts:252](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L252) ___ @@ -140,15 +140,15 @@ Functions that can be used to control an app analysis. | `installApp` | () => `Promise`<`void`\> | Install the specified app. **`See`** [PlatformApi](README.md#platformapi) | | `setAppPermissions` | (`permissions?`: `Parameters`<[`PlatformApi`](README.md#platformapi)<`Platform`, `RunTarget`, `Capabilities`\>[``"setAppPermissions"``]\>[``1``]) => `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. **`See`** [PlatformApi](README.md#platformapi) | | `startApp` | () => `Promise`<`void`\> | Start the app. **`See`** [PlatformApi](README.md#platformapi) | -| `startTrafficCollection` | (`name?`: `string`) => `Promise`<`void`\> | Start collecting the traffic of only this app. This will start a WireGuard proxy on the host computer on port `51820`. It will automatically configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. Only available on Android. Only one traffic collection can be active at a time. | +| `startTrafficCollection` | (`name?`: `string`) => `Promise`<`void`\> | Start collecting the traffic of only this app on Android and of the whole device on iOS. On Android, this will start a WireGuard proxy on the host computer on port `51820`. It will automatically configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. On iOS, this will start a mitmproxy HTTP(S) proxy on the host computer on port `8080`. It will automatically configure the target to use the proxy and trust the mitmproxy TLS certificate. Only one traffic collection can be active at a time. | | `stop` | (`options?`: { `uninstallApp?`: `boolean` }) => `Promise`<[`AppAnalysisResult`](README.md#appanalysisresult)\> | Stop the app analysis and return the collected data. | | `stopApp` | () => `Promise`<`void`\> | Force-stop the app. **`See`** [PlatformApi](README.md#platformapi) | -| `stopTrafficCollection` | () => `Promise`<`void`\> | Stop collecting the app's traffic. This will stop the proxy on the host computer. The collected traffic is available from the `traffic` property of the object returned by `stop()`. | +| `stopTrafficCollection` | () => `Promise`<`void`\> | Stop collecting the app's (or, on iOS, the device's) traffic. This will stop the proxy on the host computer. The collected traffic is available from the `traffic` property of the object returned by `stop()`. | | `uninstallApp` | () => `Promise`<`void`\> | Uninstall the app. **`See`** [PlatformApi](README.md#platformapi) | #### Defined in -[cyanoacrylate/src/index.ts:110](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L110) +[cyanoacrylate/src/index.ts:115](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L115) ___ @@ -167,7 +167,7 @@ The result of an app analysis. #### Defined in -[cyanoacrylate/src/index.ts:195](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L195) +[cyanoacrylate/src/index.ts:201](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L201) ___ @@ -266,17 +266,10 @@ ___ ### RunTargetOptions -Ƭ **RunTargetOptions**<`Capabilities`, `Capability`\>: `Object` +Ƭ **RunTargetOptions**: `Object` The options for a specific platform/run target combination. -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `Capabilities` | extends [`SupportedCapability`](README.md#supportedcapability)<``"android"`` \| ``"ios"``\>[] | -| `Capability` | `Capabilities`[`number`] | - #### Type declaration | Name | Type | Description | @@ -290,19 +283,22 @@ The options for a specific platform/run target combination. | `android.emulator.startEmulatorOptions.emulatorName?` | `string` | The name of the emulator to start. | | `android.emulator.startEmulatorOptions.ephemeral?` | `boolean` | Whether to discard all changes when exiting the emulator (default: `true`). | | `android.emulator.startEmulatorOptions.headless?` | `boolean` | Whether to start the emulator in headless mode (default: `false`). | -| `ios` | { `device`: ``"ssh"`` extends `Capability` ? { `ip`: `string` ; `rootPw?`: `string` } : `unknown` ; `emulator`: `never` } | The options for the iOS platform. | -| `ios.device` | ``"ssh"`` extends `Capability` ? { `ip`: `string` ; `rootPw?`: `string` } : `unknown` | The options for the iOS physical device run target. | +| `ios` | { `device`: { `ip`: `string` ; `proxyIp`: `string` ; `rootPw?`: `string` } ; `emulator`: `never` } | The options for the iOS platform. | +| `ios.device` | { `ip`: `string` ; `proxyIp`: `string` ; `rootPw?`: `string` } | The options for the iOS physical device run target. | +| `ios.device.ip` | `string` | The device's IP address. | +| `ios.device.proxyIp` | `string` | The IP address of the host running the proxy to set up on the device. | +| `ios.device.rootPw?` | `string` | The password of the root user on the device, defaults to `alpine` if not set. | | `ios.emulator` | `never` | The options for the iOS emulator run target. | #### Defined in -[cyanoacrylate/src/index.ts:207](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L207) +[cyanoacrylate/src/index.ts:213](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L213) ___ ### SupportedCapability -Ƭ **SupportedCapability**<`Platform`\>: `Platform` extends ``"android"`` ? ``"frida"`` \| ``"certificate-pinning-bypass"`` : `Platform` extends ``"ios"`` ? ``"ssh"`` \| ``"frida"`` : `never` +Ƭ **SupportedCapability**<`Platform`\>: `Platform` extends ``"android"`` ? ``"frida"`` \| ``"certificate-pinning-bypass"`` : `Platform` extends ``"ios"`` ? `never` : `never` A capability supported by this library. @@ -439,4 +435,4 @@ An object that can be used to instrument the device and analyze apps. #### Defined in -[cyanoacrylate/src/index.ts:282](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L282) +[cyanoacrylate/src/index.ts:286](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L286) diff --git a/src/index.ts b/src/index.ts index b41e9c9..9616046 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import type { PlatformApi, SupportedPlatform, SupportedRunTarget } from 'appstraction'; +import type { PlatformApi, PlatformApiOptions, SupportedPlatform, SupportedRunTarget } from 'appstraction'; import { parseAppMeta, platformApi } from 'appstraction'; import { getDirname } from 'cross-dirname'; import type { ExecaChildProcess } from 'execa'; @@ -19,7 +19,7 @@ const __dirname = getDirname(); export type SupportedCapability = Platform extends 'android' ? 'frida' | 'certificate-pinning-bypass' : Platform extends 'ios' - ? 'ssh' | 'frida' + ? never : never; /** Metadata about an app. */ @@ -85,17 +85,22 @@ export type Analysis< options?: { resetApp?: boolean; noSigint?: boolean } ) => Promise>; /** - * Start collecting the device's traffic. This will start a WireGuard proxy on the host computer on port `51820`. It - * will automatically configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. You - * can configure which apps to include using the `options` parameter. + * Start collecting the device's traffic. * - * Only available on Android. + * On Android, this will start a WireGuard proxy on the host computer on port `51820`. It will automatically + * configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. You can configure which + * apps to include using the `options` parameter. + * + * On iOS, this will start a mitmproxy HTTP(S) proxy on the host computer on port `8080`. It will automatically + * configure the target to use the proxy and trust the mitmproxy TLS certificate. You can not restrict the traffic + * collection to specific apps. * * Only one traffic collection can be active at a time. * * @param options Set which apps to include in the traffic collection. If not specified, all apps will be included. + * Only available on Android. */ - startTrafficCollection: (options?: TrafficCollectionOptions) => Promise; + startTrafficCollection: (options?: Platform extends 'android' ? TrafficCollectionOptions : never) => Promise; /** * Stop collecting the device's traffic. This will stop the proxy on the host computer. * @@ -160,12 +165,13 @@ export type AppAnalysis< stopApp: () => Promise; /** - * Start collecting the traffic of only this app. + * Start collecting the traffic of only this app on Android and of the whole device on iOS. * - * This will start a WireGuard proxy on the host computer on port `51820`. It will automatically configure the - * target to use the WireGuard proxy and trust the mitmproxy TLS certificate. + * On Android, this will start a WireGuard proxy on the host computer on port `51820`. It will automatically + * configure the target to use the WireGuard proxy and trust the mitmproxy TLS certificate. * - * Only available on Android. + * On iOS, this will start a mitmproxy HTTP(S) proxy on the host computer on port `8080`. It will automatically + * configure the target to use the proxy and trust the mitmproxy TLS certificate. * * Only one traffic collection can be active at a time. * @@ -173,7 +179,7 @@ export type AppAnalysis< */ startTrafficCollection: (name?: string) => Promise; /** - * Stop collecting the app's traffic. This will stop the proxy on the host computer. + * Stop collecting the app's (or, on iOS, the device's) traffic. This will stop the proxy on the host computer. * * The collected traffic is available from the `traffic` property of the object returned by `stop()`. */ @@ -204,10 +210,7 @@ export type AppAnalysisResult = { /** The options for a specific platform/run target combination. */ // Use `unknown` here to mean "no options", and `never` to mean "not supported". -export type RunTargetOptions< - Capabilities extends SupportedCapability<'android' | 'ios'>[], - Capability = Capabilities[number] -> = { +export type RunTargetOptions = { /** The options for the Android platform. */ android: { /** The options for the Android emulator run target. */ @@ -234,14 +237,14 @@ export type RunTargetOptions< /** The options for the iOS emulator run target. */ emulator: never; /** The options for the iOS physical device run target. */ - device: 'ssh' extends Capability - ? { - /** The password of the root user on the device, defaults to `alpine` if not set. */ - rootPw?: string; - /** The device's IP address. */ - ip: string; - } - : unknown; + device: { + /** The password of the root user on the device, defaults to `alpine` if not set. */ + rootPw?: string; + /** The device's IP address. */ + ip: string; + /** The IP address of the host running the proxy to set up on the device. */ + proxyIp: string; + }; }; }; @@ -258,13 +261,14 @@ export type AnalysisOptions< /** * The capabilities you want. Depending on what you're trying to do, you may not need or want to root the device, * install Frida, etc. In this case, you can exclude those capabilities. This will influence which functions you can - * run. + * run. For Android, the `wireguard` and `root` capabilities are preset in appstraction. For iOS, both the `ssh` and + * `frida` capibilities are preset, since they are required for the analysis to work. */ capabilities: Capabilities; -} & (RunTargetOptions[Platform][RunTarget] extends object +} & (RunTargetOptions[Platform][RunTarget] extends object ? { /** The options for the selected platform/run target combination. */ - targetOptions: RunTargetOptions[Platform][RunTarget]; + targetOptions: RunTargetOptions[Platform][RunTarget]; } : { /** The options for the selected platform/run target combination. */ @@ -287,28 +291,20 @@ export function startAnalysis< const platformOptions = { platform: analysisOptions.platform, runTarget: analysisOptions.runTarget, - capabilities: analysisOptions.capabilities, + capabilities: + analysisOptions.platform === 'android' + ? [...analysisOptions.capabilities, 'wireguard', 'root'] + : analysisOptions.platform === 'ios' + ? ['ssh', 'frida'] + : analysisOptions.capabilities, // eslint-disable-next-line @typescript-eslint/no-explicit-any targetOptions: analysisOptions.targetOptions as any, - }; + } as unknown as PlatformApiOptions; const venvPath = join(__dirname, '../.venv/bin'); process.env['PATH'] = `${venvPath}:${process.env['PATH']}`; - const platform = ( - analysisOptions.platform === 'android' - ? platformApi({ - ...platformOptions, - platform: 'android', - // The casting is needed because TypeScript doesn't understand that capabilities is already as expected. - capabilities: [ - ...(analysisOptions.capabilities as SupportedCapability<'android'>[]), - 'wireguard', - 'root', - ], - }) - : platformApi(platformOptions) - ) as PlatformApi; + const platform = platformApi(platformOptions); let emulatorProcess: ExecaChildProcess | undefined; let trafficCollectionInProgress = false; @@ -317,10 +313,6 @@ export function startAnalysis< const startTrafficCollection = async (options: TrafficCollectionOptions | undefined) => { if (trafficCollectionInProgress) throw new Error('Cannot start new traffic collection. A previous one is still running.'); - if (analysisOptions.platform === 'ios') - throw new Error( - 'Unimplemented: Missing WireGuard support on iOS in appstraction prevents traffic collection (see https://github.com/tweaselORG/cyanoacrylate/issues/10).' - ); trafficCollectionInProgress = true; @@ -328,30 +320,31 @@ export function startAnalysis< const harOutputPath = temporaryFile({ extension: 'har' }); + const mitmproxyOptions = [ + '-s', + join(__dirname, '../mitmproxy-addons/ipcEventsAddon.py'), + '-s', + join(__dirname, '../mitmproxy-addons/har_dump.py'), + '--set', + `hardump=${harOutputPath}`, + '--set', + 'ipcPipeFd=3', + ]; + if (analysisOptions.platform === 'android') mitmproxyOptions.push('--mode', 'wireguard'); + mitmproxyState = { - proc: execa( - 'mitmdump', - [ - '--mode', - 'wireguard', - '-s', - join(__dirname, '../mitmproxy-addons/ipcEventsAddon.py'), - '-s', - join(__dirname, '../mitmproxy-addons/har_dump.py'), - '--set', - `hardump=${harOutputPath}`, - '--set', - 'ipcPipeFd=3', - ], - { - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], - } - ), + proc: execa('mitmdump', mitmproxyOptions, { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }), harOutputPath, }; - await timeout( - Promise.all([ + const mitmproxyPromises: Promise[] = [ + awaitMitmproxyEvent(mitmproxyState.proc, (msg) => msg.status === 'running'), + ]; + + if (analysisOptions.platform === 'android') + mitmproxyPromises.push( awaitMitmproxyEvent( mitmproxyState.proc, (msg) => @@ -390,13 +383,27 @@ export function startAnalysis< > ).setProxy(stringifyIni(parsedWireguardConf)); } - }), - awaitMitmproxyEvent(mitmproxyState.proc, (msg) => msg.status === 'running'), - ]), - { - milliseconds: 30000, - } - ).catch((e) => { + }) + ); + else if (analysisOptions.platform === 'ios') + mitmproxyPromises.push( + awaitMitmproxyEvent( + mitmproxyState.proc, + (msg) => + msg.status === 'proxyChanged' && + msg.servers.some((server) => server.type === 'regular' && server.is_running) + ).then((msg) => + (platform as unknown as PlatformApi<'ios', 'device', Array<'ssh'>, 'ssh'>).setProxy({ + host: (analysisOptions as unknown as AnalysisOptions<'ios', 'device', never>).targetOptions + .proxyIp, + port: msg.status === 'proxyChanged' ? msg.servers[0]?.listen_addrs?.[0]?.[1] || 8080 : 8080, + }) + ) + ); + + await timeout(Promise.all(mitmproxyPromises), { + milliseconds: 30000, + }).catch((e) => { if (e.name === 'TimeoutError') throw new TimeoutError('Starting mitmproxy failed after a timeout.'); throw e; }); @@ -419,10 +426,10 @@ export function startAnalysis< ); } - if (mitmproxyState?.wireguardConf) - await ( - platform as unknown as PlatformApi<'android', RunTarget, Array<'wireguard'>, 'wireguard'> - ).setProxy(null); + await ( + platform as unknown as PlatformApi<'android', RunTarget, Array<'wireguard'>, 'wireguard'> + ).setProxy(null); + await platform.removeCertificateAuthority(join(homedir(), '.mitmproxy/mitmproxy-ca-cert.pem')); /* eslint-disable require-atomic-updates */ trafficCollectionInProgress = false; @@ -450,7 +457,7 @@ export function startAnalysis< analysisOptions.runTarget === 'emulator' ) { const targetOptions = analysisOptions.targetOptions as - | RunTargetOptions['android']['emulator'] + | RunTargetOptions['android']['emulator'] | undefined; const emulatorName = targetOptions?.startEmulatorOptions?.emulatorName; if (emulatorName) { @@ -481,9 +488,8 @@ export function startAnalysis< if (analysisOptions.platform !== 'android' || analysisOptions.runTarget !== 'emulator') throw new Error('Resetting devices is only supported for Android emulators.'); - const snapshotName = ( - analysisOptions.targetOptions as RunTargetOptions['android']['emulator'] - )?.snapshotName; + const snapshotName = (analysisOptions.targetOptions as RunTargetOptions['android']['emulator']) + ?.snapshotName; if (!snapshotName) throw new Error('Cannot reset device: No snapshot name specified.'); return timeout(platform.resetDevice(snapshotName), { diff --git a/src/util.ts b/src/util.ts index b5eed2e..369320a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -56,14 +56,14 @@ type MitmproxyEvent = | { status: 'proxyChanged'; /** An array of server specs which contains all the running servers, one for each mode. */ - servers: MitmproxyServerSpec<'wireguard' | string>[]; + servers: MitmproxyServerSpec<'wireguard' | 'regular' | string>[]; }; /** * The JSON serialization of the python class mitmproxy.proxy.mode_servers.ServerInstance. See * https://github.com/mitmproxy/mitmproxy/blob/8f1329377147538afdf06344179c2fd90795e93a/mitmproxy/proxy/mode_servers.py#L172. */ -type MitmproxyServerSpec = { +type MitmproxyServerSpec = { type: Type; description: string; full_spec: string;