Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
.gadgetCache/
__pycache__/
16 changes: 12 additions & 4 deletions APK.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from typing import List, Optional
from Log import Log
from ErrorHandler import ErrorHandler
from packaging.version import parse as parse_version

# If you put FridaGadget.py next to this file, this import will work.
Expand All @@ -19,14 +20,15 @@ class APK:

NULL_DECODED_DRAWABLE_COLOR = "#000000ff"

def __init__(self, apk_path: str, workdir: Optional[str] = None, verbose: bool = False):
def __init__(self, apk_path: str, workdir: Optional[str] = None, verbose: bool = False, fix_aggressive: bool = False):
self.apk_path = os.path.abspath(apk_path)
self.verbose = verbose
self._check_exists(self.apk_path)
self._tmpbase = tempfile.TemporaryDirectory() if workdir is None else None
self.workdir = workdir or self._tmpbase.name
Path(self.workdir).mkdir(parents=True, exist_ok=True)
self.has_been_merged = False
self.error_handler = ErrorHandler(fix_aggressive)

# ---------- Creation ----------
@classmethod
Expand Down Expand Up @@ -231,8 +233,14 @@ def _apktool(self, args: List[str], ok_required: bool = False):
Log.verbose(f"[apktool] {exe} {' '.join(args)}\n{cp.stdout}")
if cp.returncode != 0:
Log.verbose(cp.stderr)
if ok_required and cp.returncode != 0:
Log.abort(f"apktool failed: \n\n{exe}{' '.join(args)}\n\n" + cp.stdout +"\n\n---\n\n"+ cp.stderr)

while ok_required and cp.returncode != 0:
# Pass error to handler (action picked based on --fix-aggressive flag)
fixed = self.error_handler.handle(cp.stderr)
if fixed:
cp = subprocess.run([exe, *args], input="\r\n", text=True, capture_output=True)
else:
Log.abort(f"apktool failed: \n\n{exe}{' '.join(args)}\n\n" + cp.stdout +"\n\n---\n\n"+ cp.stderr)

def _run(self, args: List[str], ok_required: bool = False):
Log.verbose(f"[{args[0]}] {' '.join(args)}")
Expand Down Expand Up @@ -495,7 +503,7 @@ def _find_smali_file(root: str, rel: str):
r'(?m)^(\s*\.method\s+static\s+constructor\s+<clinit>\(\)V\s*$)',
r'\1\n .registers 1',
clinit_block,
count=1,
count=1,
)

# Insert the load instructions after the (possibly updated) .registers line.
Expand Down
112 changes: 112 additions & 0 deletions ErrorHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import re, os
from typing import List, Match
from Log import Log

class ErrorHandler:
def __init__(self, fix_aggressive: bool = False):
self.fix_aggressive = fix_aggressive

def handle(self, stderr: str) -> bool:
# Matches "attribue android:example not found" error
namespace_error = re.compile(r"W:\s+(?P<path>.*?):\d+:\s+error:\s+attribute\s+android:(?P<attr>[^\s]+)\s+not found\.")
matches = list(namespace_error.finditer(stderr))
if matches:
return self._handle_apktool_namespaces(matches)

# Matches "duplicate attribute" error
duplicates_error = re.compile(r"W:\s+(?P<path>.*?):\d+:\s+error:\s+duplicate\s+attribute\.")
matches = list(duplicates_error.finditer(stderr))
if matches:
self._handle_apktool_duplicates(matches)
return True

# Matches "incompatible with attribute" error
incompatible_flags_error = re.compile(r"W:\s+(?P<path>.*?):\d+:\s*error:\s*'(?P<value>[^']+)'\s*is incompatible with attribute\s*(?P<attr>\w+).*?\[(?P<allowed>[^\]]+)\]")
matches = list(incompatible_flags_error.finditer(stderr))
if matches:
self._handle_apktool_incompatible_flags(matches)
return True

return False

