From e2a0d2f05a9f1c0c0cb851aa55eeb4c2554a8f58 Mon Sep 17 00:00:00 2001 From: vvb2060 Date: Mon, 12 Jul 2021 14:29:52 +0800 Subject: [PATCH] [core]Load module in memory (#809) --- .../de/robv/android/xposed/XposedInit.java | 3 +- .../lspd/util/ClassPathURLStreamHandler.java | 108 ++++++++++ .../lspd/util/CompoundEnumeration.java | 34 ++++ .../java/org/lsposed/lspd/util/Handler.java | 192 ++++++++++++++++++ .../util/InMemoryDelegateLastClassLoader.java | 115 +++++++++++ .../java/hidden/ByteBufferDexClassLoader.java | 11 + .../dalvik/system/BaseDexClassLoader.java | 5 + .../src/main/java/sun/net/www/ParseUtil.java | 7 + 8 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java create mode 100644 core/src/main/java/org/lsposed/lspd/util/CompoundEnumeration.java create mode 100644 core/src/main/java/org/lsposed/lspd/util/Handler.java create mode 100644 core/src/main/java/org/lsposed/lspd/util/InMemoryDelegateLastClassLoader.java create mode 100644 hiddenapi-bridge/src/main/java/hidden/ByteBufferDexClassLoader.java create mode 100644 hiddenapi-stubs/src/main/java/sun/net/www/ParseUtil.java diff --git a/core/src/main/java/de/robv/android/xposed/XposedInit.java b/core/src/main/java/de/robv/android/xposed/XposedInit.java index 7c9a4ffc..15c146ac 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/core/src/main/java/de/robv/android/xposed/XposedInit.java @@ -46,6 +46,7 @@ import android.util.Log; import org.lsposed.lspd.nativebridge.NativeAPI; import org.lsposed.lspd.nativebridge.ResourcesHook; +import org.lsposed.lspd.util.InMemoryDelegateLastClassLoader; import java.io.BufferedReader; import java.io.File; @@ -373,7 +374,7 @@ public final class XposedInit { librarySearchPath.append(apk).append("!/lib/").append(abi).append(File.pathSeparator); } ClassLoader initLoader = XposedInit.class.getClassLoader(); - ClassLoader mcl = new DelegateLastClassLoader(apk, librarySearchPath.toString(), initLoader); + ClassLoader mcl = InMemoryDelegateLastClassLoader.loadApk(new File(apk), librarySearchPath.toString(), initLoader); try { if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) { diff --git a/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java b/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java new file mode 100644 index 00000000..edd54c5c --- /dev/null +++ b/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java @@ -0,0 +1,108 @@ +package org.lsposed.lspd.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import sun.net.www.ParseUtil; + +public final class ClassPathURLStreamHandler extends Handler { + private final String fileUri; + private final JarFile jarFile; + + public ClassPathURLStreamHandler(String jarFileName) throws IOException { + jarFile = new JarFile(jarFileName); + fileUri = new File(jarFileName).toURI().toString(); + } + + public URL getEntryUrlOrNull(String entryName) { + if (jarFile.getEntry(entryName) != null) { + try { + String encodedName = ParseUtil.encodePath(entryName, false); + return new URL("jar", null, -1, fileUri + "!/" + encodedName, this); + } catch (MalformedURLException e) { + throw new RuntimeException("Invalid entry name", e); + } + } + return null; + } + + @Override + protected URLConnection openConnection(URL url) throws IOException { + return new ClassPathURLConnection(url); + } + + private class ClassPathURLConnection extends JarURLConnection { + private JarFile connectionJarFile = null; + private ZipEntry jarEntry = null; + private InputStream jarInput = null; + private boolean closed = false; + + private ClassPathURLConnection(URL url) throws MalformedURLException { + super(url); + } + + @Override + public void connect() throws IOException { + if (closed) { + throw new IllegalStateException("JarURLConnection has been closed"); + } + if (!connected) { + jarEntry = jarFile.getEntry(getEntryName()); + if (jarEntry == null) { + throw new FileNotFoundException("URL=" + url + ", zipfile=" + jarFile.getName()); + } + connected = true; + } + } + + @Override + public JarFile getJarFile() throws IOException { + connect(); + if (connectionJarFile != null) return connectionJarFile; + return connectionJarFile = new JarFile(jarFile.getName()); + } + + @Override + public InputStream getInputStream() throws IOException { + connect(); + if (jarInput != null) return jarInput; + return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) { + @Override + public void close() throws IOException { + super.close(); + closed = true; + jarFile.close(); + if (connectionJarFile != null) connectionJarFile.close(); + } + }; + } + + @Override + public String getContentType() { + String cType = guessContentTypeFromName(getEntryName()); + if (cType == null) { + cType = "content/unknown"; + } + return cType; + } + + @Override + public int getContentLength() { + try { + connect(); + return (int) getJarEntry().getSize(); + } catch (IOException ignored) { + } + return -1; + } + } +} diff --git a/core/src/main/java/org/lsposed/lspd/util/CompoundEnumeration.java b/core/src/main/java/org/lsposed/lspd/util/CompoundEnumeration.java new file mode 100644 index 00000000..1f2d6cd9 --- /dev/null +++ b/core/src/main/java/org/lsposed/lspd/util/CompoundEnumeration.java @@ -0,0 +1,34 @@ +package org.lsposed.lspd.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class CompoundEnumeration implements Enumeration { + private final Enumeration[] enums; + private int index = 0; + + public CompoundEnumeration(Enumeration[] enums) { + this.enums = enums; + } + + private boolean next() { + while (index < enums.length) { + if (enums[index] != null && enums[index].hasMoreElements()) { + return true; + } + index++; + } + return false; + } + + public boolean hasMoreElements() { + return next(); + } + + public E nextElement() { + if (!next()) { + throw new NoSuchElementException(); + } + return enums[index].nextElement(); + } +} diff --git a/core/src/main/java/org/lsposed/lspd/util/Handler.java b/core/src/main/java/org/lsposed/lspd/util/Handler.java new file mode 100644 index 00000000..f5fec900 --- /dev/null +++ b/core/src/main/java/org/lsposed/lspd/util/Handler.java @@ -0,0 +1,192 @@ +package org.lsposed.lspd.util; + +import java.net.MalformedURLException; +import java.net.URL; + +public abstract class Handler extends java.net.URLStreamHandler { + + private static final String separator = "!/"; + + private static int indexOfBangSlash(String spec) { + int indexOfBang = spec.length(); + while ((indexOfBang = spec.lastIndexOf('!', indexOfBang)) != -1) { + if ((indexOfBang != (spec.length() - 1)) && + (spec.charAt(indexOfBang + 1) == '/')) { + return indexOfBang + 1; + } else { + indexOfBang--; + } + } + return -1; + } + + @Override + protected boolean sameFile(URL u1, URL u2) { + if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) + return false; + + String file1 = u1.getFile(); + String file2 = u2.getFile(); + int sep1 = file1.indexOf(separator); + int sep2 = file2.indexOf(separator); + + if (sep1 == -1 || sep2 == -1) { + return super.sameFile(u1, u2); + } + + String entry1 = file1.substring(sep1 + 2); + String entry2 = file2.substring(sep2 + 2); + + if (!entry1.equals(entry2)) + return false; + + URL enclosedURL1, enclosedURL2; + try { + enclosedURL1 = new URL(file1.substring(0, sep1)); + enclosedURL2 = new URL(file2.substring(0, sep2)); + } catch (MalformedURLException unused) { + return super.sameFile(u1, u2); + } + + return super.sameFile(enclosedURL1, enclosedURL2); + } + + @Override + protected int hashCode(URL u) { + int h = 0; + + String protocol = u.getProtocol(); + if (protocol != null) + h += protocol.hashCode(); + + String file = u.getFile(); + int sep = file.indexOf(separator); + + if (sep == -1) + return h + file.hashCode(); + + URL enclosedURL; + String fileWithoutEntry = file.substring(0, sep); + try { + enclosedURL = new URL(fileWithoutEntry); + h += enclosedURL.hashCode(); + } catch (MalformedURLException unused) { + h += fileWithoutEntry.hashCode(); + } + + String entry = file.substring(sep + 2); + h += entry.hashCode(); + + return h; + } + + + @Override + @SuppressWarnings("deprecation") + protected void parseURL(URL url, String spec, int start, int limit) { + String file = null; + String ref = null; + // first figure out if there is an anchor + int refPos = spec.indexOf('#', limit); + boolean refOnly = refPos == start; + if (refPos > -1) { + ref = spec.substring(refPos + 1); + if (refOnly) { + file = url.getFile(); + } + } + // then figure out if the spec is + // 1. absolute (jar:) + // 2. relative (i.e. url + foo/bar/baz.ext) + // 3. anchor-only (i.e. url + #foo), which we already did (refOnly) + boolean absoluteSpec = false; + if (spec.length() >= 4) { + absoluteSpec = spec.substring(0, 4).equalsIgnoreCase("jar:"); + } + spec = spec.substring(start, limit); + + if (absoluteSpec) { + file = parseAbsoluteSpec(spec); + } else if (!refOnly) { + file = parseContextSpec(url, spec); + + // Canonize the result after the bangslash + int bangSlash = indexOfBangSlash(file); + String toBangSlash = file.substring(0, bangSlash); + String afterBangSlash = file.substring(bangSlash); + afterBangSlash = canonizeString(afterBangSlash); + file = toBangSlash + afterBangSlash; + } + setURL(url, "jar", "", -1, file, ref); + } + + private String canonizeString(String file) { + int i; + int lim; + + // Remove embedded /../ + while ((i = file.indexOf("/../")) >= 0) { + if ((lim = file.lastIndexOf('/', i - 1)) >= 0) { + file = file.substring(0, lim) + file.substring(i + 3); + } else { + file = file.substring(i + 3); + } + } + // Remove embedded /./ + while ((i = file.indexOf("/./")) >= 0) { + file = file.substring(0, i) + file.substring(i + 2); + } + // Remove trailing .. + while (file.endsWith("/..")) { + i = file.indexOf("/.."); + if ((lim = file.lastIndexOf('/', i - 1)) >= 0) { + file = file.substring(0, lim + 1); + } else { + file = file.substring(0, i); + } + } + // Remove trailing . + if (file.endsWith("/.")) + file = file.substring(0, file.length() - 1); + + return file; + } + + private String parseAbsoluteSpec(String spec) { + int index; + // check for !/ + if ((index = indexOfBangSlash(spec)) == -1) { + throw new NullPointerException("no !/ in spec"); + } + // test the inner URL + try { + String innerSpec = spec.substring(0, index - 1); + new URL(innerSpec); + } catch (MalformedURLException e) { + throw new NullPointerException("invalid url: " + + spec + " (" + e + ")"); + } + return spec; + } + + private String parseContextSpec(URL url, String spec) { + String ctxFile = url.getFile(); + // if the spec begins with /, chop up the jar back !/ + if (spec.startsWith("/")) { + int bangSlash = indexOfBangSlash(ctxFile); + if (bangSlash == -1) { + throw new NullPointerException("malformed " + "context url:" + url + ": no !/"); + } + ctxFile = ctxFile.substring(0, bangSlash); + } + if (!ctxFile.endsWith("/") && (!spec.startsWith("/"))) { + // chop up the last component + int lastSlash = ctxFile.lastIndexOf('/'); + if (lastSlash == -1) { + throw new NullPointerException("malformed " + "context url:" + url); + } + ctxFile = ctxFile.substring(0, lastSlash + 1); + } + return (ctxFile + spec); + } +} diff --git a/core/src/main/java/org/lsposed/lspd/util/InMemoryDelegateLastClassLoader.java b/core/src/main/java/org/lsposed/lspd/util/InMemoryDelegateLastClassLoader.java new file mode 100644 index 00000000..fd8e46cc --- /dev/null +++ b/core/src/main/java/org/lsposed/lspd/util/InMemoryDelegateLastClassLoader.java @@ -0,0 +1,115 @@ +package org.lsposed.lspd.util; + +import static de.robv.android.xposed.XposedBridge.TAG; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.zip.ZipFile; + +import hidden.ByteBufferDexClassLoader; + +@SuppressWarnings("ConstantConditions") +public final class InMemoryDelegateLastClassLoader extends ByteBufferDexClassLoader { + private final String apk; + + private InMemoryDelegateLastClassLoader(ByteBuffer[] dexBuffers, + String librarySearchPath, + ClassLoader parent, + String apk) { + super(dexBuffers, librarySearchPath, parent); + this.apk = apk; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + var cl = findLoadedClass(name); + if (cl != null) { + return cl; + } + try { + return Object.class.getClassLoader().loadClass(name); + } catch (ClassNotFoundException ignored) { + } + ClassNotFoundException fromSuper; + try { + return findClass(name); + } catch (ClassNotFoundException ex) { + fromSuper = ex; + } + try { + return getParent().loadClass(name); + } catch (ClassNotFoundException cnfe) { + throw fromSuper; + } + } + + @Override + protected URL findResource(String name) { + try { + var urlHandler = new ClassPathURLStreamHandler(apk); + return urlHandler.getEntryUrlOrNull(name); + } catch (IOException e) { + return null; + } + } + + @Override + protected Enumeration findResources(String name) { + var result = new ArrayList(); + var url = findResource(name); + if (url != null) result.add(url); + return Collections.enumeration(result); + } + + @Override + public URL getResource(String name) { + var resource = Object.class.getClassLoader().getResource(name); + if (resource != null) return resource; + resource = findResource(name); + if (resource != null) return resource; + final var cl = getParent(); + return (cl == null) ? null : cl.getResource(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + @SuppressWarnings("unchecked") + final var resources = (Enumeration[]) new Enumeration[]{ + Object.class.getClassLoader().getResources(name), + findResources(name), + getParent() == null ? null : getParent().getResources(name)}; + return new CompoundEnumeration<>(resources); + } + + public static InMemoryDelegateLastClassLoader loadApk(File apk, String librarySearchPath, ClassLoader parent) { + var byteBuffers = new ArrayList(); + try (var apkFile = new ZipFile(apk)) { + int secondaryNumber = 2; + for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; + dexFile = apkFile.getEntry("classes" + secondaryNumber + ".dex"), secondaryNumber++) { + try (var in = apkFile.getInputStream(dexFile)) { + var byteBuffer = ByteBuffer.allocate(in.available()); + byteBuffer.mark(); + Channels.newChannel(in).read(byteBuffer); + byteBuffer.reset(); + byteBuffers.add(byteBuffer); + } catch (IOException e) { + Log.w(TAG, "Can not read " + dexFile + " in " + apk, e); + } + } + } catch (IOException e) { + Log.e(TAG, "Can not open " + apk, e); + } + var dexBuffers = new ByteBuffer[byteBuffers.size()]; + return new InMemoryDelegateLastClassLoader(byteBuffers.toArray(dexBuffers), + librarySearchPath, parent, apk.getAbsolutePath()); + } +} diff --git a/hiddenapi-bridge/src/main/java/hidden/ByteBufferDexClassLoader.java b/hiddenapi-bridge/src/main/java/hidden/ByteBufferDexClassLoader.java new file mode 100644 index 00000000..2f659f60 --- /dev/null +++ b/hiddenapi-bridge/src/main/java/hidden/ByteBufferDexClassLoader.java @@ -0,0 +1,11 @@ +package hidden; + +import java.nio.ByteBuffer; + +import dalvik.system.BaseDexClassLoader; + +public class ByteBufferDexClassLoader extends BaseDexClassLoader { + public ByteBufferDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) { + super(dexFiles, librarySearchPath, parent); + } +} diff --git a/hiddenapi-stubs/src/main/java/dalvik/system/BaseDexClassLoader.java b/hiddenapi-stubs/src/main/java/dalvik/system/BaseDexClassLoader.java index a8f8d9b2..00ffd551 100644 --- a/hiddenapi-stubs/src/main/java/dalvik/system/BaseDexClassLoader.java +++ b/hiddenapi-stubs/src/main/java/dalvik/system/BaseDexClassLoader.java @@ -7,6 +7,7 @@ package dalvik.system; import java.io.File; import java.net.URL; +import java.nio.ByteBuffer; import java.util.Enumeration; public class BaseDexClassLoader extends ClassLoader { @@ -14,6 +15,10 @@ public class BaseDexClassLoader extends ClassLoader { throw new RuntimeException("Stub!"); } + public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) { + throw new RuntimeException("Stub!"); + } + protected Class findClass(String name) throws ClassNotFoundException { throw new RuntimeException("Stub!"); } diff --git a/hiddenapi-stubs/src/main/java/sun/net/www/ParseUtil.java b/hiddenapi-stubs/src/main/java/sun/net/www/ParseUtil.java new file mode 100644 index 00000000..a3755eec --- /dev/null +++ b/hiddenapi-stubs/src/main/java/sun/net/www/ParseUtil.java @@ -0,0 +1,7 @@ +package sun.net.www; + +public class ParseUtil { + public static String encodePath(String path, boolean flag) { + throw new RuntimeException("Stub!"); + } +}