Skip to content

Commit

Permalink
Fixes #81: Implement a function to enable supervision mode on jailbro…
Browse files Browse the repository at this point in the history
…ken devices
  • Loading branch information
zner0L committed Jun 1, 2023
1 parent 75f283a commit 74d9ef0
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 40 deletions.
24 changes: 12 additions & 12 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the

#### Defined in

[index.ts:374](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L374)
[index.ts:394](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L394)

___

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

#### Defined in

[index.ts:380](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L380)
[index.ts:400](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L400)

___

Expand All @@ -112,7 +112,7 @@ An ID of a known permission on iOS.

#### Defined in

[ios.ts:388](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L388)
[ios.ts:503](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L503)

___

Expand Down Expand Up @@ -199,7 +199,7 @@ The options for the `platformApi()` function.

#### Defined in

[index.ts:312](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L312)
[index.ts:332](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L332)

___

Expand All @@ -218,7 +218,7 @@ Connection details for a proxy.

#### Defined in

[index.ts:388](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L388)
[index.ts:408](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L408)

___

Expand Down Expand Up @@ -248,7 +248,7 @@ The options for a specific platform/run target combination.

#### Defined in

[index.ts:339](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L339)
[index.ts:359](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L359)

___

Expand All @@ -266,7 +266,7 @@ A capability for the `platformApi()` function.

#### Defined in

[index.ts:367](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L367)
[index.ts:387](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L387)

___

Expand Down Expand Up @@ -308,7 +308,7 @@ Configuration string for WireGuard.

#### Defined in

[index.ts:395](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L395)
[index.ts:415](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L415)

## Variables

Expand All @@ -332,7 +332,7 @@ The IDs of known permissions on iOS.

#### Defined in

[ios.ts:371](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L371)
[ios.ts:486](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L486)

## Functions

Expand Down Expand Up @@ -371,7 +371,7 @@ An object with the properties listed above, or `undefined` if the file doesn't e

#### Defined in

[utils/index.ts:64](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L64)
[utils/index.ts:63](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L63)

___

Expand All @@ -393,7 +393,7 @@ Pause for a given duration.

#### Defined in

[utils/index.ts:41](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L41)
[utils/index.ts:40](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L40)

___

Expand Down Expand Up @@ -425,4 +425,4 @@ The API object for the given platform and run target.

#### Defined in

[index.ts:404](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L404)
[index.ts:424](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L424)
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@
"andromatic": "^1.0.0",
"autopy": "1.0.0",
"asn1js": "^3.0.5",
"bplist-creator": "^0.1.1",
"bplist-parser": "^0.3.2",
"cross-fetch": "^3.1.5",
"execa": "^6.1.0",
"file-type": "^18.3.0",
"frida": "^16.0.8",
"fs-extra": "^11.1.0",
"global-cache-dir": "^5.0.0",
"ipa-extract-info": "^1.2.6",
"node-ssh": "^13.1.0",
"p-retry": "^5.1.2",
Expand Down
20 changes: 20 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,26 @@ export type PlatformApi<
: Platform extends 'ios'
? {
ssh: NodeSSH['execCommand'];
/**
* Ensures that the current host is configured to supervise the connected device. If this is not the case,
* it sets the host to be the (only) supervisor by installing its certificate on the device. This will
* overwrite parts of the exisiting CloudConfiguration. If there is no host certificate, yet, or it has
* expired, it will be generated.
*
* Might restart the device, if a new cofniguration is pushed. You are adviced to wait for the device.
*/
ensureSupervision: () => Promise<void>;
/**
* Removes all configured supervision hosts from the device.
*
* Will restart the device. You are adviced to wait for the device.
*/
removeSupervision: () => Promise<void>;
/**
* Restarts the device only in the userspace, e.g. to keep the jailbroken kernel running. You might want
* to wait for the device to ensure it is available again.
*/
userspaceRestart: () => Promise<void>;
}
: never;
};
Expand Down
122 changes: 118 additions & 4 deletions src/ios.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { getVenv } from 'autopy';
import bplist from 'bplist-creator';
import { parseFile } from 'bplist-parser';
import { createHash } from 'crypto';
import { execa } from 'execa';
import frida from 'frida';
import { exists,mkdirp } from 'fs-extra';
import { readFile,writeFile } from 'fs/promises';
import globalCacheDir from 'global-cache-dir';
import { NodeSSH } from 'node-ssh';
import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.';
import { join } from 'path';
import type { PlatformApi,PlatformApiOptions,Proxy,SupportedCapability,SupportedRunTarget } from '.';
import { venvOptions } from '../scripts/common/python';
import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils';
import { parsePemCertificateFromFile} from './utils/crypto';

