Skip to content

Commit

Permalink
Implement --certificate flag (#64)
Browse files Browse the repository at this point in the history
Co-authored-by: Maor Magori <maor@magori.online>
  • Loading branch information
niklashigi and maormagori authored Sep 2, 2021
1 parent 5c3436e commit 035cb1b
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 22 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Sometimes you'll need to make manual changes to an app in order to get it to wor

If you want to experiment with different changes to an APK, then using `--wait` is probably not the most convenient option as it forces you to start from scratch every time you use it. In this case you might want to take a look at [APKLab][apklab]. It's an Android reverse engineering workbench built on top of VS Code that comes with [`apk-mitm` support][apklab-mitm] and should allow you to iterate much more quickly.

### Allowing specific certificates

On some devices (like Android TVs) you might not be able to add a new certificate to the system's root certificates. In those cases you can still add your proxy's certificate [directly to the app's Network Security Config][network-security-config-custom-ca] since that will work on any device. You can accomplish this by running `apk-mitm` with the `--certificate` flag set to the path of the certificate (`.pem` or `.der` file) used by your proxy.

## Caveats

- If the app uses Google Maps and the map is broken after patching, then the app's API key is probably [restricted to the developer's certificate][google-api-key-restrictions]. You'll have to [create your own API key][google-maps-android] without restrictions and run `apk-mitm` with [the `--wait` option](#making-manual-changes) to be able to replace the `com.google.android.geo.API_KEY` value in the app's `AndroidManifest.xml` file.
Expand All @@ -76,6 +80,7 @@ If you want to experiment with different changes to an APK, then using `--wait`
MIT © [Niklas Higi](https://shroudedcode.com)

[network-security-config]: https://developer.android.com/training/articles/security-config
[network-security-config-custom-ca]: https://developer.android.com/training/articles/security-config#ConfigCustom
[certificate-pinning]: https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning#what-is-pinning
[node]: https://nodejs.org/en/download/
[java]: https://www.oracle.com/technetwork/java/javase/downloads/index.html
Expand Down
43 changes: 38 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type TaskOptions = {
inputPath: string
outputPath: string
skipPatches: boolean
certificatePath?: string
apktool: Apktool
uberApkSigner: UberApkSigner
tmpDir: string
Expand All @@ -36,7 +37,7 @@ const { version } = require('../package.json')

async function main() {
const args = parseArgs(process.argv.slice(2), {
string: ['apktool'],
string: ['apktool', 'certificate'],
boolean: ['help', 'wait', 'skip-patches', 'debuggable'],
})

Expand Down Expand Up @@ -77,6 +78,16 @@ async function main() {
showSupportedExtensions()
}

// Initialize and validate certificate path
let certificatePath: string | undefined
if (args.certificate) {
certificatePath = path.resolve(process.cwd(), args.certificate)
let certificateExtension = path.extname(certificatePath)

if (certificateExtension !== '.pem' && certificateExtension !== '.der')
showSupportedCertificateExtensions()
}

const tmpDir = tempy.directory({ prefix: 'apk-mitm-' })
process.chdir(tmpDir)

Expand All @@ -92,6 +103,7 @@ async function main() {
taskFunction({
inputPath,
outputPath,
certificatePath,
tmpDir,
apktool,
uberApkSigner,
Expand Down Expand Up @@ -161,13 +173,20 @@ function formatCommandError(error: string, { tmpDir }: { tmpDir: string }) {
function showHelp() {
console.log(chalk`
$ {bold apk-mitm} <path-to-apk/xapk/apks>
{dim {bold --wait} Wait for manual changes before re-encoding {gray.italic (optional)}}
{dim {bold --debuggable} Make the patched app debuggable {gray.italic (optional)}}
{dim {bold --apktool} Path to custom Apktool.jar {gray.italic (optional)}}
{dim {bold --skip-patches} Don't apply any patches {gray.italic (optional)}}
{blue {dim.bold *} Optional flags:}
{dim {bold --wait} Wait for manual changes before re-encoding}
{dim {bold --debuggable} Make the patched app debuggable}
{dim {bold --skip-patches} Don't apply any patches (for troubleshooting)}
{dim {bold --apktool <path-to-jar>} Use custom version of Apktool}
{dim {bold --certificate <path-to-pem/der>} Add specific certificate to network security config}
`)
}

/**
* Error that is shown when the file provided through the positional argument
* has an unsupported extension. Exits with status 1 after showing the message.
*/
function showSupportedExtensions(): never {
console.log(chalk`{yellow
It looks like you tried running {bold apk-mitm} with an unsupported file type!
Expand All @@ -178,6 +197,20 @@ function showSupportedExtensions(): never {
process.exit(1)
}

/**
* Error that is shown when the file provided through the `--certificate` flag
* has an unsupported extension. Exits with status 1 after showing the message.
*/
function showSupportedCertificateExtensions(): never {
console.log(chalk`{yellow
It looks like the certificate file you provided is unsupported!
Only {bold .pem} and {bold .der} certificate files are supported.
}`)

process.exit(1)
}

function showVersions({
apktool,
uberApkSigner,
Expand Down
6 changes: 5 additions & 1 deletion src/patch-apk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export default function patchApk(options: TaskOptions) {
{
title: 'Applying patches',
skip: () => options.skipPatches,
task: () => applyPatches(decodeDir, options.debuggable),
task: () =>
applyPatches(decodeDir, {
debuggable: options.debuggable,
certificatePath: options.certificatePath,
}),
},
{
title: 'Waiting for you to make changes',
Expand Down
18 changes: 16 additions & 2 deletions src/tasks/apply-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import Listr = require('listr')
import modifyManifest from './modify-manifest'
import createNetworkSecurityConfig from './create-netsec-config'
import disableCertificatePinning from './disable-certificate-pinning'
import copyCertificateFile from './copy-certificate-file'

export default function applyPatches(decodeDir: string, debuggable = false) {
export default function applyPatches(
decodeDir: string,
{
debuggable = false,
certificatePath,
}: { debuggable?: boolean; certificatePath?: string } = {},
) {
return new Listr([
{
title: 'Modifying app manifest',
task: async context => {
task: async (context: { usesAppBundle: boolean }) => {
const result = await modifyManifest(
path.join(decodeDir, 'AndroidManifest.xml'),
debuggable,
Expand All @@ -18,11 +25,18 @@ export default function applyPatches(decodeDir: string, debuggable = false) {
context.usesAppBundle = result.usesAppBundle
},
},
{
title: 'Copying certificate file',
skip: () =>
certificatePath ? false : '--certificate flag not specified.',
task: () => copyCertificateFile(decodeDir, certificatePath!),
},
{
title: 'Replacing network security config',
task: () =>
createNetworkSecurityConfig(
path.join(decodeDir, `res/xml/nsc_mitm.xml`),
{ certificatePath },
),
},
{
Expand Down
18 changes: 18 additions & 0 deletions src/tasks/copy-certificate-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as fs from '../utils/fs'
import * as path from 'path'

/**
* Copies the certificate file at `sourcePath` to the correct location within
* the APK's `decodeDir`, so it can then be referenced in the Network Security
* Config.
*/
export default async function copyCertificateFile(
decodeDir: string,
sourcePath: string,
) {
const rawDir = path.join(decodeDir, `res/raw/`)
await fs.mkdir(rawDir, { recursive: true })

const destinationPath = path.join(rawDir, path.basename(sourcePath))
await fs.copyFile(sourcePath, destinationPath)
}
52 changes: 38 additions & 14 deletions src/tasks/create-netsec-config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
import * as fs from '../utils/fs'
import * as pathUtils from 'path'

const TEMPLATE = `<?xml version="1.0" encoding="utf-8"?>
<!-- Intentionally lax Network Security Configuration (generated by apk-mitm) -->
<network-security-config>
<!-- Allow cleartext traffic -->
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- Allow user-added (proxy) certificates -->
<certificates src="user" />
/** Generates the XML contents of the Network Security File. */
const generateConfig = ({
certificatePath,
}: {
certificatePath: string | undefined
}) => {
const certificateBlock = certificatePath
? generateCertificateBlock(certificatePath)
: ''

<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>`
return `<?xml version="1.0" encoding="utf-8"?>
<!-- Intentionally lax Network Security Configuration (generated by apk-mitm) -->
<network-security-config>
<!-- Allow cleartext traffic -->
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- Allow user-added (proxy) certificates -->
<certificates src="user" />${certificateBlock}
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>`
}

/** Generates the XML block responsible for registering a custom certificate. */
const generateCertificateBlock = (path: string) => {
const fileName = pathUtils.basename(path, '.pem')

export default async function createNetworkSecurityConfig(path: string) {
return `\n
<!-- Allow specific certificate -->
<certificates src="@raw/${fileName}" />\n`
}

export default async function createNetworkSecurityConfig(
path: string,
{ certificatePath }: { certificatePath?: string } = {},
) {
await fs.mkdir(pathUtils.dirname(path), { recursive: true })
await fs.writeFile(path, TEMPLATE, 'utf-8')

const config = generateConfig({ certificatePath })
await fs.writeFile(path, config, 'utf-8')
}

0 comments on commit 035cb1b

Please sign in to comment.