From 1fbc7f14a08fa9dadb4afb64304c61c196856819 Mon Sep 17 00:00:00 2001 From: Sleeeee Date: Mon, 4 May 2026 04:50:49 -0400 Subject: [PATCH 1/4] gitignore pycache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0a7f648..986faec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .gadgetCache/ +__pycache__/ From ff0fcdc84e6a7e3d0e7fe28827337d8a69fa80da Mon Sep 17 00:00:00 2001 From: Sleeeee Date: Mon, 4 May 2026 04:55:17 -0400 Subject: [PATCH 2/4] Fix Apktool namespace mismatching bug --- APK.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/APK.py b/APK.py index 1fefdbc..d98a15d 100644 --- a/APK.py +++ b/APK.py @@ -231,8 +231,12 @@ 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: + fixed = self._fix_apktool_error(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)}") @@ -495,7 +499,7 @@ def _find_smali_file(root: str, rel: str): r'(?m)^(\s*\.method\s+static\s+constructor\s+\(\)V\s*$)', r'\1\n .registers 1', clinit_block, - count=1, + count=1, ) # Insert the load instructions after the (possibly updated) .registers line. @@ -545,3 +549,30 @@ def _fix_private_resources(self, base: str): fh.write(ns) count += 1 Log.verbose(f" Forced {count} private resource refs to public") + + def _fix_apktool_error(self, stderr: str) -> bool: + pattern = re.compile(r"W:\s+(?P.*?):\d+:\s+error:\s+attribute\s+android:(?P[^\s]+)\s+not found\.") + matches = list(pattern.finditer(stderr)) + + if not matches: + return False + + Log.info("Detected resource namespace mismatch, 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 From 499873f3520853b25ff0420515089506e620945d Mon Sep 17 00:00:00 2001 From: Sleeeee Date: Mon, 4 May 2026 07:41:09 -0400 Subject: [PATCH 3/4] Fix Apktool duplicate attributes and incompatible flags errors --- APK.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/APK.py b/APK.py index d98a15d..b39b66a 100644 --- a/APK.py +++ b/APK.py @@ -550,13 +550,7 @@ def _fix_private_resources(self, base: str): count += 1 Log.verbose(f" Forced {count} private resource refs to public") - def _fix_apktool_error(self, stderr: str) -> bool: - pattern = re.compile(r"W:\s+(?P.*?):\d+:\s+error:\s+attribute\s+android:(?P[^\s]+)\s+not found\.") - matches = list(pattern.finditer(stderr)) - - if not matches: - return False - + def _fix_apktool_namespaces(self, matches): Log.info("Detected resource namespace mismatch, trying to resolve automatically") count = 0 @@ -575,4 +569,75 @@ def _fix_apktool_error(self, stderr: str) -> bool: except Exception as e: Log.abort(e) Log.info(f"Fixed {count} broken namespaces, restarting build") - return True + + def _fix_apktool_duplicates(self, matches): + Log.info("Detected duplicate attribute error, 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") + + def _fix_apktool_incompatible_flags(self, matches): + Log.info("Detected incompatible flags error, 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") + + def _fix_apktool_error(self, stderr: str) -> bool: + # Matches "attribue android:example not found" error + namespace_error = re.compile(r"W:\s+(?P.*?):\d+:\s+error:\s+attribute\s+android:(?P[^\s]+)\s+not found\.") + matches = list(namespace_error.finditer(stderr)) + if matches: + self._fix_apktool_namespaces(matches) + return True + + # Matches "duplicate attribute" error + duplicates_error = re.compile(r"W:\s+(?P.*?):\d+:\s+error:\s+duplicate\s+attribute\.") + matches = list(duplicates_error.finditer(stderr)) + if matches: + self._fix_apktool_duplicates(matches) + return True + + # Matches "incompatible with attribute" error + incompatible_flags_error = re.compile(r"W:\s+(?P.*?):\d+:\s*error:\s*'(?P[^']+)'\s*is incompatible with attribute\s*(?P\w+).*?\[(?P[^\]]+)\]") + matches = list(incompatible_flags_error.finditer(stderr)) + if matches: + self._fix_apktool_incompatible_flags(matches) + return True + + return False + From 4ea443df14bc7bec106185f03727caee1e03104c Mon Sep 17 00:00:00 2001 From: Sleeeee Date: Tue, 30 Jun 2026 13:05:25 +0200 Subject: [PATCH 4/4] Add context for Apktool errors, make aggressive fixing optional (--fix-aggressive) --- APK.py | 100 +++--------------------------------------- ErrorHandler.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++++ patch-apk.py | 5 ++- 3 files changed, 121 insertions(+), 96 deletions(-) create mode 100644 ErrorHandler.py diff --git a/APK.py b/APK.py index b39b66a..7a96c4d 100644 --- a/APK.py +++ b/APK.py @@ -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. @@ -19,7 +20,7 @@ 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) @@ -27,6 +28,7 @@ def __init__(self, apk_path: str, workdir: Optional[str] = None, verbose: bool = 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 @@ -231,8 +233,10 @@ 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) + while ok_required and cp.returncode != 0: - fixed = self._fix_apktool_error(cp.stderr) + # 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: @@ -549,95 +553,3 @@ def _fix_private_resources(self, base: str): fh.write(ns) count += 1 Log.verbose(f" Forced {count} private resource refs to public") - - def _fix_apktool_namespaces(self, matches): - Log.info("Detected resource namespace mismatch, 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") - - def _fix_apktool_duplicates(self, matches): - Log.info("Detected duplicate attribute error, 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") - - def _fix_apktool_incompatible_flags(self, matches): - Log.info("Detected incompatible flags error, 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") - - def _fix_apktool_error(self, stderr: str) -> bool: - # Matches "attribue android:example not found" error - namespace_error = re.compile(r"W:\s+(?P.*?):\d+:\s+error:\s+attribute\s+android:(?P[^\s]+)\s+not found\.") - matches = list(namespace_error.finditer(stderr)) - if matches: - self._fix_apktool_namespaces(matches) - return True - - # Matches "duplicate attribute" error - duplicates_error = re.compile(r"W:\s+(?P.*?):\d+:\s+error:\s+duplicate\s+attribute\.") - matches = list(duplicates_error.finditer(stderr)) - if matches: - self._fix_apktool_duplicates(matches) - return True - - # Matches "incompatible with attribute" error - incompatible_flags_error = re.compile(r"W:\s+(?P.*?):\d+:\s*error:\s*'(?P[^']+)'\s*is incompatible with attribute\s*(?P\w+).*?\[(?P[^\]]+)\]") - matches = list(incompatible_flags_error.finditer(stderr)) - if matches: - self._fix_apktool_incompatible_flags(matches) - return True - - return False - diff --git a/ErrorHandler.py b/ErrorHandler.py new file mode 100644 index 0000000..a9717eb --- /dev/null +++ b/ErrorHandler.py @@ -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.*?):\d+:\s+error:\s+attribute\s+android:(?P[^\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.*?):\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.*?):\d+:\s*error:\s*'(?P[^']+)'\s*is incompatible with attribute\s*(?P\w+).*?\[(?P[^\]]+)\]") + 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 diff --git a/patch-apk.py b/patch-apk.py index 4f8c5d8..6f25e72 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -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() @@ -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])