Skip to content

Commit

Permalink
support dumping raw bundles without packing
Browse files Browse the repository at this point in the history
workaround for malformed app: some extensions don't have executable bit
  • Loading branch information
ChiChou committed Jun 13, 2023
1 parent 3edbc42 commit ceca864
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 29 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,19 @@ Options:
-R, --remote connect to remote frida-server
-D, --device <uuid> connect to device with the given ID
-H, --host <host> connect to remote frida-server on HOST
-o, --output <output> ipa filename
-f, --force override existing files
-d, --debug enable debug output
-r, --raw dump raw app bundle to directory (no ipa)
-o, --output <output> ipa filename or directory to dump to
-h, --help display help for command
```

Example:

* `bagbak -l` to list all apps
* `bagbak --raw Chrome` to dump the app to current directory
* `bagbak com.google.chrome.ios` to dump app to `com.google.chrome.ios-[version].ipa`

## 国内用户 frida 安装失败问题

[使用国内镜像加速安装](https://github.com/chaitin/passionfruit/wiki/%E4%BD%BF%E7%94%A8%E5%9B%BD%E5%86%85%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F%E5%AE%89%E8%A3%85#%E9%A2%84%E7%BC%96%E8%AF%91%E5%8C%85%E5%A4%B1%E8%B4%A5)
8 changes: 7 additions & 1 deletion bin/bagbak.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ async function main() {
.option('-D, --device <uuid>', 'connect to device with the given ID')
.option('-H, --host <host>', 'connect to remote frida-server on HOST')

.option('-f, --force', 'override existing files')
.option('-d, --debug', 'enable debug output')
.option('-r, --raw', 'dump raw app bundle to directory (no ipa)')
.option('-o, --output <output>', 'ipa filename or directory to dump to')
.usage('[bundle id or name]');

program.parse(process.argv);
Expand Down Expand Up @@ -128,7 +131,10 @@ async function main() {
console.log(chalk.redBright('[decrypt]'), remote);
})

const saved = await job.packTo(program.output);
const saved = program.raw ?
await job.dump(program.output || '.', program.force) :
await job.pack(program.output);

console.log(`Saved to ${chalk.yellow(saved)}`);
return;
}
Expand Down
71 changes: 52 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EventEmitter } from "events";
import { mkdir, open } from "fs/promises";
import { mkdir, open, rm } from "fs/promises";
import { tmpdir } from "os";
import { basename, join, resolve } from "path";

import { Device } from "frida";

import { findEncryptedBinaries } from './lib/scan.js';
import { Pull } from './lib/scp.js';
import { Pull, quote } from './lib/scp.js';
import { connect } from './lib/ssh.js';
import { debug, directoryExists, passthrough, readAgent } from './lib/utils.js';
import zip from './lib/zip.js';
Expand All @@ -18,6 +18,9 @@ export { enumerateApps, readAgent } from './lib/utils.js';
* @property {string} event
*/

/**
* main class
*/
export class Main extends EventEmitter {
#device;

Expand All @@ -27,7 +30,7 @@ export class Main extends EventEmitter {
#app = null;

/**
*
* constructor
* @param {Device} device
* @param {import("frida").Application} app
*/
Expand All @@ -50,6 +53,33 @@ export class Main extends EventEmitter {
}
}

/**
* hack: some extension is not executable
* @param {string} path
* @returns
*/
async #executableWorkaround(path) {
if (!path.startsWith('/private/var/containers/Bundle/Application/')) {
return; // do not apply to system apps
}

const client = await connect(this.#device);
const cmd = `chmod +xX ${quote(path)}`;
return new Promise((resolve, reject) => {
client.exec(cmd, (err, stream) => {
if (err) return reject(err);
stream
.on('close', (code, signal) => {
client.end();
if (code === 0) return resolve();
reject(new Error(`remote command "${cmd}" exited with code ${code}`));
})
.on('data', () => {}) // this handler is a must, otherwise the stream will hang
.stderr.pipe(process.stderr); // proxy stderr
});
});
}

get bundle() {
return this.#app.identifier;
}
Expand All @@ -59,24 +89,24 @@ export class Main extends EventEmitter {
}

/**
*
* @param {import("fs").PathLike} dest path
* dump raw app bundle to directory (no ipa)
* @param {import("fs").PathLike} parent path
* @param {boolean} override whether to override existing files
* @returns {Promise<string>}
*/
async dumpTo(dest, override) {
const parent = join(dest, this.bundle, 'Payload');
if (await directoryExists(parent) && !override)
throw new Error('Destination already exists');

await mkdir(parent, { recursive: true });
async dump(parent, override) {
if (!await directoryExists(parent))
throw new Error('Output directory does not exist');

// fist, copy directory to local
const remoteRoot = this.remote;
debug('remote root', remoteRoot);
debug('copy to', parent);

const localRoot = join(parent, basename(remoteRoot));
if (await directoryExists(localRoot) && !override)
throw new Error('Destination already exists');

this.emit('sshBegin');
await this.copyToLocal(remoteRoot, parent);
this.emit('sshFinish');
Expand All @@ -92,6 +122,9 @@ export class Main extends EventEmitter {
// execute dump
for (const [scope, dylibs] of map.entries()) {
const mainExecutable = [remoteRoot, scope].join('/');
debug('main executable =>', mainExecutable);
await this.#executableWorkaround(mainExecutable);

const pid = await this.#device.spawn(mainExecutable);
debug('pid =>', pid);
const session = await this.#device.attach(pid);
Expand Down Expand Up @@ -140,26 +173,26 @@ export class Main extends EventEmitter {
await this.#device.kill(pid);
}

return parent;
return localRoot;
}

/**
*
* dump and pack to ipa
* @param {import("fs").PathLike?} suggested path of ipa
* @returns {Promise<string>} final path of ipa
*/
async packTo(suggested) {
const cwd = join(tmpdir(), 'bagbak');
await mkdir(cwd, { recursive: true });
const payload = await this.dumpTo(cwd, true);

async pack(suggested) {
const payload = join(tmpdir(), 'bagbak', this.bundle, 'Payload');
await rm(payload, { recursive: true, force: true });
await mkdir(payload, { recursive: true });
await this.dump(payload, true);
debug('payload =>', payload);

const ver = this.#app.parameters.version || 'Unknown';
const defaultTemplate = `${this.bundle}-${ver}.ipa`;
const ipa = suggested || defaultTemplate;

const full = resolve(process.cwd(), ipa);
const full = join(process.cwd(), ipa);
await zip(full, payload);

return ipa;
Expand Down
4 changes: 2 additions & 2 deletions lib/scp.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class SCPReceiver extends Duplex {
* @private
*/

function escapeName(name) {
export function quote(name) {
return `'${name.replace(/'/g, `'\\''`)}'`;
}

Expand Down Expand Up @@ -267,7 +267,7 @@ export class Pull extends EventEmitter {

this.#client = client;
this.#recursive = recursive;
this.#remote = escapeName(remote);
this.#remote = quote(remote);
this.#local = local;
}

Expand Down
5 changes: 2 additions & 3 deletions lib/ssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ export async function connect(device, user = 'root', password = 'alpine') {
const channel = await device.openChannel(`tcp:${port}`);

const client = new Client();
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
client
.on('ready', () => resolve(client))
.on('error', reject)
.once('ready', () => resolve(client))
.connect({
sock: channel,
username: user,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bagbak",
"version": "3.0.2",
"version": "3.0.3",
"description": "Dump iOS app from a jailbroken device, based on frida.re",
"main": "index.js",
"scripts": {
Expand Down

0 comments on commit ceca864

Please sign in to comment.