From b872ffd1b9da7daa2c6c8123462b3e009fe76f00 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:53:09 +0100 Subject: [PATCH] fix: replace patched zip early in input files (#2302) --- .../main/java/jadx/api/JadxDecompiler.java | 5 +- .../java/jadx/core/utils/files/ZipFile.java | 179 +--------------- .../java/jadx/core/utils/files/ZipPatch.java | 199 ++++++++++++++++++ 3 files changed, 207 insertions(+), 176 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 1b0c1a52ef4..4cacd98ecba 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -50,6 +50,7 @@ import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; +import jadx.core.utils.files.ZipPatch; import jadx.core.utils.tasks.TaskExecutor; import jadx.core.xmlgen.ResourcesSaver; @@ -141,7 +142,9 @@ public void reloadPasses() { private void loadInputFiles() { loadedInputs.clear(); - List inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath); + List inputs = ZipPatch.patchZipFiles(args.getInputFiles()); + args.setInputFiles(inputs); + List inputPaths = Utils.collectionMap(inputs, File::toPath); List inputFiles = FileUtils.expandDirs(inputPaths); long start = System.currentTimeMillis(); for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) { 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 571ad7876a4..c9a366d34d8 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 @@ -1,17 +1,13 @@ 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.List; -import java.util.stream.Collectors; +/** + * Deprecated zip file wrapper + */ public class ZipFile extends java.util.zip.ZipFile { public ZipFile(File file) throws IOException { @@ -31,173 +27,6 @@ public ZipFile(String name) throws IOException { } 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 cDirEntriesToFix = new ArrayList(); - var localHeaders = new ArrayList(); - List localHeaderToFix; - - try (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())); - - 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; - } - - localHeaderToFix = localHeaders - .stream() - .filter(off -> !readHeaderVexxed(raFile, off).validCompression()) - .collect(Collectors.toList()); - - if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) { - return file; - } - } - - var newFile = copyFile(file); - - try (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 = FileUtils.createTempFile(file.getName()).toFile(); - 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; + super(file, mode, charset); } } diff --git a/jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java b/jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java new file mode 100644 index 00000000000..a08acc491bd --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java @@ -0,0 +1,199 @@ +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.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ZipPatch { + private static final Logger LOG = LoggerFactory.getLogger(ZipPatch.class); + + public static List patchZipFiles(List inputs) { + List result = new ArrayList<>(inputs.size()); + for (File input : inputs) { + try { + result.add(patchZipFile(input)); + } catch (Throwable e) { + LOG.warn("Failed to patch zip file: {}", input.getAbsolutePath(), e); + result.add(input); + } + } + return result; + } + + private static File patchZipFile(File file) throws IOException { + String fileName = file.getPath().toLowerCase(); + if (!fileName.endsWith(".apk") && !fileName.endsWith(".zip")) { + return file; + } + + var cDirEntriesToFix = new ArrayList(); + var localHeaders = new ArrayList(); + List localHeaderToFix; + + try (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())); + + 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; + } + + localHeaderToFix = localHeaders + .stream() + .filter(off -> !readHeaderVexxed(raFile, off).validCompression()) + .collect(Collectors.toList()); + + if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) { + return file; + } + } + + var newFile = copyFile(file); + + try (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); + } + } + LOG.info("Input zip file patched: {}", file.getAbsolutePath()); + 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 = FileUtils.createTempFile(file.getName()).toFile(); + 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; + } +}