-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
be21aed
commit 0f85c10
Showing
6 changed files
with
283 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
const CLASS_PATTERN = /\.class(?<keywords>.+)? L(?<name>[^\s]+);/ | ||
const IMPLEMENTS_PATTERN = /\.implements L(?<name>[^\s]+);/g | ||
|
||
/** | ||
* General information about a class extracted from the first few lines of a | ||
* Smali file and used to find applicable Smali patches. | ||
*/ | ||
export interface SmaliHead { | ||
/** The name of the class. */ | ||
name: string | ||
|
||
/** The interfaces implemented by this class. */ | ||
implements: string[] | ||
|
||
/** Whether the "class" actually represents an interface. */ | ||
isInterface: boolean | ||
} | ||
|
||
/** | ||
* Extracts general information like the class name, the implemented interfaces, | ||
* and whether the class actually represents an interface from a Smali file. | ||
*/ | ||
export default function parseSmaliHead(contents: string): SmaliHead { | ||
const { keywords, name } = contents.match(CLASS_PATTERN)?.groups! | ||
|
||
return { | ||
name, | ||
implements: Array.from(contents.matchAll(IMPLEMENTS_PATTERN)).map( | ||
match => match.groups!.name, | ||
), | ||
isInterface: keywords?.trim().split(' ').includes('interface') ?? false, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { SmaliPatch } from './types' | ||
|
||
/** `return void;` in Smali. */ | ||
const RETURN_VOID_SMALI = ['.locals 0', 'return-void'] | ||
|
||
/** `return true;` in Smali. */ | ||
const RETURN_TRUE_SMALI = ['.locals 1', 'const/4 v0, 0x1', 'return v0'] | ||
|
||
/** `return new java.security.cert.X509Certificate[] {};` in Smali. */ | ||
const RETURN_EMPTY_CERT_ARRAY_SMALI = [ | ||
'.locals 1', | ||
'const/4 v0, 0x0', | ||
'new-array v0, v0, [Ljava/security/cert/X509Certificate;', | ||
'return-object v0', | ||
] | ||
|
||
/** | ||
* A declarative list of all the patches that are | ||
* applied to Smali code to disable certificate pinning. | ||
*/ | ||
const smaliPatches: SmaliPatch[] = [ | ||
{ | ||
selector: { | ||
type: 'interface', | ||
name: 'javax/net/ssl/X509TrustManager', | ||
}, | ||
methods: [ | ||
{ | ||
name: 'X509TrustManager#checkClientTrusted (javax)', | ||
signature: | ||
'checkClientTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V', | ||
replacementLines: RETURN_VOID_SMALI, | ||
}, | ||
{ | ||
name: 'X509TrustManager#checkServerTrusted (javax)', | ||
signature: | ||
'checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V', | ||
replacementLines: RETURN_VOID_SMALI, | ||
}, | ||
{ | ||
name: 'X509TrustManager#getAcceptedIssuers (javax)', | ||
signature: 'getAcceptedIssuers()[Ljava/security/cert/X509Certificate;', | ||
replacementLines: RETURN_EMPTY_CERT_ARRAY_SMALI, | ||
}, | ||
], | ||
}, | ||
{ | ||
selector: { | ||
type: 'interface', | ||
name: 'javax/net/ssl/HostnameVerifier', | ||
}, | ||
methods: [ | ||
{ | ||
name: 'HostnameVerifier#verify (javax)', | ||
signature: 'verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z', | ||
replacementLines: RETURN_TRUE_SMALI, | ||
}, | ||
], | ||
}, | ||
{ | ||
selector: { | ||
type: 'class', | ||
name: 'com/squareup/okhttp/CertificatePinner', | ||
}, | ||
methods: [ | ||
{ | ||
name: 'HostnameVerifier#check (OkHttp 2.5)', | ||
// Inspired by: https://github.com/Fuzion24/JustTrustMe/blob/152557d/app/src/main/java/just/trust/me/Main.java#L456-L478 | ||
signature: 'check(Ljava/lang/String;Ljava/util/List;)V', | ||
replacementLines: RETURN_VOID_SMALI, | ||
}, | ||
], | ||
}, | ||
{ | ||
selector: { | ||
type: 'class', | ||
name: 'okhttp3/CertificatePinner', | ||
}, | ||
methods: [ | ||
{ | ||
name: 'CertificatePinner#check (OkHttp 3.x)', | ||
// Inspired by: https://github.com/Fuzion24/JustTrustMe/blob/152557d/app/src/main/java/just/trust/me/Main.java#L480-L499 | ||
signature: 'check(Ljava/lang/String;Ljava/util/List;)V', | ||
replacementLines: RETURN_VOID_SMALI, | ||
}, | ||
{ | ||
name: 'CertificatePinner#check (OkHttp 4.2)', | ||
// Inspired by: https://github.com/Fuzion24/JustTrustMe/blob/152557d/app/src/main/java/just/trust/me/Main.java#L539-L558 | ||
signature: | ||
'check$okhttp(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V', | ||
replacementLines: RETURN_VOID_SMALI, | ||
}, | ||
], | ||
}, | ||
] | ||
|
||
export default smaliPatches |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import * as os from 'os' | ||
import * as fs from '../../utils/fs' | ||
import escapeStringRegexp = require('escape-string-regexp') | ||
import chalk = require('chalk') | ||
|
||
import parseSmaliHead, { SmaliHead } from './parse-head' | ||
import smaliPatches from './patches' | ||
import { SmaliPatch } from './types' | ||
|
||
/** | ||
* Process the given Smali file and apply applicable patches. | ||
* @returns whether patches were applied | ||
*/ | ||
export default async function processSmaliFile( | ||
filePath: string, | ||
log: (message: string) => void, | ||
): Promise<boolean> { | ||
let originalContent = await fs.readFile(filePath, 'utf-8') | ||
|
||
if (os.type() === 'Windows_NT') { | ||
// Replace CRLF with LF, so that patches can just use '\n' | ||
originalContent = originalContent.replace(/\r\n/g, '\n') | ||
} | ||
|
||
let patchedContent = originalContent | ||
|
||
const smaliHead = parseSmaliHead(patchedContent) | ||
if (smaliHead.isInterface) return false | ||
|
||
const applicablePatches = smaliPatches.filter(patch => | ||
selectorMatchesClass(patch, smaliHead), | ||
) | ||
if (applicablePatches.length === 0) return false | ||
|
||
const applicableMethods = applicablePatches.flatMap(patch => patch.methods) | ||
for (const method of applicableMethods) { | ||
const pattern = createMethodPattern(method.signature) | ||
patchedContent = patchedContent.replace( | ||
pattern, | ||
(_, openingLine: string, body: string, closingLine: string) => { | ||
const bodyLines = body | ||
.split('\n') | ||
.map(line => line.replace(/^ /, '')) | ||
|
||
const patchedBodyLines = [ | ||
'# inserted by apk-mitm to disable certificate pinning', | ||
...method.replacementLines, | ||
'', | ||
'# commented out by apk-mitm to disable old method body', | ||
'# ', | ||
...bodyLines.map(line => `# ${line}`), | ||
] | ||
|
||
log( | ||
chalk`{bold ${smaliHead.name}}{dim :} Applied {bold ${method.name}} patch`, | ||
) | ||
|
||
return [ | ||
openingLine, | ||
...patchedBodyLines.map(line => ` ${line}`), | ||
closingLine, | ||
] | ||
.map(line => line.trimEnd()) | ||
.join('\n') | ||
}, | ||
) | ||
} | ||
|
||
if (originalContent !== patchedContent) { | ||
if (os.type() === 'Windows_NT') { | ||
// Replace LF with CRLF again | ||
patchedContent = patchedContent.replace(/\n/g, '\r\n') | ||
} | ||
|
||
await fs.writeFile(filePath, patchedContent) | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* Creates a full RegExp pattern for finding a method based on its signature. | ||
*/ | ||
function createMethodPattern(signature: string): RegExp { | ||
const escapedSignature = escapeStringRegexp(signature) | ||
return new RegExp( | ||
`(\\.method public (?:final )?${escapedSignature})\\n([^]+?)\\n(\\.end method)`, | ||
'g', | ||
) | ||
} | ||
|
||
/** | ||
* Checks whether the given patch can be applied to the given | ||
* class based on its name and the interfaces it implements. | ||
*/ | ||
function selectorMatchesClass( | ||
patch: SmaliPatch, | ||
smaliHead: SmaliHead, | ||
): boolean { | ||
return ( | ||
/* The class matches */ | ||
(patch.selector.type === 'class' && | ||
patch.selector.name === smaliHead.name) || | ||
/* One of the implemented interfaces matches */ | ||
(patch.selector.type === 'interface' && | ||
smaliHead.implements.includes(patch.selector.name)) | ||
) | ||
} |
Oops, something went wrong.