From 8ff1912fee1cc4b0c83c4d28d4499c9d25b2e70f Mon Sep 17 00:00:00 2001 From: qfalconer Date: Thu, 10 Oct 2024 09:25:37 +0200 Subject: [PATCH 1/3] feat: patching semi-corrupted APKs --- .../java/jadx/cli/tools/ConvertArscFile.java | 2 +- .../main/java/jadx/api/ResourcesLoader.java | 2 +- .../jadx/api/plugins/utils/ZipSecurity.java | 2 +- .../java/jadx/core/utils/files/ZipFile.java | 200 ++++++++++++++++++ .../plugins/input/xapk/XapkCustomCodeInput.kt | 2 +- .../java/jadx/plugins/input/xapk/XapkUtils.kt | 2 +- 6 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java diff --git a/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java b/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java index a4bed122cb2..91bf04093bf 100644 --- a/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java +++ b/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java @@ -12,7 +12,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +19,7 @@ import jadx.api.JadxArgs; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.android.TextResMapFile; +import jadx.core.utils.files.ZipFile; import jadx.core.xmlgen.ResTableBinaryParser; /** diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 9b7edd11259..96e3cfdd561 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -9,7 +9,6 @@ import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +26,7 @@ import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; +import jadx.core.utils.files.ZipFile; import jadx.core.xmlgen.BinaryXMLParser; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ResContainer; diff --git a/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java b/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java index 5522d45f341..1c5eff98f05 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java +++ b/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java @@ -8,7 +8,6 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -16,6 +15,7 @@ import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.ZipFile; public class ZipSecurity { private static final Logger LOG = LoggerFactory.getLogger(ZipSecurity.class); diff --git a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java new file mode 100644 index 00000000000..4b888c644e0 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java @@ -0,0 +1,200 @@ +package jadx.core.utils.files; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.reflect.UndeclaredThrowableException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.stream.Collectors; + +public class ZipFile extends java.util.zip.ZipFile { + + public ZipFile(File file) throws IOException { + this(file, OPEN_READ); + } + + public ZipFile(File file, int mode) throws IOException { + this(file, mode, StandardCharsets.UTF_8); + } + + public ZipFile(String name, Charset charset) throws IOException { + this(new File(name), OPEN_READ, charset); + } + + public ZipFile(String name) throws IOException { + this(name, StandardCharsets.UTF_8); + } + + public ZipFile(File file, int mode, Charset charset) throws IOException { + super(patchZipFile(file), mode, charset); + } + + private static File patchZipFile(File file) throws IOException { + if (!file.getPath().toLowerCase().endsWith(".apk")) { + return file; + } + + var raFile = new RandomAccessFile(file, "r"); + var endOfCDirOffset = findEndOfCentralDir(raFile); + + raFile.seek(endOfCDirOffset + 0x10); + var cDirOffset = Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt())); + raFile.seek(endOfCDirOffset + 0x0a); + var cDirNumEntries = Short.toUnsignedLong(Short.reverseBytes(raFile.readShort())); + + var cDirEntriesToFix = new ArrayList(); + var localHeaders = new ArrayList(); + + for (long i = 0, off = cDirOffset; i < cDirNumEntries; i++) { + var info = readHeader(raFile, off); + + if (!info.validCompression()) { + cDirEntriesToFix.add(off); + } + + raFile.seek(off + 0x2a); + localHeaders.add(Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt()))); + + off += info.dataOffset; + } + + var localHeaderToFix = localHeaders + .stream() + .filter(off -> !readHeaderVexxed(raFile, off).validCompression()) + .collect(Collectors.toList()); + + if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) { + return file; + } + + var newFile = copyFile(file); + var newRaFile = new RandomAccessFile(newFile, "rwd"); + + for (var off : cDirEntriesToFix) { + var info = readHeader(newRaFile, off); + + newRaFile.seek(off + 0x0a); + newRaFile.writeShort(0); + + newRaFile.seek(off + 0x14); + newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); + + } + + for (var off : localHeaderToFix) { + var info = readHeader(newRaFile, off); + + newRaFile.seek(off + 0x08); + newRaFile.writeShort(0); + + newRaFile.seek(off + 0x12); + newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); + + newRaFile.seek(off + 0x1c); + newRaFile.writeShort(0); + + moveBlockBack(newRaFile, off + info.dataOffset, info.uncompressedSize, info.extraLen); + } + + return newFile; + } + + private static void moveBlockBack(RandomAccessFile file, long offset, long size, long delta) throws IOException { + var buffer = new byte[1024 * 1024]; + + while (size > 0) { + var len = (int) Math.min(buffer.length, size); + + file.seek(offset); + file.read(buffer, 0, len); + file.seek(offset - delta); + file.write(buffer, 0, len); + + size -= len; + offset += len; + } + } + + private static File copyFile(File file) throws IOException { + var newFile = File.createTempFile(file.getName(), ".apk"); + + try (var in = new FileInputStream(file)) { + try (var out = new FileOutputStream(newFile)) { + in.transferTo(out); + } + } + + return newFile; + } + + private static long findEndOfCentralDir(RandomAccessFile file) throws IOException { + var offset = file.length() - 0x15L + 1; + + do { + if (offset <= 0) { + throw new IllegalArgumentException("File is not a valid ZIP: End of central directory record not found"); + } + file.seek(--offset); + } while (Integer.reverseBytes(file.readInt()) != 0x06054b50); + + return offset; + } + + private static class HeaderInfo { + short compression; + long uncompressedSize; + long dataOffset; + long extraLen; + + boolean validCompression() { + return compression == 0x0 || compression == 0x8; + } + } + + private static HeaderInfo readHeaderVexxed(RandomAccessFile file, long offset) { + try { + return readHeader(file, offset); + } catch (IOException e) { + throw new UndeclaredThrowableException(e); + } + } + + private static HeaderInfo readHeader(RandomAccessFile file, long offset) throws IOException { + var info = new HeaderInfo(); + + file.seek(offset); + var signature = Integer.reverseBytes(file.readInt()); + + if (signature != 0x02014b50 && signature != 0x04034b50) { + throw new IllegalArgumentException( + String.format("Invalid ZIP header signature %x at offset %x", + signature, offset)); + } + + var isCentralHeader = signature == 0x02014b50; + var delta = isCentralHeader ? 0 : -2; + + file.seek(offset + 0x0a + delta); + info.compression = Short.reverseBytes(file.readShort()); + + file.seek(offset + 0x18 + delta); + info.uncompressedSize = Integer.toUnsignedLong(Integer.reverseBytes(file.readInt())); + + file.seek(offset + 0x1c + delta); + var nameLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort())); + info.extraLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort())); + var commentLen = 0L; + + if (isCentralHeader) { + commentLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort())); + } + + info.dataOffset = (isCentralHeader ? 0x2e : 0x1e) + nameLen + info.extraLen + commentLen; + + return info; + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt index 1038d11df1d..89db1df1550 100644 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt @@ -4,9 +4,9 @@ import jadx.api.plugins.input.ICodeLoader import jadx.api.plugins.input.JadxCodeInput import jadx.api.plugins.utils.CommonFileUtils import jadx.api.plugins.utils.ZipSecurity +import jadx.core.utils.files.ZipFile import java.io.File import java.nio.file.Path -import java.util.zip.ZipFile class XapkCustomCodeInput( private val plugin: XapkInputPlugin, diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt index 098663f68e2..9c7b9ad7ad7 100644 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt @@ -3,9 +3,9 @@ package jadx.plugins.input.xapk import com.google.gson.Gson import jadx.api.plugins.utils.ZipSecurity import jadx.core.utils.files.FileUtils +import jadx.core.utils.files.ZipFile import java.io.File import java.io.InputStreamReader -import java.util.zip.ZipFile object XapkUtils { fun getManifest(file: File): XapkManifest? { From c46745ccecf8265dfe2267d4a175a4608768d188 Mon Sep 17 00:00:00 2001 From: qfalconer Date: Thu, 10 Oct 2024 10:57:47 +0200 Subject: [PATCH 2/3] fix: using secure temp file creation --- jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java index 4b888c644e0..11253e594d2 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java @@ -8,6 +8,7 @@ import java.lang.reflect.UndeclaredThrowableException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.stream.Collectors; @@ -120,7 +121,7 @@ private static void moveBlockBack(RandomAccessFile file, long offset, long size, } private static File copyFile(File file) throws IOException { - var newFile = File.createTempFile(file.getName(), ".apk"); + var newFile = Files.createTempFile(file.getName(), ".apk").toFile(); try (var in = new FileInputStream(file)) { try (var out = new FileOutputStream(newFile)) { From 3fa1186c23d2f10238bcb04cdf8a85f71ecd0f04 Mon Sep 17 00:00:00 2001 From: qfalconer Date: Thu, 10 Oct 2024 12:55:23 +0200 Subject: [PATCH 3/3] fix: using TWR when handling the files --- .../java/jadx/core/utils/files/ZipFile.java | 85 ++++++++++--------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java index 11253e594d2..c23755d6233 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; +import java.util.List; import java.util.stream.Collectors; public class ZipFile extends java.util.zip.ZipFile { @@ -39,66 +40,70 @@ private static File patchZipFile(File file) throws IOException { return file; } - var raFile = new RandomAccessFile(file, "r"); - var endOfCDirOffset = findEndOfCentralDir(raFile); - - raFile.seek(endOfCDirOffset + 0x10); - var cDirOffset = Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt())); - raFile.seek(endOfCDirOffset + 0x0a); - var cDirNumEntries = Short.toUnsignedLong(Short.reverseBytes(raFile.readShort())); - var cDirEntriesToFix = new ArrayList(); var localHeaders = new ArrayList(); + List localHeaderToFix; - for (long i = 0, off = cDirOffset; i < cDirNumEntries; i++) { - var info = readHeader(raFile, off); + try (var raFile = new RandomAccessFile(file, "r")) { + var endOfCDirOffset = findEndOfCentralDir(raFile); - if (!info.validCompression()) { - cDirEntriesToFix.add(off); - } + raFile.seek(endOfCDirOffset + 0x10); + var cDirOffset = Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt())); + raFile.seek(endOfCDirOffset + 0x0a); + var cDirNumEntries = Short.toUnsignedLong(Short.reverseBytes(raFile.readShort())); - raFile.seek(off + 0x2a); - localHeaders.add(Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt()))); + for (long i = 0, off = cDirOffset; i < cDirNumEntries; i++) { + var info = readHeader(raFile, off); - off += info.dataOffset; - } + if (!info.validCompression()) { + cDirEntriesToFix.add(off); + } - var localHeaderToFix = localHeaders - .stream() - .filter(off -> !readHeaderVexxed(raFile, off).validCompression()) - .collect(Collectors.toList()); + raFile.seek(off + 0x2a); + localHeaders.add(Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt()))); - if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) { - return file; + off += info.dataOffset; + } + + localHeaderToFix = localHeaders + .stream() + .filter(off -> !readHeaderVexxed(raFile, off).validCompression()) + .collect(Collectors.toList()); + + if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) { + return file; + } } var newFile = copyFile(file); - var newRaFile = new RandomAccessFile(newFile, "rwd"); - for (var off : cDirEntriesToFix) { - var info = readHeader(newRaFile, off); + try (var newRaFile = new RandomAccessFile(newFile, "rwd")) { - newRaFile.seek(off + 0x0a); - newRaFile.writeShort(0); + for (var off : cDirEntriesToFix) { + var info = readHeader(newRaFile, off); - newRaFile.seek(off + 0x14); - newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); + newRaFile.seek(off + 0x0a); + newRaFile.writeShort(0); - } + newRaFile.seek(off + 0x14); + newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); - for (var off : localHeaderToFix) { - var info = readHeader(newRaFile, off); + } + + for (var off : localHeaderToFix) { + var info = readHeader(newRaFile, off); - newRaFile.seek(off + 0x08); - newRaFile.writeShort(0); + newRaFile.seek(off + 0x08); + newRaFile.writeShort(0); - newRaFile.seek(off + 0x12); - newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); + newRaFile.seek(off + 0x12); + newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); - newRaFile.seek(off + 0x1c); - newRaFile.writeShort(0); + newRaFile.seek(off + 0x1c); + newRaFile.writeShort(0); - moveBlockBack(newRaFile, off + info.dataOffset, info.uncompressedSize, info.extraLen); + moveBlockBack(newRaFile, off + info.dataOffset, info.uncompressedSize, info.extraLen); + } } return newFile;