Skip to content

Instantly share code, notes, and snippets.

@hackcatml
Last active October 11, 2024 09:15
Show Gist options
  • Save hackcatml/2dfd671599d563488f742c96edc71b7b to your computer and use it in GitHub Desktop.
Save hackcatml/2dfd671599d563488f742c96edc71b7b to your computer and use it in GitHub Desktop.
Unable to perform state transition issue
This issue occurred in the recently updated version of "com.android.art".
@hackcatml
Copy link
Author

hackcatml commented Oct 2, 2024

The frida-java-bridge/lib/android.js file was modified by referring to this pull request.
This android.js dynamically searches for the functions, allowing it to be used even with older versions of com.android.art.

diff --git a/lib/android.js b/lib/android.js
index f5dcfbe..6a8cf5d 100644
--- a/lib/android.js
+++ b/lib/android.js
@@ -1893,30 +1893,66 @@ function ensureArtKnowsHowToHandleReplacementMethods (vm) {
 
   const apiLevel = getAndroidApiLevel();
 
-  const mayUseCollector = (apiLevel > 28)
-    ? (type) => {
-        const impl = Module.findExportByName('libart.so', '_ZNK3art2gc4Heap15MayUseCollectorENS0_13CollectorTypeE');
-        if (impl === null) {
-          return false;
-        }
-        return new NativeFunction(impl, 'bool', ['pointer', 'int'])(getApi().artHeap, type);
+  let copyingPhase = null;
+  if (apiLevel > 28) {
+    copyingPhase = Module.findExportByName('libart.so', '_ZN3art2gc9collector17ConcurrentCopying12CopyingPhaseEv');
+  } else if (apiLevel > 22) {
+    copyingPhase = Module.findExportByName('libart.so', '_ZN3art2gc9collector17ConcurrentCopying12MarkingPhaseEv');
+  }
+  if (Process.arch === 'arm64' && copyingPhase === null) {
+    let CopyingPhase_string_found_addr;
+    let CopyingPhase_string = '43 6f 70 79 69 6e 67 50 68 61 73 65';
+    const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0];
+    for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, CopyingPhase_string)) {
+      if (match) {
+        CopyingPhase_string_found_addr = match.address.toString();
+        break;
       }
-    : () => false;
-  const kCollectorTypeCMC = 3;
-
-  if (mayUseCollector(kCollectorTypeCMC)) {
-    Interceptor.attach(Module.getExportByName('libart.so', '_ZN3art6Thread15RunFlipFunctionEPS0_b'), artController.hooks.Gc.runFlip);
-  } else {
-    let copyingPhase = null;
-    if (apiLevel > 28) {
-      copyingPhase = Module.findExportByName('libart.so', '_ZN3art2gc9collector17ConcurrentCopying12CopyingPhaseEv');
-    } else if (apiLevel > 22) {
-      copyingPhase = Module.findExportByName('libart.so', '_ZN3art2gc9collector17ConcurrentCopying12MarkingPhaseEv');
     }
-    if (copyingPhase !== null) {
-      Interceptor.attach(copyingPhase, artController.hooks.Gc.copyingPhase);
+    let adrp, add;
+    let adrp_add_pattern = '?1 ?? FF ?0 21 ?? ?? 91';
+    let adrp_add_in_CopyingPhase_func;
+    const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0];
+    for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) {
+      let disasm = Instruction.parse(match.address);
+      if (disasm.mnemonic === "adrp") {
+        adrp = disasm.operands.find(op => op.type === 'imm')?.value;
+        disasm = Instruction.parse(disasm.next);
+        if (disasm.mnemonic !== "add") {
+          disasm = Instruction.parse(disasm.next);
+        }
+        add = disasm.operands.find(op => op.type === 'imm')?.value;
+        if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === CopyingPhase_string_found_addr.toString()) {
+          if (adrp_add_in_CopyingPhase_func === undefined) {
+            adrp_add_in_CopyingPhase_func = match.address;
+          }
+          for (let off = 0;; off += 4) {
+            disasm = Instruction.parse(adrp_add_in_CopyingPhase_func.sub(off));
+            if (disasm.mnemonic === "sub") {
+              disasm = Instruction.parse(disasm.next);
+              if (disasm.mnemonic === "stp") {
+                copyingPhase = disasm.address.sub(0x4);
+                break;
+              }
+            }
+          }
+          break;
+        }
+      }
     }
   }
