From 40305d81c71ab12e52ded67913d1d89c693826bc Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Thu, 24 Dec 2020 00:27:19 +0330 Subject: [PATCH] dex extractor --- app/build.gradle.kts | 9 +- .../java/ir/armor/tachidesk/APKExtractor.java | 246 ++++++++++++++++++ .../main/kotlin/ir/armor/tachidesk/Main.kt | 101 ++++--- 3 files changed, 298 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/ir/armor/tachidesk/APKExtractor.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 24f3de3..2fea284 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,10 +14,10 @@ compileKotlin.kotlinOptions { } repositories { -// gradlePluginPortal() -// google() mavenCentral() - jcenter() + maven { + url = uri("http://repository-dex2jar.forge.cloudbees.com/release/") + } } dependencies { @@ -63,6 +63,9 @@ dependencies { val coroutinesVersion = "1.3.9" implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + // dex2jar + implementation("com.googlecode.d2j:dex-reader:2.0") + testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") diff --git a/app/src/main/java/ir/armor/tachidesk/APKExtractor.java b/app/src/main/java/ir/armor/tachidesk/APKExtractor.java new file mode 100644 index 0000000..8aa76d6 --- /dev/null +++ b/app/src/main/java/ir/armor/tachidesk/APKExtractor.java @@ -0,0 +1,246 @@ +package ir.armor.tachidesk; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +class APKExtractor { + // decompressXML -- Parse the 'compressed' binary form of Android XML docs + // such as for AndroidManifest.xml in .apk files + public static int endDocTag = 0x00100101; + public static int startTag = 0x00100102; + public static int endTag = 0x00100103; + + static void prt(String str) { + //System.err.print(str); + } + + public static String decompressXML(byte[] xml) { + + StringBuilder finalXML = new StringBuilder(); + + // Compressed XML file/bytes starts with 24x bytes of data, + // 9 32 bit words in little endian order (LSB first): + // 0th word is 03 00 08 00 + // 3rd word SEEMS TO BE: Offset at then of StringTable + // 4th word is: Number of strings in string table + // WARNING: Sometime I indiscriminently display or refer to word in + // little endian storage format, or in integer format (ie MSB first). + int numbStrings = LEW(xml, 4 * 4); + + // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets + // of the length/string data in the StringTable. + int sitOff = 0x24; // Offset of start of StringIndexTable + + // StringTable, each string is represented with a 16 bit little endian + // character count, followed by that number of 16 bit (LE) (Unicode) + // chars. + int stOff = sitOff + numbStrings * 4; // StringTable follows + // StrIndexTable + + // XMLTags, The XML tag tree starts after some unknown content after the + // StringTable. There is some unknown data after the StringTable, scan + // forward from this point to the flag for the start of an XML start + // tag. + int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd + // word. + // Scan forward until we find the bytes: 0x02011000(x00100102 in normal + // int) + for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) { + if (LEW(xml, ii) == startTag) { + xmlTagOff = ii; + break; + } + } // end of hack, scanning for start of first start tag + + // XML tags and attributes: + // Every XML start and end tag consists of 6 32 bit words: + // 0th word: 02011000 for startTag and 03011000 for endTag + // 1st word: a flag?, like 38000000 + // 2nd word: Line of where this tag appeared in the original source file + // 3rd word: FFFFFFFF ?? + // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS + // 5th word: StringIndex of Element Name + // (Note: 01011000 in 0th word means end of XML document, endDocTag) + + // Start tags (not end tags) contain 3 more words: + // 6th word: 14001400 meaning?? + // 7th word: Number of Attributes that follow this tag(follow word 8th) + // 8th word: 00000000 meaning?? + + // Attributes consist of 5 words: + // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF + // 1st word: StringIndex of Attribute Name + // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId + // used + // 3rd word: Flags? + // 4th word: str ind of attr value again, or ResourceId of value + + // TMP, dump string table to tr for debugging + // tr.addSelect("strings", null); + // for (int ii=0; ii"); + prtIndent(indent, "<" + name + sb + ">"); + indent++; + + } else if (tag0 == endTag) { // XML END TAG + indent--; + off += 6 * 4; // Skip over 6 words of endTag data + String name = compXmlString(xml, sitOff, stOff, nameSi); + finalXML.append(""); + prtIndent(indent, " (line " + startTagLineNo + + "-" + lineNo + ")"); + // tr.parent(); // Step back up the NobTree + + } else if (tag0 == endDocTag) { // END OF XML DOC TAG + break; + + } else { + prt(" Unrecognized tag code '" + Integer.toHexString(tag0) + + "' at offset " + off); + break; + } + } // end of while loop scanning tags and attributes of XML tree + //prt(" end at offset " + off); + return finalXML.toString(); + } // end of decompressXML + + public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) { + if (strInd < 0) + return null; + int strOff = stOff + LEW(xml, sitOff + strInd * 4); + return compXmlStringAt(xml, strOff); + } + + public static String spaces = " "; + + public static void prtIndent(int indent, String str) { + prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str); + } + + // compXmlStringAt -- Return the string stored in StringTable format at + // offset strOff. This offset points to the 16 bit string length, which + // is followed by that number of 16 bit (Unicode) chars. + public static String compXmlStringAt(byte[] arr, int strOff) { + int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff; + byte[] chars = new byte[strLen]; + for (int ii = 0; ii < strLen; ii++) { + chars[ii] = arr[strOff + 2 + ii * 2]; + } + return new String(chars); // Hack, just use 8 byte chars + } // end of compXmlStringAt + + // LEW -- Return value of a Little Endian 32 bit word from the byte array + // at offset off. + public static int LEW(byte[] arr, int off) { + return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000 + | arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF; + } // end of LEW + + public static Document loadXMLFromString(String xml) throws Exception { + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + return docBuilder.parse(new InputSource(new StringReader(xml))); + } + + public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException { + InputStream is = null; + ZipFile zip = null; + + zip = new ZipFile(filePath); + ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml"); + ZipEntry classesDex = zip.getEntry("classes.dex"); + is = zip.getInputStream(androidManifest); + + // write dex file + InputStream dexStream = zip.getInputStream(classesDex); + byte[] dexBuffer = new byte[dexStream.available()]; + FileOutputStream dexOs = new FileOutputStream(new File(dexPath)); + dexOs.write(dexBuffer); + dexOs.close(); + + + byte[] buf = new byte[10240]; + int bytesRead = is.read(buf); + + is.close(); + if (zip != null) { + zip.close(); + } + + String xml = APKExtractor.decompressXML(buf); + try { + Document xmlDoc = loadXMLFromString(xml); + String pkg = xmlDoc.getDocumentElement().getAttribute("package"); + NodeList nodes = xmlDoc.getElementsByTagName("meta-data"); + for (int i = 0; i < nodes.getLength(); i++) { + NamedNodeMap attributes = nodes.item(i).getAttributes(); + System.out.println(attributes.getNamedItem("name").getNodeValue()); + if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class")) + return pkg + attributes.getNamedItem("value").getNodeValue(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ir/armor/tachidesk/Main.kt b/app/src/main/kotlin/ir/armor/tachidesk/Main.kt index 213a79a..7c3e4fe 100644 --- a/app/src/main/kotlin/ir/armor/tachidesk/Main.kt +++ b/app/src/main/kotlin/ir/armor/tachidesk/Main.kt @@ -1,65 +1,56 @@ package ir.armor.tachidesk -import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.MangasPage -import kotlinx.coroutines.runBlocking -import okhttp3.Request -import okio.BufferedSink -import okio.buffer -import okio.sink -import rx.Observable -import java.io.File -import java.net.URL -import java.net.URLClassLoader -import kotlin.system.exitProcess - class Main { companion object { @JvmStatic fun main(args: Array) { - val contentRoot = "/tmp/tachidesk" - File(contentRoot).mkdirs() - - // get list of extensions - var apkToDownload: String = "" - runBlocking { - val api = ExtensionGithubApi() - apkToDownload = api.getApkUrl(api.findExtensions().first { - api.getApkUrl(it).endsWith("killsixbilliondemons-v1.2.3.apk") - }) - } - apkToDownload = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo/apk/tachiyomi-en.killsixbilliondemons-v1.2.3.apk" - println(apkToDownload) - - val apkFileName = apkToDownload.split("/").last() - val apkFilePath = "$contentRoot/$apkFileName" - val zipDirPath = apkFilePath.substringBefore(".apk") - val jarFilePath = "$contentRoot/$zipDirPath.jar" - - val request = Request.Builder().url(apkToDownload).build() - val response = NetworkHelper().client.newCall(request).execute(); - println(response.code) - - val downloadedFile = File(apkFilePath) - val sink: BufferedSink = downloadedFile.sink().buffer() - sink.writeAll(response.body!!.source()) - sink.close() - - Runtime.getRuntime().exec("unzip ${downloadedFile.absolutePath} -d $zipDirPath").waitFor() - Runtime.getRuntime().exec("dex2jar $zipDirPath/classes.dex -o $jarFilePath").waitFor() - - val child = URLClassLoader(arrayOf(URL("file:$jarFilePath")), this.javaClass.classLoader) - val classToLoad = Class.forName("eu.kanade.tachiyomi.extension.en.killsixbilliondemons.KillSixBillionDemons", true, child) - val instance = classToLoad.newInstance() as CatalogueSource - val result = instance.fetchPopularManga(1) - val mangasPage = result.toBlocking().first() as MangasPage - mangasPage.mangas.forEach{ - println(it.title) - } - exitProcess(0) +// val contentRoot = "/tmp/tachidesk" +// File(contentRoot).mkdirs() +// var sourcePkg = "" +// +// // get list of extensions +// var apkToDownload: String = "" +// runBlocking { +// val api = ExtensionGithubApi() +// val source = api.findExtensions().first { +// api.getApkUrl(it).endsWith("killsixbilliondemons-v1.2.3.apk") +// } +// apkToDownload = api.getApkUrl(source) +// sourcePkg = source.pkgName +// } +// apkToDownload = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo/apk/tachiyomi-en.killsixbilliondemons-v1.2.3.apk" +// println(apkToDownload) +// +// val apkFileName = apkToDownload.split("/").last() +// val apkFilePath = "$contentRoot/$apkFileName" +// val zipDirPath = apkFilePath.substringBefore(".apk") +// val jarFilePath = "$zipDirPath.jar" +// +// val request = Request.Builder().url(apkToDownload).build() +// val response = NetworkHelper().client.newCall(request).execute(); +// println(response.code) +// +// val downloadedFile = File(apkFilePath) +// val sink: BufferedSink = downloadedFile.sink().buffer() +// sink.writeAll(response.body!!.source()) +// sink.close() +// +// Runtime.getRuntime().exec("unzip ${downloadedFile.absolutePath} -d $zipDirPath").waitFor() +// Runtime.getRuntime().exec("dex2jar $zipDirPath/classes.dex -o $jarFilePath").waitFor() +// +// val child = URLClassLoader(arrayOf(URL("file:$jarFilePath")), this.javaClass.classLoader) +// val classToLoad = Class.forName("eu.kanade.tachiyomi.extension.en.killsixbilliondemons.KillSixBillionDemons", true, child) +// val instance = classToLoad.newInstance() as CatalogueSource +// val result = instance.fetchPopularManga(1) +// val mangasPage = result.toBlocking().first() as MangasPage +// mangasPage.mangas.forEach{ +// println(it.title) +// } +// exitProcess(0) + val apk = "/tmp/tachidesk/tachiyomi-en.killsixbilliondemons-v1.2.3.apk" + val dex = "/tmp/tachidesk/tachiyomi-en.killsixbilliondemons-v1.2.3.dex" + val pkg = APKExtractor.extract_dex_and_read_className(apk, dex) } } }