import { asyncUnimplemented,getObjFromFridaScript,isRecord,retryCondition } from './utils';
import {
arrayBufferToPem,
certificateFingerprint,
certificateHasExpired,
generateCertificate,
parsePemCertificateFromFile,
pemToArrayBuffer,
} from './utils/crypto';

const venv = getVenv(venvOptions);
const python = async (...args: Parameters<Awaited<typeof venv>>) => (await venv)(...args);
Expand Down Expand Up @@ -122,6 +134,8 @@ function getProxySettingsForCurrentWifiNetwork() {
send({ name: "get_obj_from_frida_script", payload: getProxySettingsForCurrentWifiNetwork() });`,
} as const;
const cloudConfigPath =
'/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/CloudConfigurationDetails.plist' as const;

export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
options: PlatformApiOptions<'ios', RunTarget, SupportedCapability<'ios'>[]>
Expand All @@ -141,6 +155,99 @@ export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
ssh.dispose();
return res;
},
async ensureSupervision() {
if (!options.capabilities.includes('ssh'))
throw new Error('SSH is currently required to ensure supervison mode.');

const OrganizationName = 'appstraction';
const cacheDir = await globalCacheDir('appstraction');

const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`);
const plist = (await parseFile(Buffer.from(encodedPlist, 'base64')))?.[0] as
| CloudConfigurationDetails
| undefined;

if (!plist) throw new Error('Failed to ensure supervision mode: Invalid CloudConfiguration.');

let hostCert;
let hostKey;

if (
(await exists(join(cacheDir, 'ios', 'supervisorCert.pem'))) &&
(await exists(join(cacheDir, 'ios', 'supervisorPrivateKey.pem')))
) {
hostCert = pemToArrayBuffer((await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString());
hostKey = pemToArrayBuffer(
(await readFile(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))).toString()
);

if (!(await certificateHasExpired(hostCert))) {
const hostCertFingerprint = await certificateFingerprint(hostCert);

// Test if the current host certificate is already controlling the device.
if (
plist.IsSupervised &&
plist.SupervisorHostCertificates &&
plist.SupervisorHostCertificates.length > 0 &&
plist.SupervisorHostCertificates.some(
async (cert) => (await certificateFingerprint(cert)) === hostCertFingerprint
)
)
return;
} else {
hostCert = undefined;
hostKey = undefined;
}
}

if (!hostCert || !hostKey) {
// We have no exsiting keys, so let’s generate one.
const generated = await generateCertificate(OrganizationName);
hostCert = generated.certificate;
hostKey = generated.privateKey;

await mkdirp(join(cacheDir, 'ios'));
await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), arrayBufferToPem(hostCert, 'CERTIFICATE'));
await writeFile(
join(cacheDir, 'ios', 'supervisorPrivateKey.pem'),
arrayBufferToPem(hostKey, 'PRIVATE KEY')
);
}

const newPlist = {
...plist,
SupervisorHostCertificates: [Buffer.from(hostCert)],
IsSupervised: true,
OrganizationName,
AllowPairing: true,
};

await this.ssh(`echo "${bplist(newPlist).toString('base64')}" | base64 -d > ${cloudConfigPath}`);
await this.userspaceRestart();
},
async removeSupervision() {
const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`);
const plist = (await parseFile(Buffer.from(encodedPlist, 'base64')))?.[0] as
| CloudConfigurationDetails
| undefined;

if (!plist) throw new Error('Failed to remove supervision mode: Invalid CloudConfiguration.');
const newPlist = {
...plist,
SupervisorHostCertificates: [],
IsSupervised: false,
OrganizationName: '',
};

await this.ssh(`echo "${bplist(newPlist).toString('base64')}" | base64 -d > ${cloudConfigPath}`);
await this.userspaceRestart();
},
async userspaceRestart() {
if (!options.capabilities.includes('ssh'))
throw new Error('SSH is currently required to restart in userspace.');

await this.ssh('ldrestart');
},
},

resetDevice: asyncUnimplemented('resetDevice') as never,
Expand Down Expand Up @@ -368,6 +475,13 @@ export const iosApi = <RunTarget extends SupportedRunTarget<'ios'>>(
},
});

type CloudConfigurationDetails = Partial<{
AllowPairing: boolean;
IsSupervised: boolean;
OrganizationName: string;
SupervisorHostCertificates: Buffer[];
}>;

/** The IDs of known permissions on iOS. */
export const iosPermissions = [
'kTCCServiceLiverpool',
Expand Down
Loading

0 comments on commit 74d9ef0

Please sign in to comment.