def _handle_apktool_namespaces(self, matches: List[Match]) -> bool:
Log.warn("Detected resource namespace mismatch. This issue is likely related to https://github.com/iBotPeaches/Apktool/pull/4137\nTo solve this, you can build Apktool from source, update to 3.0.3 (once it is released), or run patch-apk with --fix-aggressive")

if not self.fix_aggressive:
return False

Log.info("Trying to resolve automatically...")
count = 0
for m in matches:
path, attr = m.group("path"), m.group("attr")
data = ""
try:
with open(path, "r", encoding="utf-8") as f:
data = f.read()
if data:
data = data.replace(f"android:{attr}", attr)
with open(path, "w", encoding="utf-8") as f:
f.write(data)
count += 1

except Exception as e:
Log.abort(e)
Log.info(f"Fixed {count} broken namespaces, restarting build")
return True

def _handle_apktool_duplicates(self, matches: List[Match]) -> bool:
Log.warn("Detected duplicate attribute error, this issue has no known fix on Apktool side and is likely caused by heavy obfuscation\nTo solve this, you can run patch-apk with --fix-aggressive")

if not self.fix_aggressive:
return False

Log.info("Trying to resolve automatically...")
# Matches duplicates attributes, and groups the first attribute occurence (\1), the duplicate (\2) and anything in between (\3)
dup_pattern = re.compile(r'(\b([a-zA-Z0-9_:]+)="[^"]*")(.*?)\b\2="[^"]*"')

count = 0
for m in matches:
path = m.group("path")
data = ""
try:
with open(path, "r", encoding="utf-8") as f:
data = f.read()
if data and dup_pattern.search(data):
while dup_pattern.search(data):
# Rewrites contents without the duplicate attribute in \2
data = dup_pattern.sub(r"\1\3", data)

with open(path, "w", encoding="utf-8") as f:
f.write(data)
count += 1

except Exception as e:
Log.abort(e)
Log.info(f"Removed {count} duplicates, restarting build")
return True

def _handle_apktool_incompatible_flags(self, matches: List[Match]) -> bool:
Log.warn("Detected incompatible flags error. This issue is likely related to https://github.com/iBotPeaches/Apktool/pull/4140\nTo solve this, you can build Apktool from source, update to 3.0.3 (once it is released), or run patch-apk with --fix-aggressive")

if not self.fix_aggressive:
return False

Log.info("Trying to resolve automatically...")
count = 0
for m in matches:
path, attr, value = m.group("path"), m.group("attr"), m.group("value")
incomp_pattern = re.compile(rf'\s+(?:[a-zA-Z0-9_]+:)?{attr}="0x0"')
data = ""
try:
with open(path, "r", encoding="utf-8") as f:
data = f.read()
if data:
data = incomp_pattern.sub("", data)
with open(path, "w", encoding="utf-8") as f:
f.write(data)
count += 1

except Exception as e:
Log.abort(e)
Log.info(f"Removed {count} incompatible flags, restarting build")
return True
5 changes: 3 additions & 2 deletions patch-apk.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def main():
ap.add_argument("--no-install", action="store_true", help="Do not install to device at the end")
ap.add_argument("--keep-splits", action="store_true", help="Keep split APKs when extracting")
ap.add_argument("--save-apk", help="Copy final APK to this path")
ap.add_argument("--fix-aggressive", action="store_true", help="Attempt to fix known Apktool errors automatically")
ap.add_argument("-v", "--verbose", action="store_true")
args = ap.parse_args()

Expand Down Expand Up @@ -158,10 +159,10 @@ def main():

if len(local_apks) == 1:
Log.info("Single APK detected")
base = APK(local_apks[0])
base = APK(local_apks[0], fix_aggressive=args.fix_aggressive)
else:
Log.info(f"Split APK set detected ({len(local_apks)})")
apks = [APK(p) for p in local_apks]
apks = [APK(p, fix_aggressive=args.fix_aggressive) for p in local_apks]

# Find base APK (heuristic: filename containing "base", else first)
base = next((p for p in apks if "base.apk" in p.apk_path), apks[0])
Expand Down