+  if (copyingPhase !== null) {
+    Interceptor.attach(copyingPhase, artController.hooks.Gc.copyingPhase);
+  }
+
+  let runFlip = null;
+  runFlip = Module.findExportByName('libart.so', '_ZN3art6Thread15RunFlipFunctionEPS0_b');
+  if (runFlip === null) {
+    runFlip = Module.findExportByName('libart.so', '_ZN3art6Thread15RunFlipFunctionEPS0_'); // api 35
+  }
+  if (runFlip !== null) {
+    Interceptor.attach(runFlip, artController.hooks.Gc.runFlip);
+  }
 }
 
 const artGetOatQuickMethodHeaderInlinedCopyHandler = {
@@ -3926,8 +3962,78 @@ const threadStateTransitionRecompilers = {
 
 function makeArtThreadStateTransitionImpl (vm, env, callback) {
   const envVtable = env.handle.readPointer();
-  const exceptionClearImpl = envVtable.add(ENV_VTABLE_OFFSET_EXCEPTION_CLEAR).readPointer();
-  const nextFuncImpl = envVtable.add(ENV_VTABLE_OFFSET_FATAL_ERROR).readPointer();
+  let exceptionClearImpl = envVtable.add(ENV_VTABLE_OFFSET_EXCEPTION_CLEAR).readPointer();
+  let nextFuncImpl = envVtable.add(ENV_VTABLE_OFFSET_FATAL_ERROR).readPointer();
+  let checkFatalError = Module.enumerateSymbolsSync('libart.so').filter(m => m.name.indexOf('art3JNI') >= 0 &&
+                                                                            m.name.indexOf('FatalError') >=0 &&
+                                                                            m.address.toString() === nextFuncImpl.toString())[0];
+
+  if (Process.arch === 'arm64' && checkFatalError === undefined) {
+    let JNI_FatalError_Called_string_found_addr;
+    let JNI_FatalError_Called_string = '4A 4E 49 20 46 61 74 61 6C 45 72 72 6F 72 20 63 61 6C';
+    const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0];
+    for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, JNI_FatalError_Called_string)) {
+      if (match) {
+        JNI_FatalError_Called_string_found_addr = match.address.toString();
+        break;
+      }
+    }
+
+    let adrp, add;
+    let adrp_add_pattern = '?1 ?? FF ?0 21 ?? ?? 91';
+    let adrp_add_in_JNI_false_FatalError_func;
+    let adrp_add_in_JNI_true_FatalError_func;
+    let JNI_true_FatalError_func;
+    let JNI_ExceptionClear_func;
+    const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0];
+    for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) {
+      let disasm = Instruction.parse(match.address);
+      if (disasm.mnemonic === "adrp") {
+        adrp = disasm.operands.find(op => op.type === 'imm')?.value;
+        disasm = Instruction.parse(disasm.next);
+        if (disasm.mnemonic !== "add") {
+          disasm = Instruction.parse(disasm.next);
+        }
+        add = disasm.operands.find(op => op.type === 'imm')?.value;
+        if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === JNI_FatalError_Called_string_found_addr.toString()) {
+          if (adrp_add_in_JNI_false_FatalError_func === undefined) {
+            adrp_add_in_JNI_false_FatalError_func = match.address;
+            continue;
+          }
+          if (adrp_add_in_JNI_true_FatalError_func === undefined) {
+            adrp_add_in_JNI_true_FatalError_func = match.address;
+          }
+          for (let off = 0;; off += 4) {
+            disasm = Instruction.parse(adrp_add_in_JNI_true_FatalError_func.sub(off));
+            if (disasm.mnemonic === "sub") {
+              disasm = Instruction.parse(disasm.next);
+              if (disasm.mnemonic === "stp") {
+                JNI_true_FatalError_func = disasm.address.sub(0x4);
+                break;
+              }
+            }
+          }
+          if (JNI_true_FatalError_func !== undefined) {
+            for (let off = 0;; off += 4) {
+              disasm = Instruction.parse(JNI_true_FatalError_func.sub(0x4).sub(off));
+              if (disasm.mnemonic === "sub") {
+                disasm = Instruction.parse(disasm.next);
+                if (disasm.mnemonic === "stp") {
+                  JNI_ExceptionClear_func = disasm.address.sub(0x4);
+                  break;
+                }
+              }
+            }
+          }
+          break;
+        }
+      }
+    }
+    if (JNI_true_FatalError_func !== undefined && JNI_ExceptionClear_func !== undefined) {
+      exceptionClearImpl = JNI_ExceptionClear_func;
+      nextFuncImpl = JNI_true_FatalError_func;
+    }
+  }
 
   const recompile = threadStateTransitionRecompilers[Process.arch];
   if (recompile === undefined) {

@hackcatml
Copy link
Author

hackcatml commented Oct 2, 2024

The issue was resolved by applying the above patch.
But when spawning the app, the Failed to reach single-threaded state error frequently occurred, causing the phone to soft reboot.
By referring to this commit, I modified frida-core/lib/payload/cloak.vala as follows, and the issue was resolved.

diff --git a/lib/payload/cloak.vala b/lib/payload/cloak.vala
index f0ca3f5e..b2e703ee 100644
--- a/lib/payload/cloak.vala
+++ b/lib/payload/cloak.vala
@@ -1,3 +1,4 @@
+// https://github.com/frida/frida-core/blob/b1fab8470aaa2edb4606d740466cd1446ed88918/lib/payload/cloak.vala
 namespace Frida {
 	public class ThreadIgnoreScope {
 		public enum Kind {
@@ -42,122 +43,101 @@ namespace Frida {
 		}
 	}
 
-#if ANDROID
+#if LINUX
 	public class ThreadCountCloaker : Object {
-		private ReadFunc * read_slot;
-		private static ReadFunc old_read_impl;
-
-		private static string expected_magic = "%u (".printf (Posix.getpid ());
-
-		[CCode (has_target = false)]
-		private delegate ssize_t ReadFunc (int fd, void * buf, size_t count);
+		private ReadListener listener;
 
 		construct {
-			Gum.Module.enumerate_imports ("libart.so", imp => {
-				if (imp.name == "read") {
-					read_slot = (ReadFunc *) imp.slot;
-					return false;
-				}
-				return true;
-			});
-			if (read_slot != null)
-				old_read_impl = update_read_slot (on_read);
+			listener = new ReadListener ();
+			Gum.Interceptor.obtain ().attach (
+				(void*) Gum.Module.find_export_by_name (Gum.Process.query_libc_name (), "read"),
+				listener);
 		}
 
 		~ThreadCountCloaker () {
-			if (read_slot != null)
-				update_read_slot (old_read_impl);
+			//  Gum.Interceptor.obtain ().detach (listener);
 		}
 
-		private ReadFunc update_read_slot (ReadFunc new_impl) {
-			Gum.PageProtection old_prot = READ;
-			Gum.Memory.query_protection (read_slot, out old_prot);
+		private class ReadListener : Object, Gum.InvocationListener {
+			private static string expected_magic = "%u (".printf (Posix.getpid ());
 
-			bool is_writable = (old_prot & Gum.PageProtection.WRITE) != 0;
-			if (!is_writable)
-				Gum.mprotect (read_slot, sizeof (void *), old_prot | WRITE);
-
-			ReadFunc old_impl = *read_slot;
-			*read_slot = new_impl;
+			public void on_enter (Gum.InvocationContext context) {
+				Invocation * invocation = context.get_listener_invocation_data (sizeof (Invocation));
+				invocation.fd = (int) context.get_nth_argument (0);
+				invocation.buf = context.get_nth_argument (1);
+				invocation.count = (size_t) context.get_nth_argument (2);
+			}
 
-			if (!is_writable)
-				Gum.mprotect (read_slot, sizeof (void *), old_prot);
+			public void on_leave (Gum.InvocationContext context) {
+				var n = (ssize_t) context.get_return_value ();
+				if (n > 0) {
+					Invocation * invocation = context.get_listener_invocation_data (sizeof (Invocation));
+					if (file_content_might_be_from_proc_self_stat (invocation.buf, n)) {
+						try {
+							if (file_descriptor_is_proc_self_stat (invocation.fd)) {
+								unowned string raw_str = (string) invocation.buf;
+								string str = raw_str.substring (0, n);
+
+								MatchInfo info;
+								if (/^(\d+ \(.+\)(?: [^ ]+){17}) \d+ (.+)/s.match (str, 0, out info)) {
+									string fields_before = info.fetch (1);
+									string fields_after = info.fetch (2);
+
+									// We cannot simply use the value we got from the kernel and subtract the number of cloaked threads,
+									// as there's a chance the total may have changed in the last moment.
+									uint num_uncloaked_threads = query_num_uncloaked_threads ();
+
+									string adjusted_str = "%s %u %s".printf (fields_before, num_uncloaked_threads, fields_after);
+
+									var adjusted_length = adjusted_str.length;
+									if (adjusted_length <= invocation.count) {
+										Memory.copy (invocation.buf, adjusted_str, adjusted_length);
+										context.replace_return_value ((void *) adjusted_length);
+									}
+								}
+							}
+						} catch (FileError e) {
+						}
+					}
+				}
+			}
 
-			return old_impl;
-		}
+			private static bool file_content_might_be_from_proc_self_stat (void * content, ssize_t size) {
+				if (size < expected_magic.length)
+					return false;
+				if (Memory.cmp (content, expected_magic, expected_magic.length) != 0)
+					return false;
+				unowned string raw_str = (string) content;
+				return raw_str[size - 1] == '\n';
+			}
 
-		private static ssize_t on_read (int fd, void * buf, size_t count) {
-			var n = old_read_impl (fd, buf, count);
-			if (n <= 0)
-				return n;
+			private static bool file_descriptor_is_proc_self_stat (int fd) throws FileError {
+				string path = FileUtils.read_link ("/proc/self/fd/%d".printf (fd));
+				uint pid = Posix.getpid ();
+				return (path == "/proc/%u/stat".printf (pid)) ||
+					(path == "/proc/%u/task/%u/stat".printf (pid, pid));
+			}
 
-			if (!file_content_might_be_from_proc_self_stat (buf, n))
+			private static uint query_num_uncloaked_threads () throws FileError {
+				uint n = 0;
+				var dir = Dir.open ("/proc/self/task");
+				string? name;
+				while ((name = dir.read_name ()) != null) {
+					var tid = uint.parse (name);
+					if (!Gum.Cloak.has_thread (tid))
+						n++;
+				}
 				return n;
-
-			try {
-				if (!file_descriptor_is_proc_self_stat (fd))
-					return n;
-
-				unowned string raw_str = (string) buf;
-				string str = raw_str.substring (0, n);
-
-				MatchInfo info;
-				if (!/^(\d+ \(.+\)(?: [^ ]+){17}) \d+ (.+)/s.match (str, 0, out info))
-					return n;
-				string fields_before = info.fetch (1);
-				string fields_after = info.fetch (2);
-
-				// We cannot simply use the value we got from the kernel and subtract the number of cloaked threads,
-				// as there's a chance the total may have changed in the last moment.
-				uint num_uncloaked_threads = query_num_uncloaked_threads ();
-
-				string adjusted_str = "%s %u %s".printf (fields_before, num_uncloaked_threads, fields_after);
-
-				var adjusted_length = adjusted_str.length;
-				if (adjusted_length > count)
-					return n;
-				Memory.copy (buf, adjusted_str, adjusted_length);
-				n = adjusted_length;
-			} catch (FileError e) {
 			}
 
-			return n;
-		}
-
-		private static bool file_content_might_be_from_proc_self_stat (void * content, ssize_t size) {
-			if (size < expected_magic.length)
-				return false;
-			if (Memory.cmp (content, expected_magic, expected_magic.length) != 0)
-				return false;
-			unowned string raw_str = (string) content;
-			return raw_str[size - 1] == '\n';
-		}
-
-		private static bool file_descriptor_is_proc_self_stat (int fd) throws FileError {
-			string path = FileUtils.read_link ("/proc/self/fd/%d".printf (fd));
-			uint pid = Posix.getpid ();
-			return (path == "/proc/%u/stat".printf (pid)) ||
-				(path == "/proc/%u/task/%u/stat".printf (pid, pid));
-		}
-
-		private static uint query_num_uncloaked_threads () throws FileError {
-			uint n = 0;
-			var dir = Dir.open ("/proc/self/task");
-			string? name;
-			while ((name = dir.read_name ()) != null) {
-				var tid = uint.parse (name);
-				if (!Gum.Cloak.has_thread (tid))
-					n++;
+			private struct Invocation {
+				public int fd;
+				public void * buf;
+				public size_t count;
 			}
-			return n;
 		}
 	}
-#else
-	public class ThreadCountCloaker : Object {
-	}
-#endif
 
-#if LINUX
 	public class ThreadListCloaker : Object, DirListFilter {
 		private string our_dir_by_pid;
 		private DirListCloaker cloaker;
@@ -464,6 +444,9 @@ namespace Frida {
 		public abstract bool matches_file (string name);
 	}
 #else
+	public class ThreadCountCloaker : Object {
+	}
+
 	public class ThreadListCloaker : Object {
 	}

@radubogdan2k
Copy link

@hackcatml Unfortunately, this has the same problem as the original PR. On certain phones (e.g. fully updated S21 Ultra, including the latest Google Play update), the function passed to Java.perform never gets called. No exceptions are thrown but the frida script is basically useless :(

@hackcatml
Copy link
Author

@hackcatml Unfortunately, this has the same problem as the original PR. On certain phones (e.g. fully updated S21 Ultra, including the latest Google Play update), the function passed to Java.perform never gets called. No exceptions are thrown but the frida script is basically useless :(

Try this.

Java.perform(function()
{
  Java.deoptimizeEverything();
  // Code
});

@radubogdan2k
Copy link

Java.deoptimizeEverything();

No change :(

@hackcatml
Copy link
Author

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960).
But Java hooking feels unstable.
It might be better to wait for the official Frida update.

image

@radubogdan2k
Copy link

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960). But Java hooking feels unstable. It might be better to wait for the official Frida update.

image

What phone are you using?

@radubogdan2k
Copy link

It might be better to wait for the official Frida update.

Definitely, but it seems like nobody's working on it :( Ole said he doesn't have the time and everybody else that gave it a go seems a bit stuck...

@hackcatml
Copy link
Author

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960). But Java hooking feels unstable. It might be better to wait for the official Frida update.
image

What phone are you using?

Pixel 4a, Android 13 with August 1 Google Play system update

@radubogdan2k
Copy link

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960). But Java hooking feels unstable. It might be better to wait for the official Frida update.
image

What phone are you using?

Pixel 4a, Android 13 with August 1 Google Play system update

I tried several phones, includ a Pixel 4a. The Pixel was the only one on which it worked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment