Initial commit
This commit is contained in:
commit
6c12a11ef6
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.js
|
||||
__pycache__
|
||||
*.jar
|
19
LICENSE.md
Normal file
19
LICENSE.md
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2022 Phyks
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
||||
Frigida
|
||||
=======
|
||||
|
||||
Inject frida-gadget into an existing APK file.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* Java
|
||||
* Android SDK tools in `$PATH` (`aapt`, `keytool`, etc.).
|
||||
* [`apktool`](https://github.com/iBotPeaches/Apktool)
|
||||
* `openssl` / `xxd` / `tr` commands
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
python3 -m venv .venv
|
||||
./.venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
./.venv/bin/python -m frigida $PATH_TO_APK_FILE $TARGET_ARCHITECTURE
|
||||
```
|
||||
|
||||
Current working directory will be used for extracting APK. At the end of the
|
||||
process:
|
||||
|
||||
* `frida-gadget-script.js` in the working directory contains a base Frida Gadget
|
||||
script to load.
|
||||
* `$APK_NAME/dist/$APK_NAME-aligned-debugSigned.apk` in the working directory
|
||||
contains the APK with frida-gadget injected and ready to be installed.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Released under an MIT license.
|
325
frigida/__init__.py
Normal file
325
frigida/__init__.py
Normal file
@ -0,0 +1,325 @@
|
||||
import logging
|
||||
import lzma
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import apksigcopier
|
||||
import jinja2
|
||||
import lief
|
||||
import requests
|
||||
|
||||
|
||||
GADGET_SCRIPT_TEMPLATE = """
|
||||
Java.perform(function () {
|
||||
/**
|
||||
* Convert an hex-string to an array of int.
|
||||
*
|
||||
* Based on an implementation from crypto-js.
|
||||
* Taken from https://stackoverflow.com/a/34356351
|
||||
*/
|
||||
function hexToBytes(hex) {
|
||||
for (var bytes = [], c = 0; c < hex.length; c += 2) {
|
||||
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/*
|
||||
* Bypass package certificate hash.
|
||||
*
|
||||
* Altering the APK (embedding frida-gadget + resigning) does change the
|
||||
* SHA-1 of the signing certificate.
|
||||
*
|
||||
* GCM/Firebase sends this to the server to check the APK is authorized in
|
||||
* developer account (they have to declare their signing certificate for
|
||||
* distributed APK embedding GCM/Firebase).
|
||||
*
|
||||
* Hence, GCM/Firebase is no longer working in altered APKs, often
|
||||
* materializing through an "Play Store / Play Services are not available"
|
||||
* error message.
|
||||
*
|
||||
* See https://github.com/firebase/firebase-android-sdk/blob/dcf82a5296b86f04c0b8ce162e0437c7aeb42734/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java
|
||||
* and https://github.com/firebase/firebase-android-sdk/blob/dcf82a5296b86f04c0b8ce162e0437c7aeb42734/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java.
|
||||
*
|
||||
* This hook bypasses the certificate check by force-returning the original one.
|
||||
*/
|
||||
// Output of:
|
||||
// keytool -printcert -jarfile "ORIGINAL_UNALTERED_APK.apk" | grep "SHA 1:"
|
||||
var INITIAL_CERTIFICATE_HASH = "{{ initial_certificate_hash }}";
|
||||
// Output of:
|
||||
// mkdir sigs && apksigcopier extract base.apk sigs
|
||||
// openssl pkcs7 -in sigs/BNDLTOOL.RSA -inform DER -print_certs | openssl x509 -outform DER | xxd -p | tr -d \\n
|
||||
var RAW_SIGNING_CERTIFICATE = "{{ raw_signing_certificate }}";
|
||||
|
||||
// Bypass through GMS API
|
||||
var AndroidUtilsLight = Java.use('com.google.android.gms.common.util.AndroidUtilsLight');
|
||||
AndroidUtilsLight.getPackageCertificateHashBytes.implementation = function (context, packageName) {
|
||||
var value = Java.array("byte", hexToBytes(INITIAL_CERTIFICATE_HASH.replaceAll(':', '')))
|
||||
return value;
|
||||
}
|
||||
|
||||
// Bypass accessing certs through Android package manager API
|
||||
var Signature = Java.use('android.content.pm.Signature');
|
||||
Java.use('android.app.ApplicationPackageManager').getPackageInfo.overload('java.lang.String', 'int').implementation = function (packageName, flags) {
|
||||
// See https://developer.android.com/reference/android/content/pm/PackageManager#GET_SIGNING_CERTIFICATES
|
||||
// Android API level >= 28
|
||||
if (packageName == '{{ package_name }}' && flags == 134217728) {
|
||||
var value = this.getPackageInfo(packageName, flags);
|
||||
// Overload the returned signatures
|
||||
value.signingInfo.value.getApkContentsSigners.implementation = function () {
|
||||
// TODO: Handle multiple signers / history
|
||||
return Java.array(
|
||||
'android.content.pm.Signature',
|
||||
[Signature.$new(RAW_SIGNING_CERTIFICATE)]
|
||||
);
|
||||
};
|
||||
|
||||
value.signingInfo.value.getSigningCertificateHistory.implementation = function () {
|
||||
return Java.array(
|
||||
'android.content.pm.Signature',
|
||||
[Signature.$new(RAW_SIGNING_CERTIFICATE)]
|
||||
);
|
||||
};
|
||||
|
||||
return value;
|
||||
}
|
||||
return this.getPackageInfo(packageName, flags);
|
||||
};
|
||||
|
||||
// Also unblock FCM requests.
|
||||
var d = Java.use('com.google.firebase.installations.remote.RequestLimiter');
|
||||
d.isRequestAllowed.implementation = function () {
|
||||
var ret = this.isRequestAllowed();
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Log SSL/TLS keys.
|
||||
*
|
||||
* See https://github.com/PiRogueToolSuite/pirogue-cli/blob/main/pirogue_cli/frida-scripts/log_ssl_keys.js
|
||||
*/
|
||||
function _log_ssl_keys(SSL_CTX_new, SSL_CTX_set_keylog_callback) {
|
||||
function log_key(ssl, line) {
|
||||
const s_line = new NativePointer(line).readCString();
|
||||
console.log(s_line);
|
||||
}
|
||||
const keylogCallback = new NativeCallback(log_key, 'void', ['pointer', 'pointer'])
|
||||
|
||||
Interceptor.attach(SSL_CTX_new, {
|
||||
onLeave: function(retval) {
|
||||
const ssl = new NativePointer(retval);
|
||||
if (!ssl.isNull()) {
|
||||
const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']);
|
||||
SSL_CTX_set_keylog_callbackFn(ssl, keylogCallback);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_log_ssl_keys(
|
||||
Module.findExportByName('libssl.so', 'SSL_CTX_new'),
|
||||
Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback')
|
||||
);
|
||||
});
|
||||
"""
|
||||
|
||||
def get_package_name(apk_path):
|
||||
"""
|
||||
Get the package name (com.example) from an APK.
|
||||
|
||||
:param apk_path: Path to the APK file.
|
||||
:returns: Package name.
|
||||
"""
|
||||
aapt_dump_out = subprocess.check_output(
|
||||
['aapt', 'dump', 'badging', apk_path]
|
||||
).decode()
|
||||
return re.search(r"name='(.*?)'", aapt_dump_out).group(1)
|
||||
|
||||
|
||||
def decompress_apk(
|
||||
apk_path,
|
||||
should_not_decode_res=False, should_not_decode_src=True
|
||||
):
|
||||
"""
|
||||
Decompress APK file using apktool
|
||||
|
||||
APK is uncompressed in a folder named without the .apk extension in the
|
||||
working directory.
|
||||
|
||||
:param apk_path: Path to the APK file.
|
||||
:param should_not_decode_res: Do not decode resources. (default False)
|
||||
:param should_not_decode_src: Do not decode sources. Speeds up
|
||||
unpacking/repacking. (default True)
|
||||
"""
|
||||
logging.info('Uncompressing apk with apktool...')
|
||||
optional_args = []
|
||||
if should_not_decode_res:
|
||||
optional_args.append('-r')
|
||||
if should_not_decode_src:
|
||||
optional_args.append('-s')
|
||||
|
||||
return subprocess.run(
|
||||
['apktool', 'd'] + optional_args + [apk_path]
|
||||
)
|
||||
|
||||
|
||||
def inject_frida_gadget(uncompressed_apk_path, target_architecture):
|
||||
"""
|
||||
Inject frida gadget in the libs from the APK.
|
||||
|
||||
See https://fadeevab.com/frida-gadget-injection-on-android-no-root-2-methods/.
|
||||
|
||||
:param uncompressed_apk_path: Folder of the output from apktool.
|
||||
:param target_architecture: Architecture to target for injection.
|
||||
"""
|
||||
libs_path = Path(uncompressed_apk_path) / 'lib'
|
||||
for dir in os.listdir(libs_path):
|
||||
# Find correct subdirectory based on target architecture
|
||||
if dir.startswith(target_architecture):
|
||||
libs_path = libs_path / dir
|
||||
break
|
||||
libfridagadget_name = 'libgadget.so'
|
||||
|
||||
if not os.path.isfile(libs_path / libfridagadget_name):
|
||||
logging.info('Download Frida-gadget into apk libs...')
|
||||
latest_release = requests.get(
|
||||
'https://api.github.com/repos/frida/frida/releases/latest'
|
||||
).json()
|
||||
download_url = next(
|
||||
asset['browser_download_url']
|
||||
for asset in latest_release['assets']
|
||||
if (
|
||||
asset['name'].startswith('frida-gadget-')
|
||||
and asset['name'].endswith(f'-android-{target_architecture}.so.xz')
|
||||
)
|
||||
)
|
||||
req = requests.get(
|
||||
download_url
|
||||
)
|
||||
with open(libs_path / libfridagadget_name, 'wb') as fh:
|
||||
fh.write(lzma.LZMADecompressor().decompress(req.content))
|
||||
|
||||
logging.info('Injecting Frida-gadget lib in apk libs...')
|
||||
apk_libs = [item for item in os.listdir(libs_path)]
|
||||
if not apk_libs:
|
||||
logging.error('APK does not have any lib for injection!')
|
||||
raise Exception('APK does not have any lib for injection!')
|
||||
for lib in apk_libs:
|
||||
libnative = lief.parse(str(libs_path / lib))
|
||||
libnative.add_library(libfridagadget_name) # Injection!
|
||||
libnative.write(str(libs_path / lib))
|
||||
|
||||
|
||||
def rebuild_apk(uncompressed_apk_path, optional_args=['--use-aapt2']):
|
||||
"""
|
||||
Rebuild APK
|
||||
|
||||
New (rebuilt) APK is generated in ``dist/`` folder under the unpacked APK
|
||||
tree.
|
||||
|
||||
:param uncompressed_apk_path: Path to the apk ressources.
|
||||
:param optional_args: Extra arguments to pass to apktool.
|
||||
"""
|
||||
logging.info('Ensure android:extractNativeLibs is true in the AndroidManifest.xml...')
|
||||
# https://stackoverflow.com/questions/42998083/setting-androidextractnativelibs-false-to-reduce-app-size
|
||||
with open(Path(uncompressed_apk_path) / 'AndroidManifest.xml', 'r') as fh:
|
||||
android_manifest = fh.read()
|
||||
if 'android:extractNativeLibs="false"' in android_manifest:
|
||||
with open(Path(uncompressed_apk_path) / 'AndroidManifest.xml', 'w') as fh:
|
||||
fh.write(
|
||||
android_manifest.replace(
|
||||
'android:extractNativeLibs="false"',
|
||||
'android:extractNativeLibs="true"',
|
||||
)
|
||||
)
|
||||
|
||||
logging.info('Rebuilding APK...')
|
||||
return subprocess.run(
|
||||
['apktool', 'b'] + optional_args + [uncompressed_apk_path]
|
||||
)
|
||||
|
||||
|
||||
def resign_apk(apk_path):
|
||||
"""
|
||||
Resign the APK with uber apk signer.
|
||||
|
||||
APK is generated beside the source APK file with a ``-aligned-debugSigned``
|
||||
suffix.
|
||||
|
||||
:param apk_path: Path to the APK to sign.
|
||||
"""
|
||||
latest_release = requests.get(
|
||||
'https://api.github.com/repos/patrickfav/uber-apk-signer/releases/latest'
|
||||
).json()
|
||||
download_url, jar_name = next(
|
||||
(asset['browser_download_url'], asset['name'])
|
||||
for asset in latest_release['assets']
|
||||
if asset['name'].endswith('.jar')
|
||||
)
|
||||
if not os.path.isfile(jar_name):
|
||||
with open(jar_name, 'wb') as fh:
|
||||
fh.write(
|
||||
requests.get(download_url).content
|
||||
)
|
||||
return subprocess.run(
|
||||
[
|
||||
'java', '-jar', jar_name,
|
||||
'-a', apk_path
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def prepare_gadget_script(apk_path, package_name):
|
||||
"""
|
||||
Prepare the Frida Gadget script to be loaded upon APK loading to ensure
|
||||
correct behavior of the APK:
|
||||
* Masking re-signing by overloading Android APIs and presenting
|
||||
original certificate hash.
|
||||
* ...
|
||||
|
||||
Script is generated as ``frida-gadget-script.js`` in the working directory.
|
||||
|
||||
:param apk_path: Path to the original APK.
|
||||
:param package_name: Name of the package (e.g. `com.example`).
|
||||
"""
|
||||
# Get certificate hash (SHA-1) from the initial APK
|
||||
initial_certificate_hash = (
|
||||
next(
|
||||
line
|
||||
for line in subprocess.check_output(
|
||||
['keytool', '-printcert', '-jarfile', apk_path]
|
||||
).decode().splitlines()
|
||||
if line.strip().startswith('SHA 1')
|
||||
).strip().replace('SHA 1: ', '')
|
||||
)
|
||||
|
||||
# Get the signing certificate from the original APK
|
||||
with tempfile.TemporaryDirectory() as tmp_sig_out_path:
|
||||
# Get certificates from original APK
|
||||
apksigcopier.do_extract(apk_path, tmp_sig_out_path)
|
||||
# Get the hexdump of the certificate
|
||||
cert_file = os.path.join(
|
||||
tmp_sig_out_path,
|
||||
next(
|
||||
file for file in os.listdir(tmp_sig_out_path) if file.endswith('.RSA')
|
||||
)
|
||||
)
|
||||
raw_signing_certificate = subprocess.check_output(
|
||||
f'openssl pkcs7 -in {cert_file} -inform DER -print_certs | openssl x509 -outform DER | xxd -p | tr -d \\n',
|
||||
shell=True
|
||||
).decode()
|
||||
|
||||
with open('./frida-gadget-script.js', 'w') as fh:
|
||||
template = jinja2.Environment(
|
||||
loader=jinja2.BaseLoader
|
||||
).from_string(GADGET_SCRIPT_TEMPLATE)
|
||||
fh.write(
|
||||
template.render(
|
||||
initial_certificate_hash=initial_certificate_hash,
|
||||
raw_signing_certificate=raw_signing_certificate,
|
||||
package_name=package_name,
|
||||
)
|
||||
)
|
32
frigida/__main__.py
Normal file
32
frigida/__main__.py
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from frigida import (
|
||||
get_package_name, decompress_apk, inject_frida_gadget, rebuild_apk,
|
||||
resign_apk, prepare_gadget_script
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("apk", help="Path to the original APK file.")
|
||||
parser.add_argument("arch", help="Architecture to target (e.g. arm64).")
|
||||
args = parser.parse_args()
|
||||
|
||||
package_name = get_package_name(args.apk)
|
||||
|
||||
uncompressed_apk_path = os.path.splitext(os.path.basename(args.apk))[0]
|
||||
|
||||
decompress_apk(args.apk)
|
||||
inject_frida_gadget(uncompressed_apk_path, args.arch)
|
||||
rebuild_apk(uncompressed_apk_path)
|
||||
|
||||
rebuilt_apk_path = Path(uncompressed_apk_path) / 'dist' / os.path.basename(args.apk)
|
||||
resign_apk(rebuilt_apk_path)
|
||||
|
||||
prepare_gadget_script(args.apk, package_name)
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
apksigcopier
|
||||
jinja2
|
||||
lief
|
||||
requests
|
Loading…
Reference in New Issue
Block a user