diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8225baa --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +/node_modules +/dist diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..30e16e7 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + semi: false, + trailingComma: 'all', + singleQuote: true, + arrowParens: 'avoid', +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..54f3f13 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "[typescript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/README.md b/README.md index f8663df..fb82bc3 100644 --- a/README.md +++ b/README.md @@ -77,19 +77,15 @@ MIT © [Niklas Higi](https://shroudedcode.com) [certificate-pinning]: https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning#what-is-pinning [manifest-debuggable]: https://developer.android.com/guide/topics/manifest/application-element#debug [patch-certificate-pinning]: https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05c-reverse-engineering-and-tampering#patching-example-disabling-certificate-pinning - [node]: https://nodejs.org/en/download/ [java]: https://www.oracle.com/technetwork/java/javase/downloads/index.html - [google-maps-android]: https://console.cloud.google.com/google/maps-apis/apis/maps-android-backend.googleapis.com [google-api-key-restrictions]: https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions [android-app-bundle]: https://developer.android.com/platform/technology/app-bundle/ [apkpure]: https://apkpure.com/ [sai]: https://github.com/Aefyr/SAI - [charles]: https://www.charlesproxy.com/ [mitmproxy]: https://mitmproxy.org/ - [apktool]: https://ibotpeaches.github.io/Apktool/ [apktool-issues]: https://github.com/iBotPeaches/Apktool/issues [uber-apk-signer]: https://github.com/patrickfav/uber-apk-signer diff --git a/package.json b/package.json index e080f50..dc4e730 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsc", "test": "tsc", - "watch": "tsc --watch" + "watch": "tsc --watch", + "format": "prettier --write ." }, "bin": "bin/apk-mitm", "files": [ @@ -45,6 +46,7 @@ "@types/listr": "^0.14.2", "@types/node": "14.14.10", "@types/yargs-parser": "^15.0.0", + "prettier": "^2.2.1", "typescript": "^4.1.2" } } diff --git a/src/cli.ts b/src/cli.ts index d083d34..f2cb8d8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,12 +12,12 @@ import UberApkSigner from './tools/uber-apk-signer' import Tool from './tools/tool' export type TaskOptions = { - inputPath: string, - outputPath: string, - apktool: Apktool, - uberApkSigner: UberApkSigner, - tmpDir: string, - wait: boolean, + inputPath: string + outputPath: string + apktool: Apktool + uberApkSigner: UberApkSigner + tmpDir: string + wait: boolean } interface PatchingError extends Error { @@ -49,7 +49,8 @@ async function main() { const inputPath = path.resolve(process.cwd(), input) const fileExtension = path.extname(input) - const outputName = `${path.basename(input, fileExtension)}-patched${fileExtension}` + const baseName = path.basename(input, fileExtension) + const outputName = `${baseName}-patched${fileExtension}` const outputPath = path.resolve(path.dirname(inputPath), outputName) let taskFunction: (options: TaskOptions) => Listr @@ -81,33 +82,43 @@ async function main() { showVersions({ apktool, uberApkSigner }) console.log(chalk.dim(` Using temporary directory:\n ${tmpDir}\n`)) - taskFunction({ inputPath, outputPath, tmpDir, apktool, uberApkSigner, wait: args.wait }).run().then(context => { - if (taskFunction === patchApk && context.usesAppBundle) { - showAppBundleWarning() - } - - console.log( - chalk`\n {green.inverse Done! } Patched file: {bold ./${outputName}}\n`, - ) - }).catch((error: PatchingError) => { - const message = getErrorMessage(error, { tmpDir }) - - console.error( - [ - '', - chalk` {red.inverse.bold Failed! } An error occurred:`, - '', - message, - '', - ` The full logs of all commands are available here:`, - ` ${path.join(tmpDir, 'logs')}`, - '' - ].join('\n'), - ) - if (process.arch.startsWith('arm')) showArmWarning() - - process.exit(1) + taskFunction({ + inputPath, + outputPath, + tmpDir, + apktool, + uberApkSigner, + wait: args.wait, }) + .run() + .then(context => { + if (taskFunction === patchApk && context.usesAppBundle) { + showAppBundleWarning() + } + + console.log( + chalk`\n {green.inverse Done! } Patched file: {bold ./${outputName}}\n`, + ) + }) + .catch((error: PatchingError) => { + const message = getErrorMessage(error, { tmpDir }) + + console.error( + [ + '', + chalk` {red.inverse.bold Failed! } An error occurred:`, + '', + message, + '', + ` The full logs of all commands are available here:`, + ` ${path.join(tmpDir, 'logs')}`, + '', + ].join('\n'), + ) + if (process.arch.startsWith('arm')) showArmWarning() + + process.exit(1) + }) } function getErrorMessage(error: PatchingError, { tmpDir }: { tmpDir: string }) { @@ -116,18 +127,20 @@ function getErrorMessage(error: PatchingError, { tmpDir }: { tmpDir: string }) { } function formatCommandError(error: string, { tmpDir }: { tmpDir: string }) { - return error - // Replace mentions of the (sometimes very long) temporary directory path - .replace(new RegExp(tmpDir, 'g'), chalk`{bold }`) - // Highlight (usually relevant) warning lines in Apktool output - .replace(/^W: .+$/gm, line => chalk`{yellow ${line}}`) - // De-emphasize Apktool info lines - .replace(/^I: .+$/gm, line => chalk`{dim ${line}}`) - // De-emphasize (not very helpful) Apktool "could not exec" error message - .replace( - /^.+brut\.common\.BrutException: could not exec.+$/gm, - line => chalk`{dim ${line}}` - ) + return ( + error + // Replace mentions of the (sometimes very long) temporary directory path + .replace(new RegExp(tmpDir, 'g'), chalk`{bold }`) + // Highlight (usually relevant) warning lines in Apktool output + .replace(/^W: .+$/gm, line => chalk`{yellow ${line}}`) + // De-emphasize Apktool info lines + .replace(/^I: .+$/gm, line => chalk`{dim ${line}}`) + // De-emphasize (not very helpful) Apktool "could not exec" error message + .replace( + /^.+brut\.common\.BrutException: could not exec.+$/gm, + line => chalk`{dim ${line}}`, + ) + ) } function showHelp() { @@ -148,9 +161,13 @@ function showSupportedExtensions() { process.exit(1) } -function showVersions( - { apktool, uberApkSigner }: { apktool: Tool, uberApkSigner: Tool }, -) { +function showVersions({ + apktool, + uberApkSigner, +}: { + apktool: Tool + uberApkSigner: Tool +}) { console.log(chalk` {dim ╭} {blue {bold apk-mitm} v${version}} {dim ├ {bold apktool} ${apktool.version.name} diff --git a/src/patch-apk.ts b/src/patch-apk.ts index ae06564..9920616 100644 --- a/src/patch-apk.ts +++ b/src/patch-apk.ts @@ -13,7 +13,12 @@ import observeAsync from './utils/observe-async' export default function patchApk(taskOptions: TaskOptions) { const { - inputPath, outputPath, tmpDir, apktool, uberApkSigner, wait, + inputPath, + outputPath, + tmpDir, + apktool, + uberApkSigner, + wait, } = taskOptions const decodeDir = path.join(tmpDir, 'decode') @@ -32,7 +37,7 @@ export default function patchApk(taskOptions: TaskOptions) { }, { title: 'Modifying app manifest', - task: async (context) => { + task: async context => { const result = await modifyManifest( path.join(decodeDir, 'AndroidManifest.xml'), ) @@ -42,9 +47,10 @@ export default function patchApk(taskOptions: TaskOptions) { }, { title: 'Replacing network security config', - task: () => createNetworkSecurityConfig( - path.join(decodeDir, `res/xml/nsc_mitm.xml`), - ), + task: () => + createNetworkSecurityConfig( + path.join(decodeDir, `res/xml/nsc_mitm.xml`), + ), }, { title: 'Disabling certificate pinning', @@ -53,16 +59,17 @@ export default function patchApk(taskOptions: TaskOptions) { { title: 'Waiting for you to make changes', enabled: () => wait, - task: () => observeAsync(async next => { - process.stdin.setEncoding('utf-8') - process.stdin.setRawMode(true) + task: () => + observeAsync(async next => { + process.stdin.setEncoding('utf-8') + process.stdin.setRawMode(true) - next('Press any key to continue.') - await once(process.stdin, 'data') + next('Press any key to continue.') + await once(process.stdin, 'data') - process.stdin.setRawMode(false) - process.stdin.pause() - }) + process.stdin.setRawMode(false) + process.stdin.pause() + }), }, { title: 'Encoding patched APK file', @@ -70,31 +77,35 @@ export default function patchApk(taskOptions: TaskOptions) { new Listr([ { title: 'Encoding using AAPT2', - task: (_, task) => observeAsync(async next => { - try { - await apktool.encode(decodeDir, tmpApkPath, true).forEach(next) - } catch { - task.skip('Failed, falling back to AAPT...') - fallBackToAapt = true - } - }), + task: (_, task) => + observeAsync(async next => { + try { + await apktool + .encode(decodeDir, tmpApkPath, true) + .forEach(next) + } catch { + task.skip('Failed, falling back to AAPT...') + fallBackToAapt = true + } + }), }, { title: chalk`Encoding using AAPT {dim [fallback]}`, skip: () => !fallBackToAapt, task: () => apktool.encode(decodeDir, tmpApkPath, false), }, - ]) + ]), }, { title: 'Signing patched APK file', - task: () => observeAsync(async next => { - await uberApkSigner - .sign([tmpApkPath], { zipalign: true }) - .forEach(line => next(line)) + task: () => + observeAsync(async next => { + await uberApkSigner + .sign([tmpApkPath], { zipalign: true }) + .forEach(line => next(line)) - await fs.copyFile(tmpApkPath, outputPath) - }), + await fs.copyFile(tmpApkPath, outputPath) + }), }, ]) } diff --git a/src/patch-app-bundle.ts b/src/patch-app-bundle.ts index ccf4832..2b3119d 100644 --- a/src/patch-app-bundle.ts +++ b/src/patch-app-bundle.ts @@ -23,51 +23,58 @@ function patchAppBundle( const bundleDir = path.join(tmpDir, 'bundle') let baseApkPath = path.join(bundleDir, 'base.apk') - return new Listr( - [ - { - title: 'Extracting APKs', - task: () => unzip(inputPath, bundleDir), - }, - ...(isXapk ? [{ - title: 'Finding base APK path', - task: async () => { - const manifestPath = path.join(bundleDir, 'manifest.json') - const manifestContent = await fs.readFile(manifestPath, 'utf-8') - const manifest = JSON.parse(manifestContent) + return new Listr([ + { + title: 'Extracting APKs', + task: () => unzip(inputPath, bundleDir), + }, + ...(isXapk + ? [ + { + title: 'Finding base APK path', + task: async () => { + const manifestPath = path.join(bundleDir, 'manifest.json') + const manifestContent = await fs.readFile(manifestPath, 'utf-8') + const manifest = JSON.parse(manifestContent) - baseApkPath = path.join(bundleDir, getXapkBaseName(manifest)) - }, - }] : []), - { - title: 'Patching base APK', - task: () => patchApk({ - inputPath: baseApkPath, outputPath: baseApkPath, - tmpDir: path.join(tmpDir, 'base-apk'), apktool, uberApkSigner, wait, + baseApkPath = path.join(bundleDir, getXapkBaseName(manifest)) + }, + }, + ] + : []), + { + title: 'Patching base APK', + task: () => + patchApk({ + inputPath: baseApkPath, + outputPath: baseApkPath, + tmpDir: path.join(tmpDir, 'base-apk'), + apktool, + uberApkSigner, + wait, }), - }, - { - title: 'Signing APKs', - task: () => observeAsync(async next => { + }, + { + title: 'Signing APKs', + task: () => + observeAsync(async next => { const apkFiles = await globby(path.join(bundleDir, '**/*.apk')) await uberApkSigner .sign(apkFiles, { zipalign: false }) .forEach(line => next(line)) }), - }, - { - title: 'Compressing APKs', - task: () => zip(bundleDir, outputPath), - }, - ], - ) + }, + { + title: 'Compressing APKs', + task: () => zip(bundleDir, outputPath), + }, + ]) } function getXapkBaseName(manifest: any) { if (manifest.split_apks) { - return manifest.split_apks - .filter((apk: any) => apk.id === 'base')[0].file + return manifest.split_apks.filter((apk: any) => apk.id === 'base')[0].file } return `${manifest.package_name}.apk` diff --git a/src/tasks/disable-certificate-pinning.ts b/src/tasks/disable-certificate-pinning.ts index 7dbd826..f6a4cb0 100644 --- a/src/tasks/disable-certificate-pinning.ts +++ b/src/tasks/disable-certificate-pinning.ts @@ -26,10 +26,7 @@ const METHOD_PATTERNS = METHOD_SIGNATURES.map(signature => { }) /** Code inserted into `checkClientTrusted` and `checkServerTrusted`. */ -const RETURN_VOID_FIX = [ - '.locals 0', - 'return-void', -] +const RETURN_VOID_FIX = ['.locals 0', 'return-void'] /** Code inserted into `getAcceptedIssuers`. */ const RETURN_EMPTY_ARRAY_FIX = [ @@ -39,12 +36,17 @@ const RETURN_EMPTY_ARRAY_FIX = [ 'return-object v0', ] -export default async function disableCertificatePinning(directoryPath: string, task: ListrTaskWrapper) { +export default async function disableCertificatePinning( + directoryPath: string, + task: ListrTaskWrapper, +) { return observeAsync(async next => { next('Finding smali files...') // Convert Windows path (using backslashes) to POSIX path (using slashes) - const directoryPathPosix = directoryPath.split(path.sep).join(path.posix.sep) + const directoryPathPosix = directoryPath + .split(path.sep) + .join(path.posix.sep) const globPattern = path.posix.join(directoryPathPosix, 'smali*/**/*.smali') let pinningFound = false @@ -69,12 +71,8 @@ export default async function disableCertificatePinning(directoryPath: string, t for (const pattern of METHOD_PATTERNS) { patchedContent = patchedContent.replace( - pattern, ( - _, - openingLine: string, - body: string, - closingLine: string, - ) => { + pattern, + (_, openingLine: string, body: string, closingLine: string) => { const bodyLines = body .split('\n') .map(line => line.replace(/^ /, '')) @@ -89,14 +87,16 @@ export default async function disableCertificatePinning(directoryPath: string, t '', '# commented out by apk-mitm to disable old method body', '# ', - ...bodyLines.map(line => `# ${line}`) + ...bodyLines.map(line => `# ${line}`), ] return [ openingLine, ...patchedBodyLines.map(line => ` ${line}`), closingLine, - ].map(line => line.trimEnd()).join('\n') + ] + .map(line => line.trimEnd()) + .join('\n') }, ) } diff --git a/src/tasks/download-tools.ts b/src/tasks/download-tools.ts index 9376e27..36d7278 100644 --- a/src/tasks/download-tools.ts +++ b/src/tasks/download-tools.ts @@ -4,8 +4,8 @@ import createToolDownloadTask from '../utils/download-tool' import { TaskOptions } from '../cli' export default function downloadTools({ apktool, uberApkSigner }: TaskOptions) { - return new Listr([ - createToolDownloadTask(apktool), - createToolDownloadTask(uberApkSigner), - ], { concurrent: true }) + return new Listr( + [createToolDownloadTask(apktool), createToolDownloadTask(uberApkSigner)], + { concurrent: true }, + ) } diff --git a/src/tasks/modify-manifest.ts b/src/tasks/modify-manifest.ts index 60ed96f..fc0ef3d 100644 --- a/src/tasks/modify-manifest.ts +++ b/src/tasks/modify-manifest.ts @@ -2,18 +2,22 @@ import * as fs from '../utils/fs' import xml from 'xml-js' export default async function modifyManifest(path: string) { - const fileXml = xml.xml2js( - await fs.readFile(path, 'utf-8'), - { compact: true, alwaysArray: true }, - ) + const fileXml = xml.xml2js(await fs.readFile(path, 'utf-8'), { + compact: true, + alwaysArray: true, + }) const manifest = fileXml['manifest'][0] const application = manifest['application'][0] application._attributes['android:debuggable'] = 'true' application._attributes['android:networkSecurityConfig'] = '@xml/nsc_mitm' - const usesAppBundle = application['meta-data'] && application['meta-data'] - .some((meta: any) => meta._attributes['android:name'] === 'com.android.vending.splits') + const usesAppBundle = + application['meta-data'] && + application['meta-data'].some( + (meta: any) => + meta._attributes['android:name'] === 'com.android.vending.splits', + ) await fs.writeFile(path, xml.js2xml(fileXml, { compact: true, spaces: 4 })) diff --git a/src/tools/apktool.ts b/src/tools/apktool.ts index d53ca9a..87972c4 100644 --- a/src/tools/apktool.ts +++ b/src/tools/apktool.ts @@ -16,20 +16,32 @@ export default class Apktool extends Tool { } decode(inputPath: string, outputPath: string) { - return this.run([ - 'decode', inputPath, - '--output', outputPath, - '--frame-path', this.options.frameworkPath, - ], 'decoding') + return this.run( + [ + 'decode', + inputPath, + '--output', + outputPath, + '--frame-path', + this.options.frameworkPath, + ], + 'decoding', + ) } encode(inputPath: string, outputPath: string, useAapt2: boolean) { - return this.run([ - 'build', inputPath, - '--output', outputPath, - '--frame-path', this.options.frameworkPath, - ...(useAapt2 ? ['--use-aapt2'] : []), - ], `encoding-${useAapt2 ? 'aapt2' : 'aapt'}`) + return this.run( + [ + 'build', + inputPath, + '--output', + outputPath, + '--frame-path', + this.options.frameworkPath, + ...(useAapt2 ? ['--use-aapt2'] : []), + ], + `encoding-${useAapt2 ? 'aapt2' : 'aapt'}`, + ) } private run(args: string[], logName: string) { @@ -44,16 +56,15 @@ export default class Apktool extends Tool { name = 'apktool' get version() { - if (this.options.customPath) - return { name: chalk.italic('custom version') } + if (this.options.customPath) return { name: chalk.italic('custom version') } const versionNumber = '2.5.0' return { name: `v${versionNumber}`, downloadUrl: - 'https://github.com/iBotPeaches/Apktool/releases/download' - + `/v${versionNumber}/apktool_${versionNumber}.jar` + 'https://github.com/iBotPeaches/Apktool/releases/download' + + `/v${versionNumber}/apktool_${versionNumber}.jar`, } } } diff --git a/src/tools/uber-apk-signer.ts b/src/tools/uber-apk-signer.ts index 96fa58c..0c40fe4 100644 --- a/src/tools/uber-apk-signer.ts +++ b/src/tools/uber-apk-signer.ts @@ -27,8 +27,8 @@ export default class UberApkSigner extends Tool { return { name: `v${versionNumber}`, downloadUrl: - 'https://github.com/patrickfav/uber-apk-signer/releases/download' - + `/v${versionNumber}/uber-apk-signer-${versionNumber}.jar`, + 'https://github.com/patrickfav/uber-apk-signer/releases/download' + + `/v${versionNumber}/uber-apk-signer-${versionNumber}.jar`, } } } diff --git a/src/utils/download-file.ts b/src/utils/download-file.ts index 4b28ead..1e8d46a 100644 --- a/src/utils/download-file.ts +++ b/src/utils/download-file.ts @@ -5,35 +5,43 @@ const { https } = followRedirects export default function downloadFile(url: string, path: string) { return new Observable(subscriber => { - https.get(url, response => { - if (response.statusCode !== 200) { - const error = new Error(`The URL "${url}" returned status code ${response.statusCode}, expected 200.`) - - // Cancel download with error - response.destroy(error) - } - - const fileStream = fs.createWriteStream(path) - - const totalLength = parseInt(response.headers['content-length']) - let currentLength = 0 + https + .get(url, response => { + if (response.statusCode !== 200) { + const error = new Error( + `The URL "${url}" returned status code ${response.statusCode}, expected 200.`, + ) + + // Cancel download with error + response.destroy(error) + } + + const fileStream = fs.createWriteStream(path) + + const totalLength = parseInt(response.headers['content-length']) + let currentLength = 0 + + const reportProgress = () => { + const percentage = currentLength / totalLength + subscriber.next( + `${(percentage * 100).toFixed(2)}% done (${formatBytes( + currentLength, + )} / ${formatBytes(totalLength)} MB)`, + ) + } + reportProgress() - const reportProgress = () => { - const percentage = currentLength / totalLength - subscriber.next(`${(percentage * 100).toFixed(2)}% done (${formatBytes(currentLength)} / ${formatBytes(totalLength)} MB)`) - } - reportProgress() + response.pipe(fileStream) - response.pipe(fileStream) + response.on('data', (chunk: Buffer) => { + currentLength += chunk.byteLength + reportProgress() + }) + response.on('error', error => subscriber.error(error)) - response.on('data', (chunk: Buffer) => { - currentLength += chunk.byteLength - reportProgress() + fileStream.on('close', () => subscriber.complete()) }) - response.on('error', error => subscriber.error(error)) - - fileStream.on('close', () => subscriber.complete()) - }).on('error', error => subscriber.error(error)) + .on('error', error => subscriber.error(error)) }) } diff --git a/src/utils/download-tool.ts b/src/utils/download-tool.ts index 77aa16b..40aa747 100644 --- a/src/utils/download-tool.ts +++ b/src/utils/download-tool.ts @@ -13,8 +13,7 @@ export default function createToolDownloadTask(tool: Tool) { return { title: `Downloading ${tool.name} ${tool.version.name}`, task: (_, task: ListrTaskWrapper) => { - if (!tool.version.downloadUrl) - return task.skip('Using custom version') + if (!tool.version.downloadUrl) return task.skip('Using custom version') const fileName = `${tool.name}-${tool.version.name}.jar` return downloadCachedFile(task, tool.version.downloadUrl, fileName) diff --git a/yarn.lock b/yarn.lock index 2399e2c..53af15c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -703,6 +703,11 @@ picomatch@^2.0.5, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +prettier